想要实现针对多个key
的复杂原子操作有两种方法。一种是Watch+Multi
,即监视器加事务方式,另一种便是通过执行lua
脚本实现。
这里所说的复杂原子性操作比如,扣减某商品的5
个库存,需要先判断当前商品的剩余库存是否足够扣减。但是避免不了在判断足够的情况下,再去执行扣减操作时,这个期间库存没有被别人修改的情况。
Watch+Multi
Watch
可以监控多个key
,被监听的多个key
,只要其中有一个key
被修改,那么所有操作都不会被执行。Watch
并不能单独使用,而是要结合事务使用。
接着我们看下,在Watch
之后,事务执行之前,如果key
被修改,会是什么情况。
这很好理解,即便事务中各命令的顺序不一样,因为单线程执行命令的原因,exec
执行时就已经知道被监听的哪些key
被修改过了,它只是不执行事务了。
每个Redis
数据库中都保存着一个watched_keys
的字典,这个字典的键是某个被Watch
命令监视的键,如上面例子中的s1
、 s2
,而字典的值则是一个链表,链表中记录所有监视该键的客户端,即一个连接。
所有写命令,在执行之后都会对watched_keys
字典进行检查,查看是否有客户端正在监视刚刚写的键,如果有,将监视该键的客户端的REDIS_DIRTY_CAS
标识打开,表示该客户端的事务安全性已经被破坏。在客户端(一个连接)提交(exec
)事务执行时,先检测该标识是否被打开,如果是,则会拒绝当前客户端执行提交的事务。
假设在Cluster
集群下执行,会是什么结果呢?我在我的服务器上部署了一个cluster
集群,这是很早之前就部署了的。来看下在Cluster
集群下执行Watch
监听多个key
会怎样。
可以看到,在Watch
的时候就出错了,请求不允许key
在不同的slot
。即便slot
不同但都在同一个节点也是不行的,必须是同一个slot
。下图中,s4
和s5
这两个key
是在同一个节点上的。
Watch
是真的严格,不同slot
都不行。有没有想过为什么监听多个落在不同节点上的key
,会不被允许?在单节点下,Redis
单线程执行,能够保证原子性,但在不同节点下,就是多进程多线程的问题,Watch
自然就不能用。
Lua脚本
Redis
执行lua
脚本是原子性操作,与执行Redis
命令一样对待。原子性操作得益于Redis
执行命令是单线程的,lua
脚本也会放在命令执行的等待队列中排队执行,因此也需要特别注意,lua
脚本中不要执行太多代码,最好不要写for
循环语句,否则会阻塞住,严重导致redis
节点假死。
在主从Redis
集群与读写分离Redis
集群下,lua
脚本的编写不需要考虑太多问题,只需考虑脚本耗时问题。但在分槽位的cluster
集群下,我们想要通过lua
脚本实现原子性操作,就必须要确保脚本所要操作的key
都在同一个Redis
节点下,即所有key
计算出来的槽位都落到同一个Redis
节点下,才能保证命令是原子性的。
事实上,如果要操作的key
不在同一节点上,命令执行也会保错。下图是Lua
脚本试图访问群集中的非本地节点抛出的错误。
即便是在一个事务中执行多个lua
脚本,只要有一个lua
脚本操作的key
落在不同的节点,结果都会执行失败。
并不只是lua
脚本,因为事务也不支持multi
与exec
之间的命令,操作的key
落在不同节点的情况。
不同slot
也不行。
商品入库出库问题
关于商品库存的问题,很多视频教程都在讲使用分布式锁,你可能也会想到使用分布式锁,但是用过分布式锁的都知道性能是个什么情况。
如果商品库存只使用redis
缓存,在不需要修改数据库中库存的情况下。对商品的库存实现入库和出库,我们可以借助lua
脚本实现原子性修改库存。在lua
脚本中判断库存是否足够减库存,足够扣减情况下再更新库存,这一系列操作是原子的。
lua
脚本不难学,我看了一下条件选择、分支语句以及判断语句之后就能自己写出脚本了。因为我们也并不需要用lua
做多复杂的事情。下面给出一个原子性修改库存的lua
脚本例子:
local kc=tonumber(redis.call('GET',KEYS[1]));
if kc==nil
then
return -1;
end
local newKc=kc-ARGV[1];
if newKc<0
then
return 0;
else
redis.call('SET',KEYS[1],newKc);
return 1;
end