redis 拓展


事务

redis 事务

  1. redis的事务是通过multi,exec,discard 和watch这四个命令来完成的
  2. redis的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合,不能保证时时一致性,只能是最终一致性
  3. redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
  4. redis 不支持回滚操作, redis持久化但不保证数据的完整性

事务命令

multi: 用于标记事务块的开始,redis 会讲后续的命令逐个放入队列中,然后使用exec原子化地执行这个命令队列
exec:执行命令队列
discard:清除命令队列
watch:监视key
unwatch:清除监视key

事务机制

事务的执行

  1. 事务开始 在RedisClient 中,有属性flags 用来表示是否在事务中flags = redis_multi
  2. 命令入队 redisclient 将命令存放在事务队列中 (exec discard watch multi 除外)
  3. 事务队列 multiCmd *commands 用于存放命令
  4. 执行事务 redisclient 向服务器端发送exec命令,redisServer会遍历事务队列,执行队列中的命令,最后将执行的结果一次性返回给客户端。

如果某命令在入队过程中发生错误,redisClient 将flags 设置为redis_dirty_exec exec 命令将会失败返回

    typedef struct redisClient{
        int flags;
        multiState mstate;
    }redisClient;
    //事务状态
    typedef struct multiState {
        multiCmd *commands;
        int count;
    }multiState;
    //事务队列
    typedef struct multiCmd{
        robj **argv;
        int argc;
        struct redisCommand *cmd;
    } multiCmd;

Watch的执行

使用watch 命令监视数据库键,redisDb有一个watched_keys 字典,key 是某个被监视的数据的key,值是一个链表,记录了所有监视这个数据的客户端。
监视机制的触发:当修改数据后,监视这个数据的客户端的flags置为redis_dirty_cas,
事务执行:redisClient 向服务器端发送exec 命令,服务器判断redisClient的flags,如果为redis_dirty_cas,则清空事务队列。
```
typedef struct redisDb{
    //正在被watch 命令监视的键
    dict * watched_keys
} redisDb;

```

redis 的弱事务性

redis 语法错误
    整个事务的命令在队列里都消除 flags = multi_dirty
redis 运行错误
    在队列里正确的命令可以执行(弱事务性)
    弱事务性:
        在队列里正确的命令可以执行(非原子性)
        不支持回滚
redis 不支持事务回滚
    大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的
    redis为了性能方面就忽略了事务回滚(回滚记录历史版本)

Lua脚本

lua 脚本的命令是原子的,redisServer 在执行脚本命令中,不允许插入新的命令 lua 脚本的命令可以复制的,redisServer 在获得脚本后不执行,生成标识返回,client 根据标识就可以随时运行

EVAL/EVALSHA命令实现

EVAL 命令

通过执行redis的eval 命令,可以运行一段lua 脚本 eval script numbers key [ky...] arg [arg...] script 参数,是一段lua脚本程序,它被运行在redis服务器上下文中,这段脚本不必定义为一个lua脚本 numbers参数: 用于指定键名参数的个数 key [key ...] 参数

lua 脚本中调用redis 命令

redis.call():
    返回值就是redis 命令执行的返回值
    如果出错,则返回错误信息,不继续执行
redis.pcall():
    返回值就是redis 命令执行的返回值
    如果出错,则记录错误信息,继续执行
在脚本中,使用return 语句将返回值返回客户端,如果没有return 则返回nil

EVALSHA

EVAL 命令要求在每次执行脚本的时候都要发送一次脚本主题,REDIS有一个内部的缓存机制,因此不会每次都重新编译脚本,为了介绍贷款的小号,REDIS 实现了EVALSHA 命令,作用和EVAL 一样,都用于对脚本求值,但是它接受的第一个参数不是脚本,而是脚本的SHA1校验和sum

SCRIPT 命令

  1. script flush 清除所有脚本缓存
  2. script exists :根据给定的脚本校验和,检查指定的脚本是否存在脚本缓存
  3. script load : 将一个脚本装入脚本缓存,返回SHA1摘要,但并不立即运行它
  4. script kill: 杀死当前正在运行的脚本

脚本复制

Redis 传播Lua脚本,在使用主从模式和开启AOF持久化的前提下,当执行Lua脚本时,Redis 服务器有两种模式,脚本传播模式和命令传播模式

脚本传播模式

脚本传播模式是redis 复制脚本时默认使用的模式, redis会将被执行的脚本以及其参数复制到AOF文件以及从服务器里面 在这一模式下执行的脚本不能有时间,内部状态,速记函数等,执行相同的脚本以及参数必须产生相同的效果,在redis 5 也是处于同一个事务中

命令传播模式

处于命令传播模式的主服务器会将执行脚本产生的所有写命令用事务包裹起来,然后将事务复制到AOF文件以及从服务器里面。
因为命令传播模式复制的是写命令而不是脚本本身,所以即使脚本本身包含时间,内部状态,随机函数等,主服务器给所有从服务器复制的写命令仍然是相通的e
为了开启命令传播模式,用户在使用脚本执行任何写操作之前,需要在脚本里面调用redis.replicate_commands()
redis.replicate_commands() 只对调用该函数的脚本有效:在使用命令传播模式执行完当前脚本之后,服务器将自动切换回默认的脚本传播模式。

管道(pipeline),事务和脚本(lua)三者的区别

  1. 三者都可以批量执行命令
  2. 管道无原子性,命令都是独立的,属于无状态的操作
  3. 事务和脚本是有原子性的,其区别在于脚本可以借助lua 语言可在服务端存储的便利性定制和简化操作
  4. 脚本的原子性要强于事务,脚本执行期间,另外的客户端其他任何脚本或者命令都无法执行,脚本的执行时间应该尽量短,不能太耗时间的脚本

监视器

redis 客户端通过执行monitor 命令可以将自己变为一个监视器,实时接受并打印出服务器当前处理的命令请求的相关信息, 此时,当其他客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外,还会将这条命令求的信息发送给所有监视器

实时监视器

redisServer 维护一个monitors 的链表,记录自己的监视器,每次收到monitor命令之后,将客户端追加到链表尾

void monitorCommand(redisClient *c)
{
    if(c->flags & redis_slave) return;
        c->flags != (redis_slave| redis_monitor);
        listAddNodeTail(server.monitors,c);
        addReply(c,shared_ok);
}

向监视器发送命令信息

利用call函数实现向监视器发送命令

void call(redisClient * c,int flags){
    long long dirty ,start = ustime(),duration;
    int client_old_flags = c->flags;
    if(listLength(server.monitors) &&  !server.loading && !(c->cmd->flags & redis_cmd_skip_monitor))
}

call 主要调用了replicationFeedMonitors ; 这个函数的作用就是将命令打包为协议,发送给监视器。

慢查询日志

REDIS也有慢查询日志,可以用于监视和优化查询。

慢查询设置

在redis.conf 中可以配置和慢查询日志相关的选项 slowlog-log-slower-than 10000 //执行时间超过多少微秒的命令请求会被记录到日志上 0 全纪录 <0 不记录 slowlog-max-len 存储慢查询日志条数

redis 使用列表存储慢查询日志,采用队列日志
config set的方式可以临时设置,redis 重启后就无效
config set slowlog-log-slower-than 微秒
config  set slowlog-max-len 条数

慢查询记录的保存

在redisServer 中保存和慢查询日志相关的信息

struct redisServer{
long long slowlog_entry_id;
list *slowlog;
long long slowlog_log_slower_than;
unsigned long slowlog_max_len;
}

lowlog 链表保存了服务器中的所有慢查询日志,每个节点都保存一个slowlogEntry结构,每个slowlogEntry 结构代表一条慢查询日志

typedef struct slowlogEntry{
    long long id;
    time_t time;
    long long duration;
    robj **argv;
    int argc;
} slowlogEntry;

慢查询定位和处理

使用slowlog get 可以获得执行较慢的redis 命令,针对该命令可以进行优化:

  1. 尽量使用短key,对于value 有些也可精简,能使用int 就int
  2. 避免使用keys*,hgetall等全量操作
  3. 减少大key的存取,打散为小key
  4. 将rdb 改为 aof模式,rdb fork 子进程,主进程阻塞,redis 大幅下降,在数据量小的情况下,关闭持久化,改aof 命令式
  5. 想要一次添加多条数据时候可以用管道
  6. 尽可能使用哈希存储
  7. 尽量限制下redis使用的内存大小,制样可以避免使用swap 分区或者出现oom错误, 要考虑 内存和硬盘的swap