前言
锁可以理解为多线程情况下访问共享资源的一种线程同步机制。单机部署的应用中(单个JVM),通过Java提供synchronized关键字或者Lock机制可以实现。但是微服务架构中,通常一个服务都会部署多台(多个JVM),用Java的锁机制就无法实现了。此时就需要用到分布式锁。今天主要介绍一下基于Redis来实现分布式锁。
Redis实现分布式锁的基本思想
我们将需要加锁的公共资源设置一个相同的key。当操作该资源时,先用Redis的setNX(如果不存在则新增)命令往Redis中存储数据。
如果新增数据成功,则表示获取到锁。然后执行业务逻辑,执行完毕后将key对应数据删除,把锁释放。
如果新增数据失败,则表示获取锁失败。表示Redis中已有该key对应的数据,则表示此时有其他线程先获取到了锁,正在执行业务逻辑。
存在一种情况,某个线程在获取到锁后,由于服务器宕机。导致没有释放锁。那么可能会导致该锁永远无法释放。所以,我们需要给Redis的数据设置一个过期时间。超时则自动释放锁。
参考代码如下:
public Boolean lock(String lockKey){
String value = "AWSL";//(1)
Boolean lock = false;//(2)
try {
lock = redisTemplate.opsForValue().setIfAbsent(lockKey, value, 2, TimeUnit.SECONDS);//(3)
if(lock){//(4)
log.info("获取到锁,执行业务逻辑");//(5)
Thread.sleep(3000);
} else {
log.info("未获取到锁");//(6)
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(lock){//(7)
String lockValue = redisTemplate.opsForValue().get(lockKey);//(8)
if(value.equals(lockValue)){//(9)
return redisTemplate.delete(lockKey);//(10)
}
}
}
return lock;//(11)
}
这段Redis实现锁的代码存在一些问题。
1.假设线程A获取到了锁。锁的过期时间为2S。但是A执行业务逻辑用了3S。因此2S时锁已经过期释放了。此时B线程可以获取到锁。然后A业务逻辑处理完毕,走到释放锁的代码(7)(8)(9)(10)。根据(7)(8)(9)(10)行代码,此时线程A根据lockKey删除了Redis中的数据,其实释放的是线程B的锁。
这个问题可以将key对应的value设置为一个随机值或者线程ID,释放锁时判断value是否等于该随机值即可。
但是改正后依然存在问题。
2.释放锁的操作不是原子操作。
假设线程A已经判断了value等于设置的随机值。但是执行完第⑼代码,由于网络问题,2S后才执行第⑽代码去deleteKey(释放锁)。但是这两秒内,可能锁已经过期释放了。此时如果线程B获取到了锁。当线程A执行到第⑽行代码时就会释放掉B的锁。依然存在误释放其他线程锁的问题。
这里可以通过Lua脚本来保证释放锁的操作是原子操作来解决该问题。
利用Lua脚本实现Redis锁
从Redis2.6.0版本开始引入对Lua脚本的支持,通过内置的Lua解释器,Redis客户端可以使用Lua脚本,直接在服务端原子的执行多个Redis命令。
释放锁的Lua脚本为:
根据参数key去获取value,如果value和传入的参数value值相等。则删除该key对应数据,成功返回1,否则返回0。
if redis.call(‘get’, KEYS[1]) == ARGV[1]
then return redis.call(‘del’, KEYS[1])
else
return 0
end
我们也可以通过Lua脚本来获取锁:
使用setNx命令设置值。如果成功则使用expire命令设置过期时间。成功返回1,否则返回0。
if redis.call(‘setNx’,KEYS[1],ARGV[1]) == 1
then return redis.call(‘expire’,KEYS[1],ARGV[2])
else
return 0
end
代码如下:
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 成功获取锁返回值
*/
private static final Long LOCK_SUCCESS = 1L;
/**
* 成功释放锁返回值
*/
private static final Long UNLOCK_SUCCESS = 1L;
/**
* 释放锁的LUA脚本:如果value的值与参数相等,则删除,否则返回0
*/
public static final String UNLOCK_SCRIPT_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* @param [lockKey, value]
* @return boolean
* @author YiHaoXing
* @description 使用LUA脚本释放锁, 原子操作
* @date 0:47 2019/6/29
**/
public boolean releaseLockByLua(String lockKey, String value) {
RedisScript<Long> redisScript = new DefaultRedisScript<>(UNLOCK_SCRIPT_LUA, Long.class);
return UNLOCK_SUCCESS.equals(redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value));
}
/**
* 获取锁的LUA脚本:用setNx命令设置值,并设置过期时间
*/
public static final String LOCK_SCRIPT_LUA = "if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end";
/**
* @param [lockKey, value, expireTime]
* @return boolean
* @author YiHaoXing
* @description 使用LUA脚本获取锁, 原子操作。过期时间单位为秒
* @date 0:46 2019/6/29
**/
public boolean getLockByLua(String lockKey, String value, int expireTime) {
RedisScript<Long> redisScript = new DefaultRedisScript<>(LOCK_SCRIPT_LUA, Long.class);
return LOCK_SUCCESS.equals(redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value, expireTime));
}
然后做一个简单的测试
@Autowired
private RedisLockUtils redisLockUtils;
@GetMapping("/t1/{key}")
public String test1(@PathVariable String key){
String value = new StringBuilder().append(Thread.currentThread().getId()).append(Math.random()).toString();
boolean lock = false;
try {
lock = redisLockUtils.getLockByLua(key, value, 30);
if(lock){
log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
Thread.sleep(5000);
} else {
log.info("Thread:{}获取锁失败",Thread.currentThread().getId());
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock){
redisLockUtils.releaseLockByLua(key, value);
log.info("Thread:{}释放锁",Thread.currentThread().getId());
}
}
return "t1 over";
}
@GetMapping("/t2/{key}")
public String test2(@PathVariable String key){
String value = new StringBuilder().append(Thread.currentThread().getId()).append(Math.random()).toString();
boolean lock = false;
try {
lock = redisLockUtils.getLockByLua(key, value, 30);
if(lock){
log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
Thread.sleep(5000);
} else {
log.info("Thread:{}获取锁失败",Thread.currentThread().getId());
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock){
redisLockUtils.releaseLockByLua(key, value);
log.info("Thread:{}释放锁",Thread.currentThread().getId());
}
}
return "t2 over";
}
假设处理业务需要的时间为5S,因此上边的代码中会让当前线程睡眠5S。
1.首先访问 http://localhost:8080/t1/T,然后立刻访问http://localhost:8080/t2/T (5S内即可)。结果如下图:
2019-09-09 17:05:26.388 INFO 31952 --- [nio-8080-exec-1] com.demo.controller.RedisLockController : Thread:28获取锁成功
2019-09-09 17:05:26.388 INFO 31952 --- [nio-8080-exec-1] com.demo.controller.RedisLockController : Thread:28执行业务逻辑中...
2019-09-09 17:05:27.846 INFO 31952 --- [nio-8080-exec-2] com.demo.controller.RedisLockController : Thread:29获取锁失败
2019-09-09 17:05:31.392 INFO 31952 --- [nio-8080-exec-1] com.demo.controller.RedisLockController : Thread:28释放锁
可以看到Thread:28获取锁后,Thread:29再去获取锁就失败了。
2.首先访问 http://localhost:8080/t1/T,等待5S后访问http://localhost:8080/t2/T 。结果如下图:
2019-09-09 17:13:57.259 INFO 31952 --- [nio-8080-exec-6] com.demo.controller.RedisLockController : Thread:33获取锁成功
2019-09-09 17:13:57.259 INFO 31952 --- [nio-8080-exec-6] com.demo.controller.RedisLockController : Thread:33执行业务逻辑中...
2019-09-09 17:14:02.261 INFO 31952 --- [nio-8080-exec-6] com.demo.controller.RedisLockController : Thread:33释放锁
2019-09-09 17:14:03.633 INFO 31952 --- [nio-8080-exec-8] com.demo.controller.RedisLockController : Thread:35获取锁成功
2019-09-09 17:14:03.633 INFO 31952 --- [nio-8080-exec-8] com.demo.controller.RedisLockController : Thread:35执行业务逻辑中...
2019-09-09 17:14:08.635 INFO 31952 --- [nio-8080-exec-8] com.demo.controller.RedisLockController : Thread:35释放锁
可以看到Thread:33先获取到锁,执行完业务逻辑后释放锁。此时t2的请求过来后(Thread:35)就可以获取到锁。
利用Redisson实现Redis锁
有些业务场景中,我们希望如果当前线程如果没有获取到锁,可以等待几秒钟,继续获取锁。或者说,我们希望当前线程一直等待,直到获取到锁。比如:抢票时,用户点击“购买”后,如果该线程未抢到锁,直接返回失败,则用户需要不停的点击“购买”去抢票。如果该线程在未获取到锁后,可以在10S内继续尝试获取锁,那么就不需要用户不停点击“购买”去触发了。针对这种需求,用上边的方法不好实现。
Redisson是一个Java写的基于netty的操作Redis的框架。它提供了一套Java中Lock的实现。我们可以直接调用其提供的方法来实现加锁和释放锁。Redisson可以使线程阻塞,这样我们就可以利Redisson来实现上述的需求。
目前Redisson已经提供了基于SpringBoot的starter依赖。下边介绍一下SpringBoot集成Redisson。
引入redisson-spring-boot-starter依赖。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.11.1</version>
</dependency>
单机Redis配置redisson-single.yml
#单机Redis Redisson配置
singleServerConfig:
address: "redis://192.168.154.129:6379"
#redis连接密码
password: foobared123
clientName: null
#选择使用哪个数据库0~15
database: 0
#连接空闲超时 如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉
idleConnectionTimeout: 10000
pingTimeout: 1000
#连接超时
connectTimeout: 10000
#命令等待超时
timeout: 3000
#命令失败重试次数
retryAttempts: 3
#命令重试发送时间间隔
retryInterval: 1500
#重新连接时间间隔
reconnectionTimeout: 3000
failedAttempts: 3
subscriptionsPerConnection: 5
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 32
connectionPoolSize: 64
dnsMonitoringInterval: 5000
#dnsMonitoring: false
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode: "NIO"
集群Redis配置redisson-cluster.yml
#Redis集群 Redisson配置
clusterServersConfig:
idleConnectionTimeout: 10000
pingTimeout: 1000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
reconnectionTimeout: 3000
failedAttempts: 3
password: 123456
subscriptionsPerConnection: 5
clientName: null
loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
slaveSubscriptionConnectionMinimumIdleSize: 1
slaveSubscriptionConnectionPoolSize: 50
slaveConnectionMinimumIdleSize: 32
slaveConnectionPoolSize: 64
masterConnectionMinimumIdleSize: 32
masterConnectionPoolSize: 64
readMode: "SLAVE"
nodeAddresses:
- "redis://192.168.154.129:7001"
- "redis://192.168.154.129:7002"
- "redis://192.168.154.129:7003"
- "redis://192.168.154.129:7004"
- "redis://192.168.154.129:7005"
- "redis://192.168.154.129:7006"
scanInterval: 1000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode: "NIO"
application.yml
spring:
redis:
redisson:
#Redis集群配置
#config: classpath:redisson-cluster.yml
#Redis单机配置
config: classpath:redisson-single.yml
然后注入RedissonClient即可
@Autowired
private RedissonClient redissonClient;
可重入锁
public void getLock(String lockKey,int expireTime, TimeUnit timeUnit){
RLock lock = redissonClient.getLock(lockKey);
log.info("Thread:{}正在获取锁...",Thread.currentThread().getId());
//拿不到锁线程会一直阻塞.直到拿到锁
lock.lock(expireTime,timeUnit);
}
public boolean getReentrantLock(String lockKey, int waitTime, int expireTime, TimeUnit timeUnit) throws InterruptedException {
RLock lock = redissonClient.getLock(lockKey);
log.info("Thread:{}正在获取锁...",Thread.currentThread().getId());
//拿不到锁会等待waitTime,如果过了waitTime依然没有拿到锁,则获取锁失败.
return lock.tryLock(waitTime, expireTime, timeUnit);
}
Redisson实现可重入锁的方法:
1.org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit)
该方法如果获取不到锁,线程会一直阻塞,直到获取锁。
2.org.redisson.RedissonLock#tryLock(long, long, java.util.concurrent.TimeUnit)
该方法如果获取不到锁,会等待一段时间,继续尝试获取锁。超过等待时间仍未获取到锁,则获取锁失败。
简单测试一下
@GetMapping("/t1/{key}")
public String g1(@PathVariable String key) throws InterruptedException {
redisLockUtils.getLock(key, 30, TimeUnit.SECONDS);
log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
Thread.sleep(10000);
//释放锁
redisLockUtils.unlock(key);
log.info("Thread:{}释放锁",Thread.currentThread().getId());
return "t1 over";
}
@GetMapping("/t2/{key}")
public String g2(@PathVariable String key){
redisLockUtils.getLock(key, 30, TimeUnit.SECONDS);
log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
//释放锁
redisLockUtils.unlock(key);
log.info("Thread:{}释放锁",Thread.currentThread().getId());
return "t2 over";
}
@GetMapping("/t3/{key}")
public String t1(@PathVariable String key){
boolean lock = false;
try {
//等待时间3S.缓存过期时间30S
lock = redisLockUtils.getReentrantLock(key, 3, 30, TimeUnit.SECONDS);
if(lock){
//do something.
log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
Thread.sleep(10000);
}else {
log.info("Thread:{}获取锁失败",Thread.currentThread().getId());
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
if(lock){
redisLockUtils.unlock(key);
log.info("Thread:{}释放锁",Thread.currentThread().getId());
}
}
return "t3 over";
}
@GetMapping("/t4/{key}")
public String t2(@PathVariable String key){
boolean lock = false;
try {
//等待时间3S.缓存过期时间30S
lock = redisLockUtils.getReentrantLock(key, 3, 30, TimeUnit.SECONDS);
if(lock){
//do something.
log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
Thread.sleep(10000);
}else {
log.info("Thread:{}获取锁失败",Thread.currentThread().getId());
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
if(lock){
redisLockUtils.unlock(key);
log.info("Thread:{}释放锁",Thread.currentThread().getId());
}
}
return "t4 over";
}
假设处理业务需要的时间为10S,因此上边的代码中会让当前线程睡眠10S。
1.测试lock()方法。首先访问 http://localhost:8080/t1/T,然后立刻访问http://localhost:8080/t2/T 结果如下图:
2019-09-10 01:44:23.324 INFO 34696 --- [nio-8080-exec-2] com.demo.redis.RedisLockUtils : Thread:46正在获取锁...
2019-09-10 01:44:23.326 INFO 34696 --- [nio-8080-exec-2] com.demo.controller.LockController : Thread:46获取锁成功
2019-09-10 01:44:23.326 INFO 34696 --- [nio-8080-exec-2] com.demo.controller.LockController : Thread:46执行业务逻辑中...
2019-09-10 01:44:24.633 INFO 34696 --- [nio-8080-exec-5] com.demo.redis.RedisLockUtils : Thread:49正在获取锁...
2019-09-10 01:44:33.328 INFO 34696 --- [nio-8080-exec-2] com.demo.controller.LockController : Thread:46释放锁
2019-09-10 01:44:33.333 INFO 34696 --- [nio-8080-exec-5] com.demo.controller.LockController : Thread:49获取锁成功
2019-09-10 01:44:33.333 INFO 34696 --- [nio-8080-exec-5] com.demo.controller.LockController : Thread:49执行业务逻辑中...
2019-09-10 01:44:33.335 INFO 34696 --- [nio-8080-exec-5] com.demo.controller.LockController : Thread:49释放锁
Thread:46先获取到锁。24秒时Thread:49尝试获取锁。此时由于Thread:46未释放锁。所以Thread:49会等待直到Thread:46释放锁。最终33秒时Thread:49获取到锁。
2.测试tryLock()方法。首先访问 http://localhost:8080/t3/T,然后立刻访问http://localhost:8080/t4/T 结果如下图:
2019-09-10 01:26:03.936 INFO 9132 --- [nio-8080-exec-1] com.demo.redis.RedisLockUtils : Thread:45正在获取锁...
2019-09-10 01:26:03.939 INFO 9132 --- [nio-8080-exec-1] com.demo.controller.LockController : Thread:45获取锁成功
2019-09-10 01:26:03.939 INFO 9132 --- [nio-8080-exec-1] com.demo.controller.LockController : Thread:45执行业务逻辑中...
2019-09-10 01:26:05.431 INFO 9132 --- [nio-8080-exec-2] com.demo.redis.RedisLockUtils : Thread:46正在获取锁...
2019-09-10 01:26:08.437 INFO 9132 --- [nio-8080-exec-2] com.demo.controller.LockController : Thread:46获取锁失败
2019-09-10 01:26:13.941 INFO 9132 --- [nio-8080-exec-1] com.demo.controller.LockController : Thread:45释放锁
Thread:45先获取到锁,Thread:46在05秒去尝试获取锁,此时未能获取到锁,因此等待3秒,到08秒时最终获取锁失败。
3.测试tryLock()方法。首先访问 http://localhost:8080/t3/T,等待7S后访问http://localhost:8080/t4/T 。结果如下图:
2019-09-10 01:32:56.603 INFO 9132 --- [nio-8080-exec-6] com.demo.redis.RedisLockUtils : Thread:50正在获取锁...
2019-09-10 01:32:56.605 INFO 9132 --- [nio-8080-exec-6] com.demo.controller.LockController : Thread:50获取锁成功
2019-09-10 01:32:56.605 INFO 9132 --- [nio-8080-exec-6] com.demo.controller.LockController : Thread:50执行业务逻辑中...
2019-09-10 01:33:04.770 INFO 9132 --- [nio-8080-exec-8] com.demo.redis.RedisLockUtils : Thread:52正在获取锁...
2019-09-10 01:33:06.607 INFO 9132 --- [nio-8080-exec-6] com.demo.controller.LockController : Thread:50释放锁
2019-09-10 01:33:06.612 INFO 9132 --- [nio-8080-exec-8] com.demo.controller.LockController : Thread:52获取锁成功
2019-09-10 01:33:06.613 INFO 9132 --- [nio-8080-exec-8] com.demo.controller.LockController : Thread:52执行业务逻辑中...
2019-09-10 01:33:16.615 INFO 9132 --- [nio-8080-exec-8] com.demo.controller.LockController : Thread:52释放锁
Thread:50先获取到锁,04秒时Thread:52尝试获取锁。由于此时Thread:50还未释放锁,所以它需要等待。等待2S后,06秒时Thread:50释放了锁。此时Thread:52也成功获取到了锁。
需要留意一下Redisson存入Redis的数据是以hash来存储的,存储的数据是一个对象。我们先来看一下Redisson加锁时往Redis中存储的数据是什么样的。
可以看到,key为T,存储的value为对象。d249c8ab-0421-48fb-a204-af551bd8810d:50是对象的属性,1是该属性对应的值。那么属性d249c8ab-0421-48fb-a204-af551bd8810d:50以及该属性的值1分别是什么意思呢?
其实Redisson设置属性时,是根据"guid + 当前线程的ID" 生成的。因此d249c8ab-0421-48fb-a204-af551bd8810d是guid,而50是当前线程ID。
属性对应的值1表示的是当前线程获取锁的次数为1。这里Redisson实现的锁是可重入锁。因此在同一线程内,是可以多次获取这个锁的。这个值就表示当前线程内获取锁的次数。
我们可以写个方法来验证一下。
@GetMapping("/t7/{key}")
public String t7(@PathVariable String key){
boolean lock = false;
boolean lock2 = false;
try {
//等待时间3S.缓存过期时间30S
lock = redisLockUtils.getReentrantLock(key, 3, 30, TimeUnit.SECONDS);
if(lock){
//do something.
log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
log.info("Thread:{}执行业务逻辑中...",Thread.currentThread().getId());
//再次获取锁
lock2 = redisLockUtils.getReentrantLock(key, 3, 30, TimeUnit.SECONDS);
if(lock2){
log.info("Thread:{}当前线程内再次获取锁",Thread.currentThread().getId());
}
Thread.sleep(10000);
}else {
log.info("Thread:{}获取锁失败",Thread.currentThread().getId());
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock2){
redisLockUtils.unlock(key);
log.info("Thread:{}释放锁",Thread.currentThread().getId());
}
//释放锁
if(lock){
redisLockUtils.unlock(key);
log.info("Thread:{}释放锁",Thread.currentThread().getId());
}
}
return "t7 over";
}
这个方法里我们获取了两次锁。访问:http://localhost:8080/t7/T
加锁后,Redis中存储的数据为:
可以看到属性d249c8ab-0421-48fb-a204-af551bd8810d:54对应的value值为2,因此这个value表示的就是Redisson可重入锁获取锁的次数。
还有一个问题:Redisson是如何保证释放锁的操作是原子性的?查看Redisson加锁和释放锁的源码可以发现,其实底层方法也是使用的Lua脚本来操作Redis的。
加锁方法:org.redisson.RedissonLock#tryLockInnerAsync
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"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; " +
"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; " +
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
释放锁的方法:org.redisson.RedissonLock#unlockInnerAsync
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
利用Spring AOP加锁
实际开发过程中,我们并不希望在业务逻辑代码中加入太多冗余的加锁,释放锁的代码。此时可以用SpringAOP结合注解来分离加锁/释放锁的代码。
引入AOP依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
创建自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
/**
* 锁的过期时间.以秒为单位
*/
int expireTime() default 30;
/**
* 未获取到锁后等待重试时间.以秒为单位
*/
int waitTime() default 3;
/**
* redis的key
* @return
*/
String value() default "";
}
创建切面类:
@Aspect
@Component
@Slf4j
public class RedisLockAspect {
@Autowired
private RedisLockUtils redisLockUtils;
@Pointcut("@annotation(com.demo.annotation.RedisLock)")
public void redisLockPointCut() {
}
@Around("redisLockPointCut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
RedisLock annotation = method.getAnnotation(RedisLock.class);
//锁的key
String key = annotation.value();
//过期时间
int expireTime = annotation.expireTime();
//等待时间
int waitTime = annotation.waitTime();
boolean lock = false;
try {
//获取锁
lock = redisLockUtils.getReentrantLock(key, waitTime, expireTime, TimeUnit.SECONDS);
if (lock) {
log.info("Thread:{}获取锁成功",Thread.currentThread().getId());
return proceedingJoinPoint.proceed();
} else {
log.info("Thread:{}获取锁失败",Thread.currentThread().getId());
}
} catch (Throwable throwable) {
throw throwable;
} finally {
//释放锁
if(lock){
redisLockUtils.unlock(key);
log.info("Thread:{}释放锁",Thread.currentThread().getId());
}
}
return null;
}
}
测试:
public static final String LOCK_KEY = "T";
@GetMapping("/t5")
@RedisLock(LOCK_KEY)
public String test3(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "t5 over";
}
@GetMapping("/t6")
@RedisLock(LOCK_KEY)
public String test4(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "t6 over";
}
首先访问 http://localhost:8080/t5,然后立刻访问http://localhost:8080/t6,结果如下:
2019-09-10 03:03:36.070 INFO 24488 --- [nio-8080-exec-1] com.demo.redis.RedisLockUtils : Thread:45正在获取锁...
2019-09-10 03:03:36.073 INFO 24488 --- [nio-8080-exec-1] com.demo.aspect.RedisLockAspect : Thread:45获取锁成功
2019-09-10 03:03:37.186 INFO 24488 --- [nio-8080-exec-2] com.demo.redis.RedisLockUtils : Thread:46正在获取锁...
2019-09-10 03:03:40.189 INFO 24488 --- [nio-8080-exec-2] com.demo.aspect.RedisLockAspect : Thread:46获取锁失败
2019-09-10 03:03:41.078 INFO 24488 --- [nio-8080-exec-1] com.demo.aspect.RedisLockAspect : Thread:45释放锁
小结
如果项目中用Redis做缓存方案最好还是基于Lettuce(Jedis也行,不过SpringBoot2.x版本默认采用Lettuce),因为Redisson本身对字符串的操作支持很差;如果是作为分布式锁方案,可以采用Redisson。
如果是SpringBoot框架同时采用starter启动依赖的方式集成Lettuce和Redisson的话,会导致LettuceConnectionFactory注入失败,因此如果需要在一个项目里同时集成Lettuce和Redisson操作Redis的话,建议不要用starter启动依赖的方式集成Redisson。可以单独引入Redisson的依赖,然后写一个配置类来集成Redisson。
Demo代码地址:
https://github.com/YiHaoxing/redis-lock-lettuce
https://github.com/YiHaoxing/redis-lock-redisson
https://github.com/YiHaoxing/redis-lock