一、分布式锁和普通锁的区别
1.普通的锁
用来解决一个进程的多个线程同时操作同一资源的问题,如果是在分布式系统或者集群中,普通的锁是锁不住的。如图1:
2.分布式锁
用来解决多个进程同时操作同一资源的问题。他的原理就是所有的获取到同一把锁,无论有多少个服务,只有一个服务可以获取到锁,其他没有获得到的锁的服务则进行等待或者自旋等,直到锁释放,其他的服务才可以尝试获取到锁,如图2:
二、Redis实现分布式锁
1.使用setnx加锁和释放锁
Redis有一个setnx可以实现分布式锁,sexnx在指定的key不存在时,会为key设置指定的值,若给定的key已存在,setnx不做任何操作。
if(jedis.setnx(lock_stock,1) == 1){ //获取锁
try {
//需要执行的业务代码
} finally {
//释放锁
jedis.del(lock_stock)
}
2.为锁设置过期时间
使用setnx的命令进行加锁和释放锁,我们可以发现可能会存在死锁的现象,当我们在锁释放之前,服务宕机了,就会导致锁无法释放,那么其他的服务也就无法获取到锁,就会造成死锁,因此,我们在加锁时还应该为锁设置一个过期的时间。
if(jedis.setnx(lock_stock,1) == 1){ //获取锁
//设置锁超时
expire(lock_stock,2)
try {
//需要执行的业务代码
} finally {
//释放锁
jedis.del(lock_stock)
}
}
但是使用了expire设置了过期时间,仍会存在一种极值的现象,就是若在加锁和设置锁超时之间,服务发生了宕机,依然会发生死锁的现象,因此我们需要去保证加锁和释放的原子性。Redis中提供了一个set命令可以解决这个问题,相当于是加锁的设置过期时间的一个组合命令。
if(set(lock_stock,1,"NX","EX",2) == 1){ //获取锁并设置超时
try {
//需要执行的业务代码
} finally {
//释放锁
del(lock_stock)
}
}
- EX : 将键的过期时间设置为秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。
- PX : 将键的过期时间设置为毫秒。 执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。
- NX : 只在键不存在时,才对键进行设置操作。 执行 SET key value NX 的效果等同于执行 SETNX key value 。
- XX : 只在键已经存在时, 才对键进行设置操作。
3.删除锁之前判断锁
使用set命令进行加锁和释放锁,虽然保证了原子性,但是又出现了新的问题,如果我们需要执行的业务代码时间超过了锁设置的过期时间,那么在锁达到过期时间时会将这个锁释放,而在执行完业务后又进行了一次锁的删除,就会导致第二次删除的锁不是执行该业务时加的锁,造成锁的一个误删除现象。为解决这一问题,我们需要在删除锁的时候进行锁的判断,判断当前删除的锁是不是自己的锁。
String uuid = UUID.randomUUID().toString();
if(jedis.set(lock_stock,uuid,"NX","EX",5) == 1){ //获取锁并设置超时
try {
//执行自己的业务代码
} finally {
//获取锁的值
String lockValue = jedis.get(lock_stock);
//判断是不是自己的锁
if(lockValue.equals(uuid)){
//释放锁
jedis.del(lock_stock)
}
}
}
除了要判断锁,我们还应该去保证判断锁和删除锁的一个原子性,否则依然会有极值的问题,导致锁的误删除,因此这里使用Redis+Lua脚本来解决一致性的问题。
String script = "if redis.call('get', KEYS[1]) == ARGV[1]
then return redis.call('del', KEYS[1]) else return 0 end";
上述的这段Lua脚本,可以保证多个命令的原子性,其中
- redis.call(‘get’, KEYS[1]) :是调用redis的get命令,key可以通过参数传入
- == ARGV[1] :意思是是否和 某个值相等,这里的值也可以参数传入
- then return redis.call(‘del’, KEYS[1]) :如果相等就执行 redis.call('del', KEYS[1]) 删除操作
- else return 0 end :否则就返回
修改上述的代码为:
String uuid = UUID.randomUUID().toString();
if(jedis.set(lock_stock,uuid,"NX","EX",2) == 1){ //获取锁并设置超时
try {
//需要执行的业务代码
} finally {
//lua脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//执行脚本
jedis.eval(script, Collections.singletonList("lock_stock"),Collections.singletonList(uuid));
}
}
到此一个基本的redis实现分布式锁就完成了,但是考虑到实际的业务,未获得锁的线程可以做重入,等待一会儿,再次尝试获取锁。
public void method(){
String uuid = UUID.randomUUID().toString();
if(jedis.set(lock_stock,uuid,"NX","EX",5) == 1){ //获取锁并设置超时
try {
//需要执行的业务代码
} finally {
//lua脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//执行脚本
jedis.eval(script, Collections.singletonList("lock_stock"),Collections.singletonList(uuid));
}
}else{
//休眠之后重入方法,尝试获取锁,休眠的时间可根据自己的时间调整
Thread.sleep(100);
//自旋,重新进入方法
method();
}
}