对于之前案例的一些问题思考
自定义 Redis 分布式锁
思考1: 单机锁/JVM 锁解决并发
单机版本没有加锁
- 没有加锁,并发下数字不对,出现超卖现象
加锁的思考:
- 加 synchronized 锁? 还是加 ReentrantLock 锁? 还是都可以?
- 对于 synchronized 锁 和 ReentrantLock 锁使用需要区分场景。
ReentrantLock 支持 tryLock ,以及设置锁超时的时间。相对非常灵活。
思考2:分布式环境下增加自定义分布式锁
nignx 负载均衡
- 分布式部署之后,单机锁还是出现超卖的情况,需要分布式锁
Niginx 配置负载均衡
1、启动 Nginx 并测试通过
2、/usr/local/nginx/conf/nginx.conf
nignx.conf 配置文件
3、重启 ./nginx -s reload
4、/usr/local/nginx/sbin/nginx -c /usr/local/nginx/cofig/nginx.conf
5、./nginx -c /usr/local/nginx/conf/nginx.conf
6、启动 /usr/local/nginx/sbin
7、关闭 /usr/local/nginx/sbin --> ./nginx -s stop
8、重启 ./nginx -s reload
启动两个服务
模拟高并发模拟
解决方案
上 redis 分布式锁
继续修改我们的代码:
@GetMapping("/buyGoods") public String buyGoods() { String value = UUID.randomUUID().toString() + Thread.currentThread().getName(); Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);//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); stringRedisTemplate.delete(REDIS_LOCK); return "成功买到商品, 库存还剩下:" + realNumber + " 件|服务提供者 serverPort : " + serverPort; } logger.info("商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:{}", serverPort); return "商品已经售完/活动结束/调用超时, 欢迎下次光临, serverPort:" + serverPort; }
思考3: 自定义分布式锁释放失败
上面代码的问题,如果出现异常,可能无法释放锁,所以锁的的释放,我们可以在 finally 代码块中释放。
加锁解锁, lock/unlock 必须同时出现并且保存,
代码优化如下:
@GetMapping("/buyGoods") public String buyGoods() { try { String value = UUID.randomUUID().toString() + Thread.currentThread().getName(); // 加锁 Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);//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 { // 解锁 stringRedisTemplate.delete(REDIS_LOCK); } }
思考4: 加锁失败后宕机无法解锁
如果发生宕机情况下,可能是加锁成功?
部署了为为服务的 node 节点机器挂了,代码层面没有走到 finally
这一步,没有保证解锁,这个 key 没有被删除,需要加入一个过期时间限定 key 。
解决方案:
try { String value = UUID.randomUUID().toString() + Thread.currentThread().getName(); // 加锁 Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK, value);//NX // 10s 过期 (新增逻辑) stringRedisTemplate.expire(REDIS_LOCK, 10L, TimeUnit.SECONDS); 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 { // 解锁 stringRedisTemplate.delete(REDIS_LOCK); }
思考5: 加锁和设置锁的有效期如何保证事务
问题:设置 key + 过期时间分开了,必须要合并成一行,具备原子性
再次优化后的代码如下:
@GetMapping("/buyGoods") public String buyGoods() { try { String value = UUID.randomUUID().toString() + Thread.currentThread().getName(); // 加锁 //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 { // 解锁 stringRedisTemplate.delete(REDIS_LOCK); } }
思考6:防止张冠李戴,删除了别人的锁
问题:张冠李戴,删除了别人的锁
解决方案:只能删除自己的锁,别人的锁不能动
@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); } } }