在开发集成的角度来说,当一个线程从上到下执行时,首先对程序进行加锁操作,然后执行业务代码,执行完成后,再进行释放锁的操作。理论上,加锁和释放锁时,操作的Redis Key都是一样的。但是,如果其他开发人员在编写代码时,并没有调用tryLock()方法,而是直接调用了releaseLock()方法,并且他调用releaseLock()方法传递的key与你调用tryLock()方法传递的key是一样的。那此时就会出现问题了,他在编写代码时,硬生生的将你加的锁释放了!!!
所以,上述代码是不安全的,别人能够随随便便的将你加的锁删除,这就是锁的误删操作,这是非常危险的,所以,上述的程序存在很严重的问题!!
那如何实现只有加锁的线程才能进行相应的解锁操作呢? 继续向下看。
如何实现加锁和解锁的归一化?什么是加锁和解锁的归一化呢?简单点来说,就是一个线程执行了加锁操作后,后续必须由这个线程执行解锁操作,加锁和解锁操作由同一个线程来完成。
为了解决只有加锁的线程才能进行相应的解锁操作的问题,那么,我们就需要将加锁和解锁操作绑定到同一个线程中,那么,如何将加锁操作和解锁操作绑定到同一个线程呢?其实很简单,相信很多小伙伴都想到了—— 使用ThreadLocal实现 。没错,使用ThreadLocal类确实能够解决这个问题。
此时,我们将RedisLockImpl类的代码修改成如下所示。
public class RedisLockImpl implements RedisLock{ @Autowired private StringRedisTemplate stringRedisTemplate; private ThreadLocal<String> threadLocal = new ThreadLocal<String>(); @Override public boolean tryLock(String key, long timeout, TimeUnit unit){ String uuid = UUID.randomUUID().toString(); threadLocal.set(uuid); return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit); } @Override public void releaseLock(String key){ //当前线程中绑定的uuid与Redis中的uuid相同时,再执行删除锁的操作 if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){ stringRedisTemplate.delete(key); } } }上述代码的主要逻辑为:在对程序执行尝试加锁操作时,首先生成一个uuid,将生成的uuid绑定到当前线程,并将传递的key参数操作Redis中的key,生成的uuid作为Redis中的Value,保存到Redis中,同时设置超时时间。当执行解锁操作时,首先,判断当前线程中绑定的uuid是否和Redis中存储的uuid相等,只有二者相等时,才会执行删除锁标志位的操作。这就避免了一个线程对程序进行了加锁操作后,其他线程对这个锁进行了解锁操作的问题。
继续分析我们将加锁和解锁的方法改成如下所示。
public class RedisLockImpl implements RedisLock{ @Autowired private StringRedisTemplate stringRedisTemplate; private ThreadLocal<String> threadLocal = new ThreadLocal<String>(); private String lockUUID; @Override public boolean tryLock(String key, long timeout, TimeUnit unit){ String uuid = UUID.randomUUID().toString(); threadLocal.set(uuid); lockUUID = uuid; return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit); } @Override public void releaseLock(String key){ //当前线程中绑定的uuid与Redis中的uuid相同时,再执行删除锁的操作 if(lockUUID.equals(stringRedisTemplate.opsForValue().get(key))){ stringRedisTemplate.delete(key); } } }相信很多小伙伴都会看出上述代码存在什么问题了!! 没错,那就是 线程安全的问题。
所以,这里,我们需要使用ThreadLocal来解决线程安全问题。
可重入性分析在上面的代码中,当一个线程成功设置了锁标志位后,其他的线程再设置锁标志位时,就会返回失败。还有一种场景就是在提交订单的接口方法中,调用了服务A,服务A调用了服务B,而服务B的方法中存在对同一个商品的加锁和解锁操作。
所以,服务B成功设置锁标志位后,提交订单的接口方法继续执行时,也不能成功设置锁标志位了。也就是说,目前实现的分布式锁没有可重入性。
这里,就存在可重入性的问题了。我们希望设计的分布式锁 具有可重入性 ,那什么是可重入性呢?简单点来说,就是同一个线程,能够多次获取同一把锁,并且能够按照顺序进行解决操作。
其实,在JDK 1.5之后提供的锁很多都支持可重入性,比如synchronized和Lock。
如何实现可重入性呢?
映射到我们加锁和解锁方法时,我们如何支持同一个线程能够多次获取到锁(设置锁标志位)呢?可以这样简单的设计:如果当前线程没有绑定uuid,则生成uuid绑定到当前线程,并且在Redis中设置锁标志位。如果当前线程已经绑定了uuid,则直接返回true,证明当前线程之前已经设置了锁标志位,也就是说已经获取到了锁,直接返回true。