一,Redis实现分布式锁
实现redis分布式锁需要保证几个特性:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
1, 一眼就看出来的错误
* 获取锁
public boolean tryLock(String key,String testValue,int expireSeconds) {
// 1,尝试获取redis锁, 获取成功直接返回
if (redisTemplate.opsForValue().setIfAbsent(key, testValue)) {
//若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
redisTemplate.expire(key, 5, TimeUnit.MINUTES);
}
...
}
* 解锁
public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
//不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的
jedis.del(lockKey);
}
2, 网络上最容易混淆的错误之处2,很多博客都写错了
public boolean tryLock(String key,int expireSeconds){
Date now = new Date();
Date newExpire = DateUtils.addSeconds(now, expireSeconds);
// 1,尝试获取redis锁, 获取成功直接返回
if(redisTemplate.opsForValue().setIfAbsent(key,newExpire)){
return true;
}
// 2,判断锁超时 - 防止原来的操作异常,没有运行解锁操作 防止死锁
Date oldExpire = (Date) redisTemplate.opsForValue().get(key);
if(oldExpire != null && now.compareTo(oldExpire) < 0){
return false;
}
// 3,锁超时, 放入新的锁
Date replacedExpire = (Date) redisTemplate.opsForValue().getAndSet(key,newExpire);
//getAndSet 获取原来key键对应的值并重新赋新值,防止并发
if (replacedExpire != null && replacedExpire.compareTo(oldExpire) == 0) {
return true;
} else {
return false;
}
}
这种方式错误在哪里?
1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
2. 当锁过期的时候,在执行第3步的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
3. 锁不具备拥有者标识,即任何客户端都可以解锁。
* 解锁
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
// 判断加锁与解锁是不是同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}
}
问题:jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?
答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了或者锁过期时间短正常过期,
此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。
那么正确的写法是什么呢? 采用redis+lua脚本实现,lua脚本有原子性的保障。 另外可以使用Redis官方提供的Java组件Redisson,如果你的项目中Redis是多机部署的,可以直接使用它。它的底层就是用lua脚本实现的。
3, redisson实现
public class LockTest {
private static RedissonClient redissonClient;
static {
Config config=new Config();
config.useSingleServer().setAddress("redis://***:6379");
config.useSingleServer().setPassword("***");
redissonClient= Redisson.create(config);
}
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5 ; i++) {
executorService.execute(new Runnable() {
public void run() {
try {
RLock rLock=redissonClient.getLock("updateAccount_feng");
// 最多等待100s,超过100s就放弃等待;上锁10s以后过期自动释放锁
if(rLock.tryLock(100,10, TimeUnit.SECONDS)){
System.out.println(Thread.currentThread().getName() + "--获取锁成功");
}
Thread.sleep(2000);
rLock.unlock();
System.out.println(Thread.currentThread().getName() + "--释放锁成功");
}catch (Exception e){
e.printStackTrace();
}
}
});
}
}
}
控制台输出----
redisson加锁和释放锁原理,在源码RedissonLock类中查看获取锁和释放锁的逻辑。
可以看出加锁和释放锁,为了保证redis的原子性,采用的是lua脚本实现的。
具体详情看下面的源码分析:Redis分布式锁redisson实现原理分析
2,Zookeeper实现分布式锁
public class MutexDemo {
public static void main(String[] args) {
CuratorFramework curatorFramework=
CuratorFrameworkFactory.builder().
connectString("81.68.204.211:2181").
sessionTimeoutMs(5000).
retryPolicy(new ExponentialBackoffRetry
(1000,3)).
connectionTimeoutMs(4000).build();
curatorFramework.start(); //表示启动.
/**
* locks 表示命名空间
* 锁的获取逻辑是放在zookeeper
* 当前锁是跨进程可见
*/
InterProcessMutex lock=new InterProcessMutex(curatorFramework,"/locks");
for(int i=0;i<10;i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"->尝试抢占锁");
try {
lock.acquire();//抢占锁,没有抢到,则阻塞
System.out.println(Thread.currentThread().getName()+"->获取锁成功");
} catch (Exception e) {
e.printStackTrace();
}
try {
Thread.sleep(4000);
lock.release(); //释放锁
System.out.println(Thread.currentThread().getName()+"->释放锁成功");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
},"t-"+i).start();
}
}
}
每个线程都尝试在zk上建立了临时有序的节点,然后按照最小的节点编号依次获取锁进行逻辑处理,当释放锁后,会进行通知其他线程可以同时抢占锁了,然后剩下的最小序号的节点会获取锁,依次往下执行... 下面是不断刷新ZooInspector的图示: