JavaScript 操作Redis事务以及watch命令

Tags
数据库事务
Redis
链表
哈希算法
锁设计
JavaScript
CreatedTime
Aug 24, 2022 02:40 PM
Slug
UpdatedTime
Last updated August 24, 2022

Redis 事务

Redis 中的事务(transaction)是一组命令的集合。事务同命令一样都是 Redis 的最小执行单位。

命令

开始事务:MULTI
提交+执行事务:EXEC
举例:
notion image

事务异常场景

  • 执行EXEC前意外情况强退:所有命令失效,不会被提交
  • [解析态] 事务中语句出现语法错误:所有命令都无法执行
  • [运行态] 事务里的一条命令出现了运行错误:事务里其他的命令依然会继续执行(包括出错命令之后的命令)。redis 不提供回滚,因此这种情况中,开发者需要自己想办法恢复数据。
从场景中可以看出来,redis事务不保证原子性。需要借助后面的 watch 命令来实现。

JS实现redis事务

const Redis = require('ioredis') const { host, port, password } = require('./config') const redis = new Redis({ host, port, password, db: 1 }) const multi = redis.multi() multi.set('num', 1) multi.incr('num') multi.exec((err, replies) => { console.log('>>> replies are', replies) redis.quit() })

Watch命令

Redis Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
如何取消监听?使用UNWATCH命令,取消所有监听。

演示

例 1:
127.0.0.1:6379> get key "3" 127.0.0.1:6379> watch key OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> INCR key QUEUED 127.0.0.1:6379(TX)> EXEC 1) (integer) 4 127.0.0.1:6379> get key "4"
  1. 监听 key
  1. 开始事务
  1. 提交事务:由于事务开始前,没有修改 key 的值,因此事务执行成功,key 的值更新为 4
例 2:
127.0.0.1:6379> get key "4" 127.0.0.1:6379> watch key OK 127.0.0.1:6379> set key 5 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379(TX)> set key_2 100 QUEUED 127.0.0.1:6379(TX)> set key 100 QUEUED 127.0.0.1:6379(TX)> set key_2 1000 QUEUED 127.0.0.1:6379(TX)> EXEC (nil) 127.0.0.1:6379> get key "5" 127.0.0.1:6379> get key_2 "2"
  1. key 和 key_2 分别为 4 和 2
  1. 监听 key 的值
  1. 事务开始前修改 key 值
  1. 开始事务,在事务中尝试修改 key 和 key_2 的值
  1. 提交事务:由于事务开始前,修改了 key 的值,因此事务整体失败。获取的 key 和 key_2 的值是事务之前的。

JS实现watch命令

// ioredis-js client.watch('num', (err) => { if (err) throw err; client.multi() .incr('num') .exec((err, results) => { if (err) throw err; console.log('>>> results are', results) client.quit() }) })

用途

可以用来被用来将几个操作封装成一个原子操作,避免出现「竞态」条件。相当于拿到了一个「锁」。一般也和事务配合使用,从而避免出现脏写等情况。
伪代码如下:
def hsetxx($key, $field, $value) WATCH $key $isFieldExists = HEXISTS $key, $field if $isFieldExists is 1 MULTI HSET $key, $field, $value EXEC UNWATCH return $isFieldExists
为什么这里会这样用呢?
在上面代码中,事务启动的条件,是$isFieldExists为 true。如果进行监听,直接读取$isFieldExists 的值,然后开启事务。那么在读取之后,如果其它客户端修改此值,正确情况是事务不执行,但是由于这里读到的是旧值,因此依然会执行(错误情况)。
利用 watch,相当于拿到了值的「快照」,提交事务的时候,如果真实值和快照的值不同,那么就不会执行

Watch命令底层实现原理

底层数据结构:字典+链表

notion image
watched_keys 字典(上图)
在每个代表数据库的 redis.h/redisDb 结构类型中, 都保存了一个 watched_keys 字典, 字典的键是这个数据库被监视的键, 而字典的值则是一个链表, 链表中保存了所有监视这个键的客户端。

整体逻辑

notion image
在任何对数据库键空间(key space)进行修改的命令成功执行之后 (比如 FLUSHDB 、 SET 、 DEL 、 LPUSH 、 SADD 、 ZREM ,诸如此类), multi.c/touchWatchedKey 函数都会被调用 —— 它检查数据库的 watched_keys 字典, 看是否有客户端在监视已经被命令修改的键, 如果有的话, 程序将所有监视这个/这些被修改键的客户端的 REDIS_DIRTY_CAS 选项打开
当客户端发送 EXEC 命令、触发事务执行时, 服务器会对客户端的状态进行检查
  • 如果客户端的 REDIS_DIRTY_CAS 选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败。
  • 如果 REDIS_DIRTY_CAS 选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务。

参考链接