实现方式一:存在抛异常后lock值无法归0的问题
@Autowired private StringRedisTemplate stringRedisTemplate;
@RequestMapping("deduct_stock") public void deductStock(){ Long num = stringRedisTemplate.opsForValue().increment("lock", 1); //多个线程过来 只有一个线程会将num值设置为1 其余的线程都不可能为1 if (num==1){ int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if (stock>0){ stringRedisTemplate.opsForValue().set("stock",(stock-1)+""); System.out.println("扣减成功,库存stock:"+(stock-1)+""); }else{ System.out.println("扣减失败,库存不足"); } } //还原 stringRedisTemplate.opsForValue().increment("lock",-1); }
实现方式二:加try…finally。无论程序是否抛出异常,最终都会还原。但是在还原为0的时候有可能redis挂了,或者是程序还没执行完try代码块中的内容,整个web应用挂掉了,finally块中的内容也无法执行。即使程序重启,后面的线程也始终无法满足num==1的条件。
@Autowired private StringRedisTemplate stringRedisTemplate; @RequestMapping("deduct_stock") public void deductStock(){ try{ Long num = stringRedisTemplate.opsForValue().increment("lock", 1); //多个线程过来 只有一个线程会将num值设置为1 其余的线程都不可能为1 if (num==1){ int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if (stock>0){ stringRedisTemplate.opsForValue().set("stock",(stock-1)+""); System.out.println("扣减成功,库存stock:"+(stock-1)+""); }else{ System.out.println("扣减失败,库存不足"); } } }finally { //还原 stringRedisTemplate.opsForValue().increment("lock",-1); } }
实现方式三:设置lock的超时时间。存在这么一个场景,由于某一段时间内,网络环境差,导致本应10秒内就执行完的程序执行了15秒才完成,而锁已经过期了,也就意味着后面的线程能拿到锁,导致锁形同虚设。 还存在这么一个情况:第一个线程执行时长15秒,假设第二个线程执行时长8秒,由于锁已失效,第二个线程是能重新拿到新锁的,结果第一个线程在执行归0操作释放锁时,它自己的锁已失效,导致释放的并不是自己的锁,而是第二个线程的锁。总之,存在一系列场景导致锁失效。
@Autowired private StringRedisTemplate stringRedisTemplate; @RequestMapping("deduct_stock") public void deductStock(){ try{ String lockkey = "lock"; Long num = stringRedisTemplate.opsForValue().increment(lockkey, 1); //给key设置超时时间 到期自动清理 stringRedisTemplate.expire(lockkey,10, TimeUnit.SECONDS); //多个线程过来 只有一个线程会将num值设置为1 其余的线程都不可能为1 if (num==1){ int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if (stock>0){ stringRedisTemplate.opsForValue().set("stock",(stock-1)+""); System.out.println("扣减成功,库存stock:"+(stock-1)+""); }else{ System.out.println("扣减失败,库存不足"); } } }finally { //还原 stringRedisTemplate.opsForValue().increment("lock",-1); } }
这种场景下,可以开启一个分线程,与当前线程绑定,如果锁的失效时间是10秒,那么就每隔5秒去扫描一下这把锁,看看锁是否还在,如果还在就再次将失效时间重置为10秒,不断延时,也就相当于让这把锁具备了自动延时的功能,直到当前线程亲自将这把锁释放,否则一致延时下去。
Redisson框架实现分布式锁
上述分析也就是Redisson的实现原理,只不过会增加一个线程,让等待的线程不断地尝试加锁,通过while循环来实现的,俗称就是自旋加锁。
配置单机Redis
@Configuration public class RedissonConfig { @Bean("redisson")//如果不写value 默认就会以方法名作为bean的名称进行注入 public Redisson getRedisson(){ Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0); return (Redisson) Redisson.create(config); } }
加锁代码:很简单,就三行代码。重要的是理解运行原理。
@SuppressWarnings("all") @RestController public class RedissionController { @Autowired private Redisson redisson; @RequestMapping("redisson_lock") public void redissonDeductStock() { String lockkey = "lock"; RLock lock = redisson.getLock(lockkey); try { lock.lock();//如果使用默认的午餐lock方法 默认超时时间设置的是30秒 可以传入自定义的超时时间 int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if (stock > 0) { stringRedisTemplate.opsForValue().set("stock", (stock - 1) + ""); System.out.println("扣减成功,库存stock:" + (stock - 1) + ""); } else { System.out.println("扣减失败,库存不足"); } } finally { lock.unlock(); } } }
Redisson实质上就是给代码加上了一把悲观锁,而悲观锁是比较影响性能的。increment自增加1的方式实质是乐观锁。 Redis天生就是单线程的,在高并发环境主从集群还是会存在一定问题。