Redis分布式锁 概述 所谓分布式锁,应当基本如下几项核心性质:
独占性:对于同一把锁,在同一时刻只能被一个取锁方占有,这是锁最基础的一项特征
健壮性:即不能产生死锁(dead lock). 假如某个占有锁的使用方因为宕机而无法主动执行解锁动作,锁也应该能够被正常传承下去,被其他使用方所延续使用
对称性:加锁和解锁的使用方必须为同一身份. 不允许非法释放他人持有的分布式锁
高可用:当提供分布式锁服务的基础组件中存在少量节点发生故障时,应该不能影响到分布式锁
服务的稳定性
setnx 实现分布式锁时需要实现的两个基本方法:
互斥:确保只能有一个线程获取锁
1 2 3 4 5 6 SETNX lock thread1 EXPIRE lock 10 # 添加锁,NX是互斥,EX是设置超时时间 SET lock thread1 NX EX 10
手动释放
超时释放:获取锁时添加的一个超时时间
基于setnx实现简单的Redis锁 lock接口
1 2 3 4 public interface MyLock { public boolean tryLock (long timeoutSec) ; public void unlock () ; }
实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RequiredArgsConstructor public class SimpleRedisLock implements MyLock { private String name; private StringRedisTemplate stringRedisTemplate; private static final String KEY_PREFIX="lock:" ; @Override public boolean tryLock (long timeoutSec) { long threadId=Thread.currentThread().getId(); Boolean success=stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX+name,threadId+"" ,timeoutSec,TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } @Override public void unlock () { stringRedisTemplate.delete(KEY_PREFIX+name); } }
改进Redis的分布式锁 误删锁
线程1阻塞,导致Redis超时释放锁
线程2获取锁,线程1恢复执行删除锁
线程1删除的是线程2的锁
线程3此时加锁,加锁成功
方案
在获取锁时存入线程标识(可以使用UUID表示)
在释放锁时先获取锁中的线程标识,判断是否与当前线程标识相同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private static final String ID_PREFIX=UUID.randomUUID.toString(true )+"-" ;@Override public boolean tryLock (long timeoutSec) { String thread = ID_PREFIX+Thread.currentThread().getId(); long threadId=Thread.currentThread().getId(); Boolean success=stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX+name,threadId+"" ,timeoutSec,TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } @Override public void unlock () { String threadId=ID_PREFIX+Thread.currentThread.getId(); String od=stringRedisTemplate.opsForValue.get(KEY_PREFIX+name); if (threadId.equals(id)){ stringRedisTemplate.delete(KEY_PREFIX+name); } }
判断后阻塞
线程1准备释放锁,判断标识成功
线程1阻塞,超时释放锁
线程2获取锁,线程1恢复执行删除锁
线程1删除的是线程2的锁
线程3此时加锁,加锁成功
Redis的Lua脚本 Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性
Redis提供的函数调用,语法如下:
1 redis.call('命令名称','key','其他参数',...)
例如,执行set name wereash 则脚本是这样的:
1 redis.call('set','name','wereash')
例如,我们要执行set name wereash,再执行get name,则脚本如下:
1 2 3 redis.call('set','name','wereash') local name=redis.call('get','name') return name
写好脚本以后,需要用Redis命令来调用脚本
例如,要执行redis.call(‘set’,’name’,’wereash’)这个脚本,语法如下:
1 EVAL "return redis.call('set','name','wereash')" 0 #0 是脚本需要的key类型的参数个数
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其他参数放入ARGV数组,在脚本中可以从KeYS和ARGV数组获取这些参数:
1 EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 name wereash
Lua脚本实现原子性 Lua脚本
1 2 3 4 5 6 if (redis.call('GET' ,KEYS[1 ])==ARGV[1 ]) then return redis.call('DEL' ,KEYS[1 ]) end return 0
Redis锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static { UNLOCK_SCRIPT=new DefaultRedisScript <>(); UNLOCK_SCRIPT.setLocation(new ClassPathResource ("unlock.lua" )); UNLOCK_SCRIPT.setResultType(Long.class); } @Override public void unlock () { stringRedisTemplate.execute( UNLOCK_SCRIPT, Colletions.singletonList(KEY_PREFIX+name), ID_PREFIX+Thread.currentThread().getId() ); }
缺点
不可重入:同一个线程无法多次获取同一把锁
同一个线程中,方法1获取了锁并调用方法2,方法2就没办法再次获取锁
不可重试:获取锁只尝试一次就返回false,没有重试机制
超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致所释放,存在安全隐患
主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当在执行setxn获取锁后,还未同步,主节点宕机,新的主节点没有获取之前的锁数据
Redisson Redisson是一个在Redis的基础上实现的Java驻内存数据网络(In-Memory Data-Grid)。它不仅提供了一系列分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种的分布式锁的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Resource private RedissonClient redissonClient;@Test void testRedisson () throws InterruptedException{ RLock lock=redissonClient.getLock("anyLock" ); boolean isLock=lock.tryLock(1 ,10 ,TimeUnit.SECONDS); if (isLock){ try { }finally { lock.unlock(); } } }