【高并发】高并发分布式锁架构解密,不是所有的锁都是分布式锁!! (3)

所以,我们在分布式高并发环境下,可以使用Redis的SETNX命令来实现分布式锁。假设此时有线程A和线程B同时访问临界区代码,假设线程A首先执行了SETNX命令,并返回结果1,继续向下执行。而此时线程B再次执行SETNX命令时,返回的结果为0,则线程B不能继续向下执行。只有当线程A执行DELETE命令将设置的锁状态删除时,线程B才会成功执行SETNX命令设置加锁状态后继续向下执行。

引入分布式锁

了解了如何使用Redis中的命令实现分布式锁后,我们就可以对下单接口进行改造了,加入分布式锁,如下所示。

/** * 为了演示方便,我这里就简单定义了一个常量作为商品的id * 实际工作中,这个商品id是前端进行下单操作传递过来的参数 */ public static final String PRODUCT_ID = "100001"; @RequestMapping("/submitOrder") public String submitOrder(){ //通过stringRedisTemplate来调用Redis的SETNX命令,key为商品的id,value为字符串“binghe” //实际上,value可以为任意的字符换 Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe"); //没有拿到锁,返回下单失败 if(!isLock){ return "failure"; } int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if(stock > 0){ stock -= 1; stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock)); logger.debug("库存扣减成功,当前库存为:{}", stock); }else{ logger.debug("库存不足,扣减库存失败"); throw new OrderException("库存不足,扣减库存失败"); } //业务执行完成,删除PRODUCT_ID key stringRedisTemplate.delete(PRODUCT_ID); return "success"; }

那么,在上述代码中,我们加入了分布式锁的操作,那上述代码是否能够在高并发场景下保证业务的原子性呢?答案是可以保证业务的原子性。但是,在实际场景中,上面实现分布式锁的代码是不可用的!!

假设当线程A首先执行stringRedisTemplate.opsForValue()的setIfAbsent()方法返回true,继续向下执行,正在执行业务代码时,抛出了异常,线程A直接退出了JVM。此时,stringRedisTemplate.delete(PRODUCT_ID);代码还没来得及执行,之后所有的线程进入提交订单的方法时,调用stringRedisTemplate.opsForValue()的setIfAbsent()方法都会返回false。导致后续的所有下单操作都会失败。这就是分布式场景下的死锁问题。

所以,上述代码中实现分布式锁的方式在实际场景下是不可取的!!

引入try-finally代码块

说到这,相信小伙伴们都能够想到,使用try-finall代码块啊,接下来,我们为下单接口的方法加上try-finally代码块。

/** * 为了演示方便,我这里就简单定义了一个常量作为商品的id * 实际工作中,这个商品id是前端进行下单操作传递过来的参数 */ public static final String PRODUCT_ID = "100001"; @RequestMapping("/submitOrder") public String submitOrder(){ //通过stringRedisTemplate来调用Redis的SETNX命令,key为商品的id,value为字符串“binghe” //实际上,value可以为任意的字符换 Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe"); //没有拿到锁,返回下单失败 if(!isLock){ return "failure"; } try{ int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if(stock > 0){ stock -= 1; stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock)); logger.debug("库存扣减成功,当前库存为:{}", stock); }else{ logger.debug("库存不足,扣减库存失败"); throw new OrderException("库存不足,扣减库存失败"); } }finally{ //业务执行完成,删除PRODUCT_ID key stringRedisTemplate.delete(PRODUCT_ID); } return "success"; }

那么,上述代码是否真正解决了死锁的问题呢?我们在写代码时,不能只盯着代码本身,觉得上述代码没啥问题了。实际上,生产环境是非常复杂的。如果线程在成功加锁之后,执行业务代码时,还没来得及执行删除锁标志的代码,此时,服务器宕机了,程序并没有优雅的退出JVM。也会使得后续的线程进入提交订单的方法时,因无法成功的设置锁标志位而下单失败。所以说,上述的代码仍然存在问题。

引入Redis超时机制

在Redis中可以设置缓存的自动过期时间,我们可以将其引入到分布式锁的实现中,如下代码所示。

/** * 为了演示方便,我这里就简单定义了一个常量作为商品的id * 实际工作中,这个商品id是前端进行下单操作传递过来的参数 */ public static final String PRODUCT_ID = "100001"; @RequestMapping("/submitOrder") public String submitOrder(){ //通过stringRedisTemplate来调用Redis的SETNX命令,key为商品的id,value为字符串“binghe” //实际上,value可以为任意的字符换 Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, "binghe"); //没有拿到锁,返回下单失败 if(!isLock){ return "failure"; } try{ stringRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS); int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if(stock > 0){ stock -= 1; stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock)); logger.debug("库存扣减成功,当前库存为:{}", stock); }else{ logger.debug("库存不足,扣减库存失败"); throw new OrderException("库存不足,扣减库存失败"); } }finally{ //业务执行完成,删除PRODUCT_ID key stringRedisTemplate.delete(PRODUCT_ID); } return "success"; }

在上述代码中,我们加入了如下一行代码来为Redis中的锁标志设置过期时间。

stringRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS);

此时,我们设置的过期时间为30秒。

那么问题来了,这样是否就真正的解决了问题呢?上述程序就真的没有坑了吗?答案是还是有坑的!!

“坑位”分析

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wssxwg.html