前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis实现原子操作的两种方式与商品入库出库解决方案

Redis实现原子操作的两种方式与商品入库出库解决方案

作者头像
吴就业
发布2020-07-10 11:20:30
1.9K0
发布2020-07-10 11:20:30
举报
文章被收录于专栏:Java艺术Java艺术

在单机的Redis集群下,想要实现针对多个key的复杂原子操作有两种方法。一种是Watch+Multi,即监视器加事务方式,另一种便是通过执行lua脚本实现。

这里所说的复杂原子性操作比如,扣减某商品的5个库存,需要先判断当前商品的剩余库存是否足够扣减。但是避免不了在判断足够的情况下,再去执行扣减库存操作时,这个期间库存没有被别人修改的情况。

1

Watch+Multi

Watch可以监控多个key,被监听的多个key,只要其中有一个key被修改,那么所有操作都不会被执行,即事务不会执行。Watch并不能单独使用,而是要结合事务使用。

截图漏掉了Watch s1 s2

接着我们看下,在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。下图中,s4s5这两个key是在同一个节点上的。

Watch是真的严格,不同slot都不行。有没有想过为什么监听多个落在不同节点上的key,会不被允许?在单节点下,Redis单线程执行,能够保证原子性,但在不同节点下,就是多进程多线程的问题,Watch自然就不能用。再简单点,watched_keys字典不一样就是不行。

2

Lua脚本

Redis执行lua脚本是原子性操作,与执行Redis命令一样对待。原子性操作得益于Redis执行命令是单线程的,lua脚本也会放在命令执行的等待队列中排队执行,因此也需要特别注意,lua脚本中不要执行太多代码,最好不要写for循环语句,控制脚本的执行耗时,否则会影响Redis的性能。

在主从Redis集群与读写分离Redis集群下,lua脚本的编写不需要考虑太多问题,只需考虑脚本耗时问题。但在分槽位的cluster集群下,我们想要通过lua脚本实现原子性操作,就必须要确保脚本所要操作的key都在同一个Redis节点下,即所有key计算出来的槽位都落到同一个Redis节点(小集群中的master)下,才能保证命令是原子性的。

事实上,如果要操作的key不在同一节点上,命令执行也会保错。下图是Lua脚本试图访问群集中的非本地节点抛出的错误。

即便是在一个事务中执行多个lua脚本,只要有一个lua脚本操作的key落在不同的节点,结果都会执行失败。

并不只是lua脚本,因为事务也不支持multiexec之间的命令,操作的key落在不同节点的情况。

不同slot也不行。

3

商品入库出库问题

关于商品库存的问题,很多视频教程都在讲使用分布式锁,你可能也会想到使用分布式锁,但是用过分布式锁的都知道性能是个什么情况。

如果商品库存只使用redis缓存,在不需要修改数据库中库存的情况下。对商品的库存实现入库和出库,我们可以借助lua脚本实现原子性修改库存。在lua脚本中判断库存是否足够减库存,足够扣减情况下再更新库存,这一系列操作是原子的。

lua脚本不难学,我看了一下条件选择、分支语句以及判断语句之后就能自己写出脚本了。因为我们也并不需要用lua做多复杂的事情。下面给出一个原子性修改库存的lua脚本例子:

代码语言:javascript
复制
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

如果是SKU商品,SKU是商品的库存单位,比如一件衣服,有颜色和码数之分,那么红色M码就是一件SKU商品,可以称这件衣服是主商品,红色M码是子商品或附属商品。

因为前面说过,要确保lua脚本所操作的key必须都在同一个节点上,最好是同一个solt槽,也就是一段脚本只操作一个key。显然,主商品与子商品不是一个key,那就不能使用lua实现这种需求的原子操作了。

其实我们也只用确保子商品的库存能够更新成功就行了,然后再更新主商品的库存,因为主商品的库存永远都等于所有附属商品库存的总和。我们在购买商品的时候,也是先选择完颜色和尺码之外,才能看到是否还有货,就比如你喜欢某个鞋子,但刚好你看的那个码数没货了。

即便主商品的库存修改失败,也不会导致商品超卖的问题,但还是要尽可能的保证,附属商品库存更新时主商品库存也要更新。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java艺术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档