通过使用的 synchronized 或者 Lock 都是线程锁,对于同一个 JVM 进程中的多个线程有效,因为锁的本质 是内存中存放一个标记,记录获取锁的线程是谁,这个标记对每个线程都可见,在分布式系统中就凉凉了,本文利用 Redis 实现分布式锁
获取锁我们可以通过 redis 中的 setnx 实现,该命令如果 key 不存在设置成功返回1,存在返回0
设置成功后,我们可以通过 setex 命令设置过期时间即可(如果在释放锁之前服务突然宕机,那么这个锁就成了死锁,永远不会释放,所以需要设置过期时间),释放锁使用 del key 删除掉这个 key 就行了
@Component public class RedisLock { @Autowired private RedisTemplate<String, String> redisTemplate; // 防止死锁超时时间 private int timeOut = 10; public boolean lock(String key, String value) { try { if (redisTemplate.opsForValue().setIfAbsent(key, value)) { redisTemplate.expire(key,timeOut, TimeUnit.SECONDS); return true; } return false; } catch (Exception e) { e.printStackTrace(); return false; } } public boolean unlock(String key) { try { if (key == null) { return false; } return redisTemplate.delete(key); } catch (Exception e) { e.printStackTrace(); return false; } } }在上面这个分布式锁案例中分布式锁是实现了,但是可能会出现两个问题
setnx 获取锁成功了,还没来得及 setex 服务就宕机了,由于两个操作是非原子性的死锁又发生了。(不过在 redis 2.6 及以后提供了 set nx ex 连用的命令)
如此一来问题就出现了,B和C同时获取了锁,违反了互斥性,如何解决这个问题呢,我们应该在删除锁之前先判断,这个锁是不是自己设置的锁,如果不是那就不要删除了。
所以我们要在设置锁的之后在 value 中设置当前服务的唯一标识,用于在删除锁的时候进行判断,这个锁是不是我们自己设置的
使用 Lua 脚本保证 setex nx 命令原子性解决获取锁时的问题,在设置锁的时候 value 设置为当前服务唯一值,可以使用当前内网 IP ,在释放锁的时候进行判断锁的 value 值是否与当前内网 IP 地址一致
@Component public class RedisLock { @Autowired private RedisTemplate redisTemplate; public boolean lock(String key, String value) { try { if (luaExpress(key,value)) { return true; } return false; } catch (Exception e) { e.printStackTrace(); return false; } } public boolean unlock(String key, String value) { try { if (redisTemplate.opsForValue().get(key).equals(value)) { return redisTemplate.delete(key); } return false; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 获取lua结果 * @param key * @param value * @return */ public Boolean luaExpress(String key,String value) { DefaultRedisScript<Boolean> lockScript = new DefaultRedisScript<Boolean>(); lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("add.lua"))); lockScript.setResultType(Boolean.class); List<Object> keyList = new ArrayList<Object>(); keyList.add(key); keyList.add(value); Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList); return result; } }Lua setnx setex add 脚本
local lockKey = KEYS[1] local lockValue = KEYS[2] -- setnx info local result_1 = redis.call('SETNX', lockKey, lockValue) if result_1 == true then local result_2= redis.call('SETEX', lockKey,3600, lockValue) return result_1 else return result_1 end