乐观锁常用于解决更改数据时的多线程安全问题,悲观锁则更多用于修改数据时的线程安全问题。
通过加锁可以解决在单机情况下的多线程安全问题,但是在集群模式下就不行了。当我们部署了多个tomcat,每个tomcat都有一个属于自己的jvm,那么假设在服务器A的tomcat内部,有两个线程,他们的锁对象是同一个,是可以实现互斥的,但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却与服务器A的不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是集群环境下,syn锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。
此处我们重点讨论基于Redis实现分布式的方法和原理。
redis基于setnx实现分布式锁的核心思想:利用setnx方法,将某个键作为锁的标识,通过 SETNX 来尝试获取锁,如果返回1表示获取锁成功,否则表示锁已被其他节点占用。
需要注意的是,由于 SETNX 命令是原子性的,当获取到锁后,需要在适当的时候释放锁,以避免锁的长期占用。
private static final String KEY_PREFIX="myLock:" @Override public boolean tryLock(long timeoutSec) { // 堆代码 duidaima.com // 获取线程标示 String threadId = Thread.currentThread().getId() // 获取锁 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); }
public void unlock() { //通过del删除锁 stringRedisTemplate.delete(KEY_PREFIX + name); }
当持有锁的线程1在锁的内部出现了阻塞,导致他的锁自动释放,这时线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,由于key一致,此时就会把本应该属于线程2的锁进行删除,这就是锁误删。
public void unlock() { // 获取线程标示 String threadId = ID_PREFIX + Thread.currentThread().getId(); // 获取锁中的标示 String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); // 判断标示是否一致 if(threadId.equals(id)) { // 释放锁 stringRedisTemplate.delete(KEY_PREFIX + name); } }在实际业务中,还存在更为极端的误删情况:线程1持有锁完成业务后,进入删除锁的方法,获取锁标识完成了条件判断,但是此时他的锁到期了,线程1超时释放锁,同时线程2进来,获得锁;当阻塞完成后,线程1会接着往后执行,进行锁删除,相当于条件判断并没有起到作用。
这是删锁时的原子性问题,之所以有这个问题,是因为线程1的拿锁,比锁,删锁,实际上并不是原子性的,我们要防止刚才的情况发生。
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示 -- 获取锁中的标示,判断是否与当前线程标示一致 if (redis.call('GET', KEYS[1]) == ARGV[1]) then -- 一致,则删除锁 return redis.call('DEL', KEYS[1]) end -- 不一致,则直接返回 return 0RedisTemplate中,可以利用execute方法去执行lua脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT; static { UNLOCK_SCRIPT = new DefaultRedisScript<>(); UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); UNLOCK_SCRIPT.setResultType(Long.class); } public void unlock() { // 调用lua脚本 stringRedisTemplate.execute( UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX + name), ID_PREFIX + Thread.currentThread().getId()); }