参考文档 redis分布式锁官方介绍
前言
多线程并发执行情况下如何保证一个代码块在同一时间只能由同一线程执行(同一个JVM中多线程同步执行)?
可以使用线程锁的机制(如synchronized,ReentrantLock类)
synchronized(obj){
......
}
ReentrantLock lock = new ReentrantLock();
lock.lock();
.....
lock.unlock();

在分布式的集群环境中如何保证不同节点的线程同步执行?
分布式锁
这里我们介绍使用Redis实现
如何用Redis实现分布式锁
一、核心要素
Redis分布式锁的基本流程并不难理解,但要想写得尽善尽美,也并不是那么容易。在这里,我们需要先了解分布式锁实现的三个核心要素:
1.加锁
最简单的方法是使用setnx命令。key是锁的唯一标识,按业务来决定命名。比如想要给一种商品的秒杀活动加锁,可以给key命名为 “lock_sale_商品ID” 。而value设置成什么呢?我们可以姑且设置成1。加锁的伪代码如下:
setnx(key,1)
当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁;当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败。
2.解锁
有加锁就得有解锁。当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行del指令,伪代码如下:
del(key)
释放锁之后,其他线程就可以继续执行setnx命令来获得锁。
3.锁超时
锁超时是什么意思呢?如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程再也别想进来。
所以,setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。setnx不支持超时参数,所以需要额外的指令,伪代码如下:
expire(key, 30)
综合起来,我们分布式锁实现的第一版伪代码如下:
if(setnx(key,1) == 1){
expire(key,30)
try {
do something ......
} finally {
del(key)
}
}
二、存在的问题
1. setnx和expire的非原子性
设想一种极端场景,当某线程执行setnx.获得锁,setnx刚执行成功,还未来执行expire命令,节点1突然挂掉,这样锁没有设置过期时间,永远无法被释放。
解决?
Redis2.6.12版本,改进了set方法,可以同时设置value和expire
SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
-
− 设置指定的到期时间(以秒为单位)。EX seconds
-
- 设置指定的到期时间(以毫秒为单位)。PX milliseconds
-
- 仅在键不存在时设置键。NX
-
- 只有在键已存在时才设置。XX
redis 命令 :set key value ex 30 nx
Jedis java :
String result = jedis.set(key,threadId,"NX","EX",30);
2.del误删
假如线程A获得锁并且设置超时时间为30秒,某种原因导致线程A执行很慢,超过30秒后线程A锁自动过期,释放了锁,线程B获了锁。随后线程A执行完成,del删除锁,但线程B还未执行完成,实际上线程A删除的是线程B的锁。
解决?
保证加锁和删除锁是同一个线程,在del前加判断条件。如可以把uuid当前value ,在删除前验证key的value是不是当前线程的uuid
伪代码
if(uuild.equals(redisClient.get(key))){
del(key);
}
但判断和删除锁是两个独立的操作,并不是原子级别的。
我们使用Lua脚本来解决子性操作 (redis2.6后特性)
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId)); //Collections.singletonList——用来生成只读 的单一元素的List
3.出现并发的可能
上述第二点场景,虽然避免了key误删,但当超时时,还是存在A,B两个线程同时访问代码块。
解决?
让获得锁的线程开启一个守护线程,给快要过期但认为未完成的锁验证过期时间。
关于守护线程的生命周期: 与锁线程周期一致,线程A执行完成会显式关掉守护线程。
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
//....延时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.setDaemon(true);//设置当前线程为守护线程
t.start();
三、实现
基于上面原理,jedis整合spring来简单实现单实例分布锁
spring-redis.xml
使用sharedJedisPool
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="minIdle" value="${redis.minIdle}" />
<property name="maxIdle" value="${redis.maxIdle}" />
<property name="maxTotal" value="${redis.maxActive}" />
<property name="maxWaitMillis" value="${redis.maxWait}" />
<property name="testOnBorrow" value="${redis.testOnBorrow}" />
</bean>
<bean id="shardedJedisPool" class="redis.clients.jedis.ShardedJedisPool">
<constructor-arg index="0" ref="poolConfig" />
<constructor-arg index="1">
<list>
<bean class="redis.clients.jedis.JedisShardInfo">
<constructor-arg index="0" value="${redis.host}" />
<constructor-arg name="port" value="${redis.port}"/>
<property name="password" value="${redis.password}" />
</bean>
</list>
</constructor-arg>
</bean>
RedisDistributedLockServiceImpl 实现阻塞式加锁、解锁
import com.ticket.service.RedisDistributedLockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Service("redisDistributedLockService")
public class RedisDistributedLockServiceImpl implements RedisDistributedLockService {
private final static String LOCK_SUFFIX = "_redis_lock";
/**
* 重试时间
*/
private static final int DEFAULT_ACQUIRY_RETRY_MILLIS = 20;
@Autowired
private ShardedJedisPool shardedJedisPool;
// value值和执行线程相联系,可以用uuid避免重复
@Override
public boolean lock(String lockKey,String value) throws InterruptedException {
String key = lockKey + LOCK_SUFFIX ;
ShardedJedis jedis = shardedJedisPool.getResource();
while(true){
//30秒后过期,释放锁
String result = jedis.set(key,value,"NX","EX",30);
if("OK".equals(result)){
jedis.close();
return true;
}
TimeUnit.MILLISECONDS.sleep(DEFAULT_ACQUIRY_RETRY_MILLIS);
}
}
@Override
public void unlock(String lockKey ,String value) {
String key = lockKey + LOCK_SUFFIX ;
ShardedJedis shardedJedis = shardedJedisPool.getResource();
Jedis jedis = shardedJedis.getShard(key);
//保证加锁和解锁为同一线程,通过value值判断
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end ";
Object obj = jedis.eval(luaScript,Collections.singletonList(key),Collections.singletonList(value));;
}
}
建议:
1.添加守护线程给快要过期且未完成任务的线程,延长锁过期时间。
2.不建议使用spring的redisTemplate,并不没有提供set(key,value,"NX","EX",30) 类似的操作,除非使用lua脚本操作
小结
其他更好实现,参考官方基于RedLock算法的Redisson方式,它有一整套的解决方案
参考 Redisson Wiki Home