个人网站建设基本流程,文友胜做的网站,想做网站要学什么,七星彩网站开发公司前言
小张目前在使用分布式锁 Redisson 实现一个需求。那我在想我能否自己手撸一个能用于分布式环境的锁呢#xff1f;于是果然尝试。 历经一天后#xff0c;小张手撸的锁终于写出来了#xff0c;再次给各位看看#xff0c;看给位有没有什么优化的建议#xff1a;
// 加…前言
小张目前在使用分布式锁 Redisson 实现一个需求。那我在想我能否自己手撸一个能用于分布式环境的锁呢于是果然尝试。 历经一天后小张手撸的锁终于写出来了再次给各位看看看给位有没有什么优化的建议
// 加锁 5分钟后过期// TODO 如何解决当前接口访问慢已经超过了30秒钟锁过期了 使用原生的方法就是把锁的时间设置长一点例如五分钟Boolean lockFlag redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, Duration.ofMillis(5));if (lockFlag) {// 加锁成功log.info(加锁成功: {}, couponId);try {// 执行业务逻辑// 封装条件查询促销券并且已发布的LambdaQueryWrapperCouponDO queryWrapper new LambdaQueryWrapper();queryWrapper.eq(CouponDO::getId, couponId).eq(CouponDO::getCategory, category).eq(CouponDO::getPublish, CouponPublishEnum.PUBLISH);CouponDO couponDO couponMapper.selectOne(queryWrapper);// 校验优惠券是否可以领取this.checkCoupon(couponDO, loginUser.getId());// 构建领卷记录CouponRecordDO couponRecordDO new CouponRecordDO();BeanUtils.copyProperties(couponDO, couponRecordDO, id);couponRecordDO.setCreateTime(new Date());couponRecordDO.setUseState(CouponStateEnum.NEW.name());couponRecordDO.setUserId(loginUser.getId());couponRecordDO.setUserName(loginUser.getName());couponRecordDO.setCouponId(couponId);// 扣减库存int rows couponMapper.reduceStock(couponId);if (rows 1) {// 库存扣减成功才保存记录couponRecordMapper.insert(couponRecordDO);} else {log.warn(发放优惠券失败: {},用户: {}, couponDO, loginUser);throw new BizException(BizCodeEnum.COUPON_NO_STOCK);}} finally {// 解锁// 使用 Redis 中的 lua 脚本来执行判断和删除逻辑。要么全成功要么全失败 保证原子性String script if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1]) else return 0 end;Integer result redisTemplate.execute(new DefaultRedisScript(script, Integer.class), Arrays.asList(lockKey), uuid);log.info(解锁成功: {}, result);}} else {log.info(加锁失败开始自旋);// 加锁失败证明当前线程抢购的优惠券已经有线程正在执行了try {TimeUnit.SECONDS.sleep(1); // 睡眠一秒} catch (InterruptedException e) {log.error(自旋失败);}addCoupon(couponId, category);}
那么在自己实现的基于 Redis 的分布式锁中我们发现为了保证解锁操作的原子性我们使用了 Lua 表达式来配合 Redis 。Redis 执行 Lua 到底能不能保证原子性今天就来聊一聊。 一、原子性
通常意义的原子性
通常意义上我们说的原子性是指关系型数据库 RDBMS比如 MySQL的原子性也就是 ACIDAtomicity、Consistency、Isolation、Durability中 Atomicity这一项特性。
ACID 中的原子性指事务中的所有操作要么全部执行要么全部不执行。
这里以银行转账账户A 给账户B 转账100元为例来解释原子性
账户A 减去100元账户B 增加100元
原子性是指上面两个过程要么全部执行要么全部不执行。也就是说账户A 减去 100元的同时账户B 必须增加100元否则该操作就不具备原子性。Java代码简要实现如下图 Lua 原子性
在分析 Lua的原子性之前我们先看看 Lua是什么下图摘自 Lua官方描述 从官方描述可以得知Lua 是一种功能强大、高效、轻量级、可嵌入的脚本语言。它支持过程编程、面向对象编程、函数式编程、数据驱动编程和数据描述。 Lua 将简单的过程语法与基于关联数组和可扩展语义的强大数据描述结构相结合。Lua 是动态类型的通过使用基于寄存器的虚拟机解释字节码来运行并具有自动内存管理和增量垃圾回收功能使其成为配置、脚本编写和快速原型设计的理想选择。
Lua 本身并没有提供对于原子性的直接支持它只是一种脚本语言通常是嵌入到其他宿主程序中运行比如 Redis。
在 Redis中执行 Lua的原子性是指整个 Lua脚本在执行期间会被当作一个整体不会被其他客户端的命令打断。
为了对 Redis执行 Lua的原子性有一个感官上的认识这里以 Lua脚本中需要完成 SET key1 value1 和 INCRBY key2 value2 和 SET key3 value3 三个命令为例 上述例子整个 luaScript 字符串脚本作为一个整体被执行且不被其他事务打断这就是一个原子性的操作。
好了总结下 ACID的原子性和 Redis执行 Lua脚本原子性在概念上的差异
ACID的原子性是指事务中的命令要么全执行要么全部不执行Redis中执行 Lua脚本原子性是指Lua脚本会作为一个整体执行且不被其他客户端打断至于 Lua脚本里面的命令是否必须全部成功或者全部失败并不要求。关于这一点在接下来的内容也会详细解释
在分析原子性概念时我们可以发现“原子性”其实是事务中的一项特性因此接下来分析 Redis的事务。
二、Redis 事务
下图是 Redis官方对事务描述的摘要 文档看起来很长总结成一句话Redis 事务允许执行一批命令通过执行 MULTI命令开启事务执行 EXEC命令结束事务WATCH 和 DISCARD 配合事务一起使用提供了一种 CAS(check-and-set) 乐观锁的机制。WATCH 用于监听 Key如果被监听的 Key有任何一个发生变化则中止事务被动关闭事务而 DISCARD 用于主动中止事务。
MULTI/EXEC
用一个示例来理解 MULTI/EXEC 通过执行的结果可以看出Redis的事务是以 MULTI命令开启以 EXEC命令结束期间所有的命令都是先进入队列只有执行 EXEC命令时才会把队列中的所有命令顺序串行执行并且返回一个所有命令执行结果的数组包括命令执行的错误信息。
需要注意的是在 EXEC 执行后即使事务队列中有命令执行失败队列中的所有其他命令也会被处理Redis 不会停止执行这些命令。
DISCARD 和 WATCH 也是 Redis 中用于事务的两个命令它们与 MULTI 和 EXEC 一起使用提供更复杂的事务处理机制。
WATCH
WATCH 命令用于监听一个或多个 Key如果在执行事务期间这些 Key中任何一个Key的 value被其他事务修改当前整个事务将会被中止。需要注意低于 6.0.9 的 Redis 版本Key过期不会中止事务
如下示例事务1 watch key1 key2事务2在事务1执行期间修改 key2 10当事务1执行 exec命令时因为 watch监听到 key2被其他事务事务2修改了(value10) 因此事务1被取消事务队列中的所有命令被清除即 set key1 value1 和 incrby key 2两条命令都不执行key2的 value还是10
事务1事务2watch key1 key2multiset key1 value1incrby key2 2set key2 10execkeys * // 只有key210keys * // 只有key210 DISCARD
DISCARD 命令用于中止事务。
如下示例执行 DISCARD命令后当前事务被中止因此执行 EXEC 时会报“ERR EXEC without MULTI”错误。 事务中的错误
事务中主要会出现两种类型的错误 事务命令进入事务队列之前出错。例如命令语法错误参数错误、命令名称错误等或者可能存在一些关键情况比如内存不足。如下示例命令incr key2 1/0 在进入事务队列之前报错所以当前事务被中止执行 EXEC命令会报错 调用 EXEC 命令后事务队列中的命令执行失败。例如对字符串值进行加1操作。如下示例key的 value是字符串当对 key 执行incr key 操作时报错因此该条命令执行失败
事务回滚
Redis的事务不支持回滚。 Redis 不支持事务回滚因为支持回滚会对 Redis 的简单性和性能产生重大影响。
官方说明简明扼要其实多加思考也能理解Redis 是 REmote DIctionary Server 的缩写翻译为“远程字典服务”设计的初衷是用于缓存追求快速高效。而了解过 ACID事务的小伙伴应该能明白事务回滚的复杂度因此Redis不支持事务回滚似乎也合情合理。
到此我们也对 Redis事务做个小结Redis的事务由 MULTI/EXEC 两个命令完成WATCH/DISCARD 两个命令的加持给 Redis事务提供了 CAS 乐观锁机制。Redis 事务不支持回滚它和关系型数据库比如 MySQL的事务ACID是不一样的。
三、Redis 如何执行 Lua
分析完原子性和 Redis事务这些理论知识后我们就得动手实操看看 Redis是如何执行 Lua的。
一般情况下Redis执行 Lua常用的方法有 2种
原生命令比如 EVAL/EVALSHA命令等编程工具比如编程语言中提供的三方工具包或类库
在编写 Lua脚本时需要注意区分 redis.call() 和 redis.pcall() 两个命令的使用。
EVAL EVAL script numkeys [key [key ...]] [arg [arg ...]] EVAL语法很简单EVAL script numkeys 是必填项[key [key ...]] [arg [arg ...]]是选填项。
如下示例截图分别展示了不传Key传 1个key 和 2个 key 3种场景 下图示例展示了 [key [key ...]] [arg [arg ...]] 和 numkeys 匹配错误时报错的场景 redis.call()
redis.call() 用于执行 Redis的命令。当命令执行出错时会阻断整个脚本执行并将错误信息返回给客户端。
如下示例当执行INCRBY key2 1/0 失败时会抛异常后续流程被阻断即SET key3 value3没有被执行。
Redis原生命令执行示例如下 EVAL redis.call(SET, key1, value1); redis.call(INCRBY, key2, 1/0); redis.call(SET, key3, value3) 0 使用 Jedis框架执行 Lua示例如下 查看 Lua执行后各个key的值截图如下 redis.pcall()
redis.pcall() 也用于执行 Redis的命令。当命令执行出错时不会阻断脚本的执行而是内部捕获错误并继续执行后续的命令。
如下示例当执行INCRBY key2 1/0 失败时不会抛异常后续流程继续执行即SET key3 value3 也被执行。
Redis原生命令执行示例 EVAL redis.pcall(SET, key1, value1); redis.pcall(INCRBY, key2, 1/0); redis.pcall(SET, key3, value3) 0 使用 Jedis框架执行 Lua示例 对于 Lua中 redis.call() 和 redis.pcall() 如何选择需要根据实际业务来判断标准是当 Lua脚本中某条命令执行出错时是否需要阻断后续的命令执行。
四、如何保证原子性
首先可以肯定的是Redis执行 Lua脚本可以保证原子性不过这和 Redis Server的部署方式密不可分。
Redis是典型的 C/S(Client/Server) 模型如下图 因此Redis 通常有 3种不同的部署方式部署方式不同原子性的保证也不一样。 单机部署
不管 Lua脚本中操作的 key是不是同一个都能保证原子性
主从部署
Redis 主从复制是用于将主节点的数据同步到从节点以保持数据的一致性。而Redis的所有写操作都在主节点上所以不管 Lua脚本中操作的 key是不是同一个都能保证原子性
需要注意当主节点执行写命令时从节点会异步地复制这些写操作。在这个复制的过程中从节点的数据可能与主节点存在一定的延迟。因此如果在 Lua 脚本中包含读操作并且该脚本在主节点上执行可能会读到最新的数据但如果在从节点上执行可能会读到稍有延迟的数据。
Cluster集群部署
如果 Lua脚本操作的 key是同一个能保证原子性
如果操作的 Key不相同可能被 hash 到不同的 slot也可能 hash 到相同的 slot所以不一定能保证原子性
因此在 Cluster集群部署的环境下使用 Lua脚本时一定要注意Lua脚本中操作的是同一个 Key
原子性保证
这里以 Redis单机部署为例当客户端向服务器发送一个带有 Lua脚本的请求时Redis会把该脚本当作一个整体然后加载到一个脚本缓存中因为 Redis读写命令是单线程操作关于 Redis的单线程模型和多路复用线程模型会在其他的文章中讲解最终Lua脚本的读写在 Redis服务器上可以简单地抽象成下图所有的 Lua脚本会按照进入顺序放入队列中然后串行进行读写这样就保证每个 Lua不会被其他的客户端打断从而保证了原子性 五、面试该如何回答
在面试中Redis 执行 Lua脚本时能否保证原子性这个问题如何作答
第一步需要解释这里的原子性是什么它和关系数据事务 ACID中的一致性的差异是什么消除原子性在具体载体RDBMS/NoSQL上概念的差异第二步需要解释 Redis的事务说明 RDBMS/NoSQL 在事务上的差异点第三步需要解释 Redis在不同部署方式下原子性能否保证。Redis部署方式有3种单机部署主从部署Cluster集群部署需要说明在哪些部署方式下能保证原子性哪些不能保证原子性第四步解释 Redis 执行 Lua脚本是如何保证原子性第五步分析下 Redis的单线程模型 和 IO多路复用模型加分项这步是可选项
六、Why Lua
既然 Redis事务能保证原子性为什么还需要 Lua脚本呢
Lua 是一种嵌入式语言是 Redis官方推荐的脚本语言Lua 脚本一般比 MULTI/EXEC 更快、更简单Redis 事务中事务队列中的所有命令都必须在 EXEC命令执行才会被执行对于多个命令之间存在依赖关系比如后面的命令需要依赖上一个命令结果的场景Redis事务无法满足因此 Lua 脚本更适合复杂的场景Redis 事务能做的 Lua能做Redis事务做不到的 Lua也能做
七、Lua注意事项
Redis执行 Lua脚本时Lua的编写需要注意以下几个点
不要在 Lua脚本中使用阻塞命令如BLPOP、BRPOP等。因此这些命令可能会导致 Redis服务器在执行脚本期间被阻塞无法处理其他请求不要编写过长的 Lua脚本。因为 Redis读写命令是单线程过长的脚本加载解析运行会比较耗时导致其他命令的延迟延迟增加不要在 Lua脚本中进行复杂耗时的逻辑因为 Redis读写命令是单线程的长时间运行脚本可能导致其他命令的延迟增加Lua脚本中需要注意区分 redis.call() 和 redis.pcall() 命令Lua 索引表从索引 1 开始而不是 0
八、总结
原子性需要区分具体使用的载体在关系型数据库比如 MySQL)和 No SQL比如Redis中原子性的概念是不相同的Redis的事务MULTI/ESXEC和关系型数据库比如 MySQL的事务ACID也是不相同的ACID的原子性指命令要么全部执行要么全部不执行Redis执行 Lua脚本的原子性指Lua脚本会当作一个整体被执行且不被其他事务打断但是 Lua 脚本里面的命令无法保证“要么全部执行要么全部不执行”Lua脚本使用 redis.pcall() 执行命令出错时会被catch后续命令会正常执行Lua脚本使用 redis.call() 执行命令出错时会抛给客户端后续命令会被阻断Lua 脚本一般比 MULTI/EXEC 更快、更简单Redis的部署方式决定了 Redis执行 Lua脚本是否能保证原子性编写 Lua脚本时特别需要注意在一个事务中是否要求操作同一个 key
九、参考资料
Scripting with Luaredis.io/docs/intera…
Atomicity with Luadeveloper.redis.com/develop/jav…
Redis Transactionsredis.io/docs/intera…
The Programming Language Luawww.lua.org/
十、温馨提示 本文基于 Redis服务器版本为7.0.4不同的版本可能略有差异 本文所有示例都是基于单机环境运行 Redis的命令不区分大小写但是 Key 和 Value 区分大小写