锁一般用在多线程系统中,通过获取锁和释放锁来保证线程的串行执行,也就是同步排队执行。
在单体应用中我们可以使用synchronized关键字保证线程的同步执行,但是在分布式场景中,由于无法保证锁的唯一性,因此synchronized方法便不再可行。需要使用一个公共的锁
使用redis实现分布式锁主要使用了redis的set(setnx)命令 和expire命令
在介绍分布式锁之前先简单了解下redis的事务transaction (官方文档地址 https://redis.io/topics/transactions)
All the commands in a transaction are serialized and executed sequentially
在事务中的所有命令都是串行执行的(注意事务中执行失败的命令不会影响执行成功的命令)
A Redis script is transactional by definition
redis脚本是默认一个事务
下面介绍两种不同的分布式场景下使用的分布式锁(官方文档地址 https://redis.io/topics/distlock)
场景一:一个监听redis key过期的服务部署在两台服务器上,当监听到key过期事件时只需要任意一个服务器上的代码被执行(未获取到锁的直接return)
spring-data-redis代码如下
@Autowired
private StringRedisTemplate redisTemplate;
private Boolean lock(String lockKey){
Long timeOut = redisTemplate.getExpire(lockKey);
SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
List<Object> exec = null;
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForValue().setIfAbsent(lockKey,"lock");
if(timeOut == null || timeOut == -2) {
operations.expire(lockKey, 30, TimeUnit.SECONDS);
}
exec = operations.exec();
if(exec.size() > 0) {
return (Boolean) exec.get(0);
}
return false;
}
};
return redisTemplate.execute(sessionCallback);
}
private void unlock(String lockKey){
redisTemplate.delete(lockKey);
}
jedis代码如下
private JedisPool pool = new JedisPool("192.168.92.128",6380);
public boolean lock(String lockKey){
Jedis jedis = pool.getResource();
Long timeOut = jedis.ttl(lockKey);
Transaction transaction = jedis.multi();
transaction.setnx(lockKey,"lock");
if(timeOut == null || timeOut == -2) {
transaction.expire(lockKey,30);
}
List<Object> list = transaction.exec();
if(!list.isEmpty()){
Long l = (Long)list.get(0);
return l == 1L;
}
return false;
}
使用 SET resource_name my_random_value NX PX 30000 命令 Jedis的String set(String key, String value, SetParams params)接口
private JedisPool pool = new JedisPool("192.168.92.128",6380);
public boolean lock(String lockKey){
Jedis jedis = pool.getResource();
//不同jedis版本可能接口不一样
String str = jedis.set(lockKey,"lock", SetParams.setParams().nx().ex(30));
return str != null && str.equals("OK");
}
使用lua脚本
private JedisPool pool = new JedisPool("192.168.92.128",6380);
public boolean lockLua(String lockKey){
String script = "if redis.call('exists',KEYS[1]) == 1 then return 0 else " +
"redis.call('set',KEYS[1],ARGV[1]); redis.call('expire',KEYS[1],ARGV[2]);return 1 end;";
Jedis jedis = pool.getResource();
String argv1 = "lock";
String argv2 = "30";
List<String> keys = new ArrayList<>();
List<String> args = new ArrayList<>();
keys.add(lockKey);
args.add(argv1);
args.add(argv2);
Long result = (Long) jedis.eval(script, keys,args);
return result == 1;
}
场景二:一个减少库存的接口部署在两台服务器上,当库存小于一定数量时,4个用户a、b、c、d同时请求这个接口,需要保证a、b、c、d串行执行(执行顺序与其获取锁的顺序相同)
spring-data-redis代码如下
public boolean lock(String lockKey){
long timeout = 30000;
long start = System.currentTimeMillis();
while (true){
Boolean b = redisTemplate.execute(new SessionCallback<Boolean>() {
List<Object> exec = null;
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForValue().setIfAbsent(lockKey,"lock");
operations.expire(lockKey, 30, TimeUnit.SECONDS);
exec = operations.exec();
if(exec.size() > 0) {
return (Boolean) exec.get(0);
}
return false;
}
});
if(b != null && b){
return true;
}
long l = System.currentTimeMillis() - start;
//超时未获取锁直接返回false
if (l>=timeout) {
return false;
}
try {
//未获取锁时每100毫秒尝试获取锁
Thread.sleep(100);
} catch (InterruptedException e) {
//
}
}
}
public void unlock(String lockKey){
redisTemplate.delete(lockKey);
}
测试代码如下
@Test
public void testLock(){
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
if(lock("lock_key")){
try {
System.out.println("t1 thread start");
Thread.sleep(3000);
System.out.println("t1 thread end");
unlock("lock_key");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
if(lock("lock_key")){
try {
System.out.println("t2 thread start");
Thread.sleep(3000);
System.out.println("t2 thread end");
unlock("lock_key");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.start();
t2.start();
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
打印结果如下
t1 thread start
t1 thread end
t2 thread start
t2 thread end
当去掉run方法的lock时将会明显看到线程的并行执行
上面的加锁过程有一个问题,当方法的执行时间超过redis锁设置的过期时间时,会导致锁被释放其他的线程获取到锁,从而导致方法的并行执行
解决方法可以在获取锁后执行的方法中开启一个守护线程轮询锁是否过期,过期则自动延期 setDaemon(true) 测试代码如下
@Test
public void testDaemon() throws Exception{
if(lock("lock_key")){
boolean[] flag = new boolean[]{false};
Thread d = new Thread(new Runnable() {
@Override
public void run() {
while (true){
String value = redisTemplate.opsForValue().get("lock_key");
if(StringUtils.isEmpty(value)){
lock("lock_key");
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果不是守护线程则在方法外定义一个跳出循环的flag
if(flag[0]){
break;
}
}
}
});
// d.setDaemon(true);//开启守护线程
d.start();
String value = redisTemplate.opsForValue().get("lock_key");
System.out.println(value);
Thread.sleep(31000);
value = redisTemplate.opsForValue().get("lock_key");
System.out.println(value);
flag[0] = true;
}
}
使用redisson实现redis分布式锁 https://github.com/redisson/redisson https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers
导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.15.0</version>
</dependency>
Config config = new Config();
{
config.useSingleServer().setAddress("redis://192.168.92.128:6380");
}
获取锁的实例
RedissonClient client = Redisson.create(config);
{
RLock lock = client.getLock("lock_key");
lock.lock();
lock.unlock();
}
加锁与解锁代码如下
Config config = new Config();
{
config.useSingleServer().setAddress("redis://192.168.92.128:6380");
}
RedissonClient client = Redisson.create(config);
public boolean lockRedisson(String lockKey){
RLock lock = client.getLock(lockKey);
//获取锁 如果20秒内没有获取到锁则获取失败,获取到锁后如果30秒内锁未失效则自动失效
try {
return lock.tryLock(20,30,TimeUnit.SECONDS);
} catch (InterruptedException e) {
return false;
}
}
public void unlockRedisson(String lockKey){
RLock lock = client.getLock(lockKey);
lock.unlock();
}
测试代码如下(与上面的testLock方法一样)
@Test
public void testLockRedisson(){
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
if(lockRedisson("lock_key")){
try {
System.out.println("t1 thread start");
Thread.sleep(3000);
System.out.println("t1 thread end");
unlockRedisson("lock_key");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
if(lockRedisson("lock_key")){
try {
System.out.println("t2 thread start");
Thread.sleep(3000);
System.out.println("t2 thread end");
unlockRedisson("lock_key");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.start();
t2.start();
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
两个线程也是串行执行的
redisson加锁与解锁的过程是通过lua脚本实现的
获取锁的lua脚本
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit,
long threadId, RedisStrictCommand<T> command) {
//过期时间
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
//如果锁不存在,则通过hset设置它的值,并设置过期时间
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
//如果锁已存在,并且锁的是当前线程,则通过hincrby给数值递增1
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
//如果锁已存在,但并非本线程,则返回过期时间ttl
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
可以看到redis中的锁使用的是hash结构
这段LUA代码看起来并不复杂,有三个判断:
通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功
通过hexists判断,如果锁已存在,并且锁的是当前线程,执行hincrby将对应的value+1,加锁成功(重入锁) 例如:
@Test
public void testRent(){
//可重入锁 无论一个线程中锁几次都是获取成功的 b=true b1= true 锁几次就需要释放几次锁
boolean b = lockRedisson("lock_key");
boolean b1 = lockRedisson("lock_key");
System.out.println(b);
System.out.println(b1);
//锁了两次需要释放两次
unlockRedisson("lock_key");
unlockRedisson("lock_key");
}
如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间,加锁失败
释放锁
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, EVAL,
//如果锁已经不存在, 发布锁释放的消息
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
//如果释放锁的线程和已存在锁的线程不是同一个线程,返回null
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
//通过hincrby递减1的方式,释放一次锁
//若剩余次数大于0 ,则刷新过期时间
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
//否则证明锁已经释放,删除key并发布锁释放的消息
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()),
LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}
如果锁已经不存在,通过publish发布锁释放的消息,解锁成功
如果解锁的线程和当前锁的线程不是同一个,解锁失败,抛出异常
通过hincrby递减1,先释放一次锁。若剩余次数还大于0,则证明当前锁是重入锁,刷新过期时间;若剩余次数等于0,删除key并发布锁释放的消息,解锁成功
注意a线程解锁b线程获取的锁会抛出异常