当程序执行业务的时间超过了锁的过期时间会发生什么呢? 想必很多小伙伴都能够想到,那就是前面的请求没执行完,锁过期失效了,后面的请求获取到分布式锁,继续向下执行了,程序无法做到真正的互斥,无法保证业务的原子性了。
那如何解决这个问题呢?答案就是:我们必须保证在业务代码执行完毕后,才能释放分布式锁。 方案是有了,那如何实现呢?
说白了,我们需要在业务代码中,时不时的执行下面的代码来保证在业务代码没执行完时,分布式锁不会因超时而被释放。
springRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS);这里,我们需要定义一个定时策略来执行上面的代码,需要注意的是:我们不能等到30秒后再执行上述代码,因为30秒时,锁已经失效了。例如,我们可以每10秒执行一次上面的代码。
有些小伙伴说,直接在RedisLockImpl类中添加一个while(true)循环来解决这个问题,那我们就这样修改下RedisLockImpl类的代码,看看有没有啥问题。
public class RedisLockImpl implements RedisLock{ @Autowired private StringRedisTemplate stringRedisTemplate; private ThreadLocal<String> threadLocal = new ThreadLocal<String>(); private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>(); @Override public boolean tryLock(String key, long timeout, TimeUnit unit){ Boolean isLocked = false; if(threadLocal.get() == null){ String uuid = UUID.randomUUID().toString(); threadLocal.set(uuid); isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit); //如果获取锁失败,则自旋获取锁,直到成功 if(!isLocked){ for(;;){ isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit); if(isLocked){ break; } } } //定义更新锁的过期时间 while(true){ Integer count = threadLocalInteger.get(); //当前锁已经被释放,则退出循环 if(count == 0 || count <= 0){ break; } springRedisTemplate.expire(key, 30, TimeUnit.SECONDS); try{ //每隔10秒执行一次 Thread.sleep(10000); }catch (InterruptedException e){ e.printStackTrace(); } } }else{ isLocked = true; } //加锁成功后将计数器加1 if(isLocked){ Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get(); threadLocalInteger.set(count++); } return isLocked; } @Override public void releaseLock(String key){ //当前线程中绑定的uuid与Redis中的uuid相同时,再执行删除锁的操作 if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){ Integer count = threadLocalInteger.get(); //计数器减为0时释放锁 if(count == null || --count <= 0){ stringRedisTemplate.delete(key); } } } }相信小伙伴们看了代码就会发现哪里有问题了:更新锁过期时间的代码肯定不能这么去写。因为这么写会 导致当前线程在更新锁超时时间的while(true)循环中一直阻塞而无法返回结果。 所以,我们不能将当前线程阻塞,需要异步执行定时任务来更新锁的过期时间。
此时,我们继续修改RedisLockImpl类的代码,将定时更新锁超时的代码放到一个单独的线程中执行,如下所示。
public class RedisLockImpl implements RedisLock{ @Autowired private StringRedisTemplate stringRedisTemplate; private ThreadLocal<String> threadLocal = new ThreadLocal<String>(); private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>(); @Override public boolean tryLock(String key, long timeout, TimeUnit unit){ Boolean isLocked = false; if(threadLocal.get() == null){ String uuid = UUID.randomUUID().toString(); threadLocal.set(uuid); isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit); //如果获取锁失败,则自旋获取锁,直到成功 if(!isLocked){ for(;;){ isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit); if(isLocked){ break; } } } //启动新线程来执行定时任务,更新锁过期时间 new Thread(new UpdateLockTimeoutTask(uuid, stringRedisTemplate, key)).start(); }else{ isLocked = true; } //加锁成功后将计数器加1 if(isLocked){ Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get(); threadLocalInteger.set(count++); } return isLocked; } @Override public void releaseLock(String key){ //当前线程中绑定的uuid与Redis中的uuid相同时,再执行删除锁的操作 String uuid = stringRedisTemplate.opsForValue().get(key); if(threadLocal.get().equals(uuid)){ Integer count = threadLocalInteger.get(); //计数器减为0时释放锁 if(count == null || --count <= 0){ stringRedisTemplate.delete(key); //获取更新锁超时时间的线程并中断 long threadId = stringRedisTemplate.opsForValue().get(uuid); Thread updateLockTimeoutThread = ThreadUtils.getThreadByThreadId(threadId); if(updateLockTimeoutThread != null){ //中断更新锁超时时间的线程 updateLockTimeoutThread.interrupt(); stringRedisTemplate.delete(uuid); } } } } }