yang-db 0.1.3

个人使用数据库操作
Documentation
# Redis Lua 脚本支持

## 概述

yang-db 提供了对 Redis Lua 脚本的完整支持,基于 redis-rs 的原生 `Script` 类型实现。Lua 脚本允许在 Redis 服务器端执行复杂的原子操作,提高性能并保证操作的原子性。

## 主要特性

- **原子性执行**:脚本内的所有操作都是原子性的,不会被其他客户端的命令中断
- **性能优化**:自动使用 EVALSHA 命令缓存脚本,减少网络传输
- **类型安全**:支持类型化的返回值,利用 Rust 的类型系统
- **自动回退**:如果脚本不在缓存中,自动回退到 EVAL 命令

## 基本用法

### 创建和执行脚本

```rust
use yang_db::RedisClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = RedisClient::connect("redis://127.0.0.1:6379").await?;
    
    // 创建脚本
    let script = client.script(r#"return "Hello, Lua!""#);
    
    // 执行脚本
    let result: String = client.eval_script(&script, &[], &[]).await?;
    println!("结果: {}", result);
    
    Ok(())
}
```

### 使用 KEYS 和 ARGV 参数

```rust
// 创建脚本:使用 KEYS 和 ARGV 参数
let script = client.script(
    r#"
    redis.call('SET', KEYS[1], ARGV[1])
    return redis.call('GET', KEYS[1])
    "#
);

// 执行脚本
let result: String = client.eval_script(
    &script,
    &["my_key".to_string()],      // KEYS 参数
    &["my_value".to_string()]     // ARGV 参数
).await?;
```

## 实用示例

### 1. 原子性计数器增加

```rust
let script = client.script(
    r#"
    local current = redis.call('GET', KEYS[1])
    if not current then
        current = 0
    end
    local new_value = tonumber(current) + tonumber(ARGV[1])
    redis.call('SET', KEYS[1], new_value)
    return new_value
    "#
);

let result: i64 = client.eval_script(
    &script,
    &["counter".to_string()],
    &["10".to_string()]
).await?;

println!("新值: {}", result);
```

### 2. 条件更新(乐观锁)

```rust
// 只有当余额足够时才扣减
let script = client.script(
    r#"
    local balance = tonumber(redis.call('GET', KEYS[1]) or 0)
    local amount = tonumber(ARGV[1])
    if balance >= amount then
        redis.call('DECRBY', KEYS[1], amount)
        return 1
    else
        return 0
    end
    "#
);

let success: i64 = client.eval_script(
    &script,
    &["user:1000:balance".to_string()],
    &["100".to_string()]
).await?;

if success == 1 {
    println!("扣款成功");
} else {
    println!("余额不足");
}
```

### 3. 原子性转账

```rust
// 原子性地从一个账户转账到另一个账户
let script = client.script(
    r#"
    local from_balance = tonumber(redis.call('GET', KEYS[1]))
    local amount = tonumber(ARGV[1])
    
    if from_balance >= amount then
        redis.call('DECRBY', KEYS[1], amount)
        redis.call('INCRBY', KEYS[2], amount)
        return 1
    else
        return 0
    end
    "#
);

let success: i64 = client.eval_script(
    &script,
    &["account_a".to_string(), "account_b".to_string()],
    &["30".to_string()]
).await?;
```

### 4. 批量操作

```rust
// 批量获取多个键的值
let script = client.script(
    r#"
    local result = {}
    for i = 1, #KEYS do
        table.insert(result, redis.call('GET', KEYS[i]))
    end
    return result
    "#
);

let result: Vec<String> = client.eval_script(
    &script,
    &["key1".to_string(), "key2".to_string(), "key3".to_string()],
    &[]
).await?;
```

## 返回值类型

Lua 脚本支持多种返回值类型:

- **字符串**`String`
- **整数**`i64`
- **浮点数**`f64`
- **数组**`Vec<T>`
- **混合数组**`Vec<redis::Value>`
- **nil**`Option<T>`

示例:

```rust
// 返回字符串
let result: String = client.eval_script(&script, &[], &[]).await?;

// 返回整数
let result: i64 = client.eval_script(&script, &[], &[]).await?;

// 返回数组
let result: Vec<String> = client.eval_script(&script, &[], &[]).await?;

// 返回可选值
let result: Option<String> = client.eval_script(&script, &[], &[]).await?;
```

## 性能优化

### EVALSHA 自动缓存

redis-rs 的 `Script` 类型会自动处理脚本缓存:

1. **首次执行**:脚本被发送到 Redis 服务器并缓存,返回 SHA1 哈希值
2. **后续执行**:使用 EVALSHA 命令,只传输 SHA1 哈希值,节省网络带宽
3. **自动回退**:如果脚本不在缓存中(例如 Redis 重启),自动回退到 EVAL 命令

这个过程完全自动化,无需手动管理。

### 最佳实践

1. **复用脚本对象**:对于频繁执行的脚本,创建一次脚本对象并复用
2. **减少网络往返**:将多个操作合并到一个脚本中
3. **避免长时间运行**:Lua 脚本会阻塞 Redis,避免执行耗时操作
4. **使用局部变量**:在 Lua 中使用 `local` 关键字声明变量

## 错误处理

```rust
let result: Result<i64, _> = client.eval_script(
    &script,
    &["key".to_string()],
    &[]
).await;

match result {
    Ok(value) => println!("成功: {}", value),
    Err(e) => eprintln!("脚本执行失败: {}", e),
}
```

## 注意事项

1. **原子性**:脚本内的所有操作都是原子性的,但脚本执行期间会阻塞 Redis
2. **时间复杂度**:注意脚本的时间复杂度,避免在脚本中执行大量操作
3. **键命名**:使用 KEYS 参数传递键名,而不是在脚本中硬编码
4. **参数传递**:使用 ARGV 参数传递值,保持脚本的可复用性
5. **错误处理**:脚本中的错误会导致整个脚本失败,注意处理边界情况

## API 参考

### `RedisClient::script()`

创建 Lua 脚本对象。

```rust
pub fn script(&self, code: &str) -> redis::Script
```

**参数**:
- `code`: Lua 脚本代码

**返回**:
- `redis::Script` 对象

### `RedisClient::eval_script()`

执行 Lua 脚本。

```rust
pub async fn eval_script<T>(
    &self,
    script: &redis::Script,
    keys: &[String],
    args: &[String],
) -> Result<T>
where
    T: redis::FromRedisValue,
```

**参数**:
- `script`: 脚本对象(通过 `script()` 方法创建)
- `keys`: KEYS 参数列表(在脚本中通过 KEYS[1], KEYS[2] 访问)
- `args`: ARGV 参数列表(在脚本中通过 ARGV[1], ARGV[2] 访问)

**返回**:
- `Ok(T)`: 脚本执行成功,返回类型化结果
- `Err(DbError)`: 脚本执行失败

## 测试

项目包含完整的单元测试和集成测试:

```bash
# 运行单元测试
cargo test --test test_redis_script

# 运行集成测试(需要 Docker)
cargo test --test integration_redis_script
```

## 相关资源

- [Redis Lua 脚本官方文档]https://redis.io/docs/manual/programmability/eval-intro/
- [redis-rs Script 文档]https://docs.rs/redis/latest/redis/struct.Script.html
- [Lua 5.1 参考手册]https://www.lua.org/manual/5.1/