思考7: 解锁过程的原子性/保证事务
问题:
finally 块的判断 + del 删除操作不是原子性的
// 解锁, 判断加锁与解锁不是同一个客户端 if (Objects.equals(stringRedisTemplate.opsForValue().get(REDIS_LOCK), value)) { // 若在此时,这把锁不是这个客户端的,则会错误的解锁 stringRedisTemplate.delete(REDIS_LOCK); }
解决方案,不用 lua 脚本,其他的操作
- redis 本身的事务
事务介绍
- redis 的事务是通过 MULTI、EXEC、DISCARD 和 WATCH 这四个命令来完成的。
- redis 的单个命令都是原子性的,所以这里确保事务性的对象是命令集合。
- redis 将命令集合序列化确保处于一事务的命令集合连续且不被打断的执行。
- redis 不支持回滚的操作
相关命令
序号 | 命令及描述 |
1 | DISCARD 取消事务,放弃执行事务块内的所有命令。 |
2 | EXEC 执行所有事务块内的命令。 |
3 | MULTI 标记一个事务块的开始。 |
4 | UNWATCH 取消 WATCH 命令对所有 key 的监视。 |
5 | WATCH key [key ...] 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。 |
命令执行
redis 自身的事务优化
@GetMapping("/buyGoods") public String buyGoods() { String value = UUID.randomUUID().toString() + Thread.currentThread().getName(); try { // 加锁 //Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);//NX // 10s 过期 // stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS); Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);//NX if (Boolean.FALSE.equals(flag)) { logger.info("抢锁失败, serverPort:{}", serverPort); return "抢锁失败"; } String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.valueOf(result); if (goodsNumber > 0) { int realNumber = goodsNumber - 1; stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber)); logger.info("成功买到商品, 库存还剩下:{} 件|服务提供者 serverPort : {}", realNumber, serverPort); return "成功买到商品, 库存还剩下:" + realNumber + " 件|服务提供者 serverPort : " + serverPort; } logger.info("商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:{}", serverPort); return "商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:" + serverPort; } finally { // redis 事务 while (true) { stringRedisTemplate.watch(REDIS_LOCK); if (Objects.equals(stringRedisTemplate.opsForValue().get(REDIS_LOCK), value)) { stringRedisTemplate.setEnableTransactionSupport(true); stringRedisTemplate.multi(); stringRedisTemplate.delete(REDIS_LOCK); List<Object> list = stringRedisTemplate.exec(); if (CollectionUtils.isEmpty(list)) { continue; } } stringRedisTemplate.unwatch(); break; } } }
通过 lua 脚本的方式来删除
@GetMapping("/buyGoods") public String buyGoods() { String value = UUID.randomUUID().toString() + Thread.currentThread().getName(); try { // 加锁 //Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);//NX // 10s 过期 // stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS); Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);//NX if (Boolean.FALSE.equals(flag)) { logger.info("抢锁失败, serverPort:{}", serverPort); return "抢锁失败"; } String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.valueOf(result); if (goodsNumber > 0) { int realNumber = goodsNumber - 1; stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber)); logger.info("成功买到商品, 库存还剩下:{} 件|服务提供者 serverPort : {}", realNumber, serverPort); return "成功买到商品, 库存还剩下:" + realNumber + " 件|服务提供者 serverPort : " + serverPort; } logger.info("商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:{}", serverPort); return "商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:" + serverPort; } finally { // 解锁 /* if (Objects.equals(stringRedisTemplate.opsForValue().get(REDIS_LOCK), value)) { stringRedisTemplate.delete(REDIS_LOCK); } */ /* while (true) { stringRedisTemplate.watch(REDIS_LOCK); if (Objects.equals(stringRedisTemplate.opsForValue().get(REDIS_LOCK), value)) { stringRedisTemplate.setEnableTransactionSupport(true); stringRedisTemplate.multi(); stringRedisTemplate.delete(REDIS_LOCK); List<Object> list = stringRedisTemplate.exec(); if (CollectionUtils.isEmpty(list)) { continue; } } stringRedisTemplate.unwatch(); break; } */ String RELEASE_LOCK_LUA_SCRIPT = "if redis.call("get",KEYS[1]) == ARGV[1] then\n" + " return redis.call("del",KEYS[1])\n" + "else\n" + " return 0\n" + "end"; DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(RELEASE_LOCK_LUA_SCRIPT, Long.class); // 参数一:redisScript,参数二:key列表,参数三:arg(可多个) Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(REDIS_LOCK), value); if (Objects.equals(result, 1)) { logger.info(" ========> del redis lock ok!"); } else { logger.info(" ========> del redis lock err!"); } } }
思考8: 终极解决方案还是得用 redisson
确保 redislock 过期时间大于业务执行时间的问题
老身长谈的问题:redis 分布式锁如何续期
集群 + CAP 对比 zookeeper
- redis - ap ; redis 异步复制造成的锁丢失,比如:主节点还没来得及把刚刚 set 进来的这条数据给从节点,就挂了
- zookeeper - cp
总结
redis 集群环境下,我们字节写的也不 ok, 直接使用 redlock 之 redisson 落地实现
上 redisson
- 导入依赖
implementation 'org.redisson:redisson-spring-boot-starter:3.16.8'
- 配置类 (java)
@Bean public Redisson redisson() { Config config = new Config(); config.useSingleServer().setAddress("http://127.0.0.1:6379").setDatabase(0); return (Redisson) Redisson.create(config); }
- 代码调整
@GetMapping("/buyGoods") public String buyGoods() { RLock rLock = redisson.getLock(REDIS_LOCK); rLock.lock(); try { String result = stringRedisTemplate.opsForValue().get("goods:001"); int goodsNumber = result == null ? 0 : Integer.valueOf(result); if (goodsNumber > 0) { int realNumber = goodsNumber - 1; stringRedisTemplate.opsForValue().set("goods:001", String.valueOf(realNumber)); logger.info("成功买到商品, 库存还剩下:{} 件|服务提供者 serverPort : {}", realNumber, serverPort); return "成功买到商品, 库存还剩下:" + realNumber + " 件|服务提供者 serverPort : " + serverPort; } logger.info("商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:{}", serverPort); return "商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:" + serverPort; } finally { if (rLock.isLocked() && rLock.isHeldByCurrentThread()) { rLock.unlock(); } } }
Redis 分布式锁总结
1、synchronized 可以解决单机问题,但是对于我们生产分布式环境,需要通分布式锁来解决数据操作的原子性
2、通过 niginx 来建立分分布式微服务环境(验证单机锁的问题)
3、取消单机锁,上 redis 分布式锁 setnx
4、只是加锁,没有释放锁,对于出现异常的情况,可能无法释放锁,必须要在代码层面 finally 释放锁
5、宕机了,部署了微服务代码层面根本没有走 finally 中的代码语句,没有保证解锁,这个 key 没有被删除,所以我们需要有 lockKey 的过期时间设定
6、为 redis 的分布式锁 key , 增加过期时间,此外,还必须要 setnx + 过期时间必须同一行
7、必须规定只能自己删除自己的锁,你不能把别人的的锁删除了,防止张冠李戴
1 删 2 , 2 删 3.
8、redis 集群环境下,我们自己写的也不 ok 直接上 redlock 之 redisson 落地实现。