• Redis事务的不足及改进措施
  • Murphey 发表于 2015/10/10 13:41:00 | 分类标签: Redis事务 NOSQL

  • Redis的事务特性

    数据ACID特性满足了几条? 
    为了保持简单,redis事务保证了其中的一致性和隔离性; 
    不满足原子性和持久性;

    原子性

    redis事务在执行的中途遇到错误,不会回滚,而是继续执行后续命令;(违反原子性)

    事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作; 
    中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做; 
    比如:
    redis 127.0.0.1:7000> multi
    OK
    redis 127.0.0.1:7000> set a aaa
    QUEUED
    redis 127.0.0.1:7000> set b bbb
    QUEUED
    redis 127.0.0.1:7000> set c ccc
    QUEUED
    redis 127.0.0.1:7000> exec
    1) OK
    2) OK
    3) OK
    如果在set b bbb处失败,set a已成功不会回滚,set c还会继续执行;

    持久性

    事务不过是用队列包裹起了一组 Redis 命令,并没有提供任何额外的持久性功能,所以事务的持久性由 Redis 所使用的持久化模式决定:

    在单纯的内存模式下,事务肯定是不持久的。
    在 RDB 模式下,服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败,所以 RDB 模式下的 Redis 事务也是不持久的。
    在 AOF 的“总是 SYNC ”模式下,事务的每条命令在执行成功之后,都会立即调用 fsync 或 fdatasync 将事务数据写入到 AOF 文件。但是,这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,所以从命令执行成功到数据保存到硬盘之间,还是有一段非常小的间隔,所以这种模式下的事务也是不持久的。
    其他 AOF 模式也和“总是 SYNC ”模式类似,所以它们都是不持久的。

    隔离性和一致性

    redis事务在执行的过程中,不会处理其它命令,而是等所有命令都执行完后,再处理其它命令(满足隔离性) 
    redis事务在执行过程中发生错误或进程被终结,都能保证数据的一致性;(详见参考资料1)

    redis事务的缺陷

    除了不保证原子性和持久性,在实际使用中还有以下问题:

    1) 遇到有查询的情况穿插在事务中间,不会返回结果; 
    设置事务开始标志后,所有的命令都是queued,即使是查询指令; 
    如果后续的更新操作需要依赖于前面的查询指令,那redis事务就无法有效的完成任务; 
    例如:
    redis 127.0.0.1:7000> multi
    OK
    redis 127.0.0.1:7000> set a aaa
    QUEUED
    redis 127.0.0.1:7000> get b
    QUEUED
    业务逻辑...
    redis 127.0.0.1:7000> set c ccc
    QUEUED
    redis 127.0.0.1:7000> exec
    1) OK
    2) bbb
    3) OK
    第二步 get a 返回的是queued,并不是a的查询结果, 
    如果后续的set操作依赖于get的结果(存在依赖业务逻辑),就不能将get操作放在事务操作中;

    2) 事务中的每条命令都与redis服务器进行了一次网络交互; 

    redis 事务指定开始后,执行一个事务返回的都是queued,那这个入队操作是在客户端实现,还是在服务器端实现的? 
    查看源码,很容易发现是在服务器端实现; 
    在Redis.c中有这么一段:
    int processCommand(redisClient *c) {
    ...
        /* Exec the command */
        if (c->flags & REDIS_MULTI &&
            c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
            c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
        {
            queueMultiCommand(c); // 将事务中的命令都放入到队列中,然后返回"QUEUED"
            addReply(c,shared.queued);
        } else {
            if (server.vm_enabled && server.vm_max_threads > 0 &&
                blockClientOnSwappedKeys(c)) return REDIS_ERR;
    
            //调用该命令函数来处理命令
            call(c);
        }
        return REDIS_OK;
    }
    这里就涉及到客户端与服务器端的多次交互,明明是需要一次批量执行的n条命令,还需要通过多次网络交互,有些浪费;

    更新操作中的查询实现

    如果有这样的需求:在事务开始后,中间穿插有查询逻辑; 
    那么使用redis事务(单库),无法满足这个要求;

    可能的解决方案:

    可以考虑使用多个库,读写分离,查询库只用来查询,更新库用来开事务做写操作;

    不再使用redis的事务指令,自己在客户端将待执行的命令批量打包,决定是否回滚还是全部执行;这样可以在更新的间隙执行查询逻辑;而不需要将查询逻辑提前到事务指令multi之前;

    将查询业务逻辑提前;严格规范代码编写要求,所有的redis查询逻辑都放在事务之外:
     redis 127.0.0.1:7000> get b
     bbb
     业务逻辑...
     redis 127.0.0.1:7000> multi
     OK
     redis 127.0.0.1:7000> set a aaa
     QUEUED
     redis 127.0.0.1:7000> set c ccc
     QUEUED
     redis 127.0.0.1:7000> exec
     1) OK
     2) OK

    优化网络特性

    将多个命令打包批量发送到redis服务器执行,减少网络交互,优化性能,可能的解决方案:

    对于所有的get/set操作,可使用现有的mget/mset指令;
    对于各种不同类型的更新操作,可使用lua脚本将命令打包后,发送到服务器端一次执行;
  • 请您注意

    ·自觉遵守:爱国、守法、自律、真实、文明的原则

    ·尊重网上道德,遵守《全国人大常委会关于维护互联网安全的决定》及中华人民共和国其他各项有关法律法规

    ·严禁发表危害国家安全,破坏民族团结、国家宗教政策和社会稳定,含侮辱、诽谤、教唆、淫秽等内容的作品

    ·承担一切因您的行为而直接或间接导致的民事或刑事法律责任

    ·您在编程中国社区新闻评论发表的作品,本网站有权在网站内保留、转载、引用或者删除

    ·参与本评论即表明您已经阅读并接受上述条款

  • 感谢本文作者
  • 作者头像
  • 昵称:Murphey
  • 加入时间:2013/7/13 0:00:00
  • TA的签名
  • 这家伙很懒,虾米都没写
  • +进入TA的空间
  • 以下内容也很赞哦
分享按钮