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

原创 吴就业 198 0 2020-03-21

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/5a3f3615e9ba4705b90635c91c25c358

作者:吴就业
链接:https://wujiuye.com/article/5a3f3615e9ba4705b90635c91c25c358
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

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

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

Watch+Multi

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

接着我们看下,在Watch之后,事务执行之前,如果key被修改,会是什么情况。

这很好理解,即便事务中各命令的顺序不一样,因为单线程执行命令的原因,exec执行时就已经知道被监听的哪些key被修改过了,它只是不执行事务了。

每个Redis数据库中都保存着一个watched_keys的字典,这个字典的键是某个被Watch命令监视的键,如上面例子中的s1s2,而字典的值则是一个链表,链表中记录所有监视该键的客户端,即一个连接。

所有写命令,在执行之后都会对watched_keys字典进行检查,查看是否有客户端正在监视刚刚写的键,如果有,将监视该键的客户端的REDIS_DIRTY_CAS标识打开,表示该客户端的事务安全性已经被破坏。在客户端(一个连接)提交(exec)事务执行时,先检测该标识是否被打开,如果是,则会拒绝当前客户端执行提交的事务。

假设在Cluster集群下执行,会是什么结果呢?我在我的服务器上部署了一个cluster集群,这是很早之前就部署了的。来看下在Cluster集群下执行Watch监听多个key会怎样。

可以看到,在Watch的时候就出错了,请求不允许key在不同的slot。即便slot不同但都在同一个节点也是不行的,必须是同一个slot。下图中,s4s5这两个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脚本,因为事务也不支持multiexec之间的命令,操作的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
#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

遇到VerifyError束手无策?从虚拟机源码分析原因

在学习使用asm动态生成字节码的过程中,我们或多或少都会遇到这样个错误,那么越到这个问题我们该如何解决呢?

教你如何将开源项目发布到maven中央仓库

如何将开源项目发布到maven中央仓库,让别人通过依赖使用你的开源项目,想必很多朋友都有过这个想法。

如何优化大表分页查询的Limit性能问题?

如果表的数据量非常大,我们除了优化查询总数的`sql`之外,还是需要优化`limit`的。

教你如何写出高性能的Mybatis分页插件

本篇介绍是什么原因导致的`mybatis-plus`分页插件性能下降,以及如何通过使用`JsqlParser`这个开源的`sql`解析工具包与`mybatis-plus`提供的自定义`sql`优化器功能,自己实现高性能的分页插件。

一道很有意思的Redis面试题,关于Bitmap算法,我选出了一些优质评论

起源于我在一个短视频中分享的一道面试题,当然,这道面试题我确实在工作中用过,只是业务场景不同。

一篇文章说清楚Java的全局异常处理,深入到hotspot源码

本篇将介绍如何使用Java提供的全局异常处理,以及分析一点hotspot虚拟机的源码,让大家了解虚拟机是如何将异常交给全局异常处理器处理的。