Redis分布式锁逻辑实现
分布式系统要访问共享资源,为了避免并发访问资源带来错误,我们为共享资源添加一把锁,让各个访问互斥,保证并发访问的安全性。
例如有这么一个场景:当前项目是一个处理贷款审批流程的项目,例如订单A当前在B流程池中,现在用户C对订单A进行引入操作(即将订单A接收至自己名下)。与此同时,用户D也可能对订单A进行同样的操作,这样显然会造成问题。这时候我们需要用到分布式锁来解决这类问题,保证在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行。
伪代码如下:
// 加锁
if(set(lock_apply_A,1) == 1){
// 添加锁超时时间
expire(lock_apply_A,30)
try {
do something ......
} finally {
// 释放锁
del(lock_sale_商品ID)
}
}
优化点1,例如线程P1加锁成功之后挂掉了,那就无法进行后续的添加锁超时时间等操作,造成死锁。所以我们在加锁的同时能设置好超时时间就最好不过了。优化伪代码:
if(set(lock_apply_A,1,30,NX) == 1){
try {
do something ......
} finally {
del(lock_apply_A)
}
}
优化点2,例如线程P1获取锁成功之后,30秒之内依然没有执行完,这时候锁过期会自动释放。然后此时线程P2成功获取到了锁,执行过程中。P1的do something执行完毕,进行删除锁动作,这时候P2还没执行完,P1删除的锁实际上是P2的锁。为了避免这种情况,在删除之前需要验证当前线程删除的是自己的锁。优化伪代码:
String threadId = Thread.currentThread().getId()
if(set(lock_apply_A,threadId ,30,NX) == 1){
try {
do something ......
} finally {
if(threadId .equals(get(key))){
del(lock_apply_A)
}
}
}
spring-boot下的redis分布式锁实现
新建redis分布式锁实现类
package com.example.demo.service.redislocker;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @author liwenhuan
* @date 2020/5/27 10:58
*/
@Slf4j
@Service
public class RedisLockerService {
/**
* redis的key值前缀
*/
private static String KEY_PRE = "SPRINGBOOT:STRING:REDISLOCKER:";
/**
* 默认持有时间10分钟
*/
private static long DEFAULT_KEEP_TIME = 600000L;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
@Qualifier("lockScript")
private RedisScript<Boolean> lockScript;
@Autowired
@Qualifier("unlockScript")
private RedisScript<String> unlockScript;
/**
* 加锁
* 持有时间默认为10分钟
*
* @param key key值
* @return 加锁结果:false 失败,true 成功
*/
public boolean tryLock(String key) {
return this.tryLock(key, DEFAULT_KEEP_TIME);
}
/**
* 加锁
*
* @param key key值
* @param keepTime 保持时间
* @return 加锁结果:false 失败,true 成功
*/
public boolean tryLock(String key, long keepTime) {
log.info("【加锁】开始,key={}, keepTime={}", key, keepTime);
String redisKey = KEY_PRE + key;
String threadId = String.valueOf(Thread.currentThread().getId());
List<String> keys = new ArrayList<>();
keys.add(redisKey);
Boolean result = stringRedisTemplate.execute(lockScript, keys, threadId, String.valueOf(keepTime));
log.info("【加锁】结束,key={}, keepTime={}, result={}", key, keepTime, result);
return result == null ? false : result;
}
/**
* 解锁
*
* @param key key值
*/
public void unlock(String key) {
log.info("【解锁】开始,key={}", key);
String redisKey = KEY_PRE + key;
String threadId = String.valueOf(Thread.currentThread().getId());
List<String> keys = new ArrayList<>();
keys.add(redisKey);
String result = stringRedisTemplate.execute(unlockScript, keys, threadId);
if ("0".equals(result)) {
log.warn("【解锁】失败,锁不存在,key={}", key);
} else if ("2".equals(result)) {
log.warn("【解锁】失败,锁不属于当前线程,key={}", key);
} else {
log.info("【解锁】结束,key={}", key);
}
}
}
Lua脚本编写类
package com.example.demo.service.redislocker;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
/**
* lua脚本
*
* @author liwenhuan
* @date 2020-05-27
*/
@Configuration
public class RedisScriptConfig {
/**
* 加锁脚本:加锁成功则返回1,失败则返回0
*/
@Bean(name = "lockScript")
public RedisScript<Boolean> lockScript() {
StringBuilder lockScript = new StringBuilder();
// 加锁,并设置对应value为当前线程id
lockScript.append("if (redis.call('setnx', KEYS[1], ARGV[1]) == 1) then ");
// 加锁成功后,为锁设置超时时间
lockScript.append(" redis.call('pexpire', KEYS[1], ARGV[2]); ");
lockScript.append(" return 1; ");
lockScript.append("else ");
lockScript.append(" return 0; ");
lockScript.append("end; ");
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(lockScript.toString());
redisScript.setResultType(Boolean.class);
return redisScript;
}
/**
* 解锁脚本:1-解锁成功;2-锁不属于当前线程;0-未找到锁
*/
@Bean(name = "unlockScript")
public RedisScript<String> unlockScript() {
StringBuilder unlockScript = new StringBuilder();
// 检查锁是否存在
unlockScript.append("if (redis.call('exists', KEYS[1]) == 1) then ");
// 存在则获取锁对应的value,value值为当前线程id才进行删除锁操作
unlockScript.append(" if (redis.call('get', KEYS[1]) == ARGV[1]) then ");
unlockScript.append(" redis.call('del', KEYS[1]); ");
unlockScript.append(" return '1'; ");
unlockScript.append(" else ");
unlockScript.append(" return '2'; ");
unlockScript.append(" end; ");
unlockScript.append("else ");
unlockScript.append(" return '0'; ");
unlockScript.append("end; ");
DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(unlockScript.toString());
redisScript.setResultType(String.class);
return redisScript;
}
}
测试
public static void main(String args[]) {
RedisLockerService service = new RedisLockerService();
// 加锁结果默认false
boolean lockSuccess = false;
String key = "abc";
try {
lockSuccess = service.tryLock(key);
if (lockSuccess) {
// 加锁成功则进行相关业务逻辑处理
} else {
// 加锁失败,提示当前操作不允许(结合实际业务)
}
} finally {
// 加锁成功
if (lockSuccess) {
// 才进行解锁
service.unlock(key);
}
}
}
PS:使用Lua脚本的好处
1.减少网络开销。可以将多个请求通过脚本的形式一次性发送,减少网络时延。
2.原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入,无需使用事务。
3.复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本。