1.背景
項目中最近迫切的需要一套分佈式鎖來解決一些接口的併發問題,在網上搜集了各種資料。並結合自己的項目場景,依賴Redis實現了一套分佈式鎖,使用簡單,支持註解。在這裏分享一下實現過程,希望能對你們有一些幫助。
2.項目結構
核心包:
3.maven依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.2.5.RELEASE</version>
<exclusions>
<exclusion>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</exclusion>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.0</version>
</dependency>
4.Redis配置
無需多說
spring:
redis:
host: xxxxx
password: xxxxx
timeout: 3000
port: 6379
5.Redisson配置
redisson配置項需要單獨配置一份yaml文件,在springboot的yml中配置是無法生效的。
redisson配置有兩種方式:編碼形式和配置式
方式一:純編碼形式無需編寫yaml文件,直接對所需配置項進行配置就好:如下
@Configuration
public class RedissonConfig {
@Bean
@ConditionalOnProperty("spring.redis.host")
public RedissonClient redissonClient(){
Config config = new Config();
config.setTransportMode(TransportMode.EPOLL);
config.useClusterServers()
// use "rediss://" for SSL connection
.addNodeAddress("perredis://127.0.0.1:7181");
return Redisson.create(config);
}
}
配置方式二 配置式:新增redission.yaml配置文件
clusterServersConfig:
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
failedSlaveReconnectionInterval: 3000
failedSlaveCheckInterval: 60000
password: null
subscriptionsPerConnection: 5
clientName: null
loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
slaveConnectionMinimumIdleSize: 24
slaveConnectionPoolSize: 64
masterConnectionMinimumIdleSize: 24
masterConnectionPoolSize: 64
readMode: "SLAVE"
subscriptionMode: "SLAVE"
nodeAddresses:
- "redis://127.0.0.1:7004"
- "redis://127.0.0.1:7001"
- "redis://127.0.0.1:7000"
scanInterval: 1000
pingConnectionInterval: 0
keepAlive: false
tcpNoDelay: false
threads: 16
nettyThreads: 32
codec: !<org.redisson.codec.FstCodec> {}
transportMode: "NIO"
之後編寫配置類,將此配置文件引入即可(注意路徑)。
Config config = Config.fromYAML(new File("redisson.yaml"));
RedissonClient redisson = Redisson.create(config);
配置參數根據不同的redis使用場景(單機、主備、cluster等)需要自行去選擇,官方文檔都做了詳細的講解,具體配置參數請移步官網進行詳細查看:redisso官方文檔
6.編寫分佈式鎖代碼
編寫註解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {
/**
* 分佈式鎖的key值,非空。支持el表達式獲取入參對象參數
*/
String[] keys();
/**
* 分佈式鎖key前綴,默認採用類全限定名:方法名
*/
String prefix() default "";
/**
* key與前綴的分隔符
*/
String separator() default ":";
/**
* 等待獲取鎖的時間
*/
long waitTime() default 8;
/**
* 鎖的租期,超時後自動釋放
*/
long leaseTime() default 3;
/**
* 時間單位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
考慮到我們的業務場景,這裏的keys我是用了數組形式,可以獲取多個入參的參數。支持EL
配置切面及解析邏輯
@Aspect
@Component
@Slf4j
public class DistributedLockAspect {
@Resource
DistributedLocker distributedLocker;
@Pointcut("@annotation(xxx.lock.annotation.DistributedLock)")
public void pointCut(){}
/**
* 環繞增強,嘗試獲取鎖/釋放鎖
*
* @param joinPoint 切面
* @return Object
* @throws Throwable
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
DistributedLock annotation = AnnotationUtils.findAnnotation(targetMethod, DistributedLock.class);
assert annotation != null;
String key=getLockKey(targetMethod,joinPoint,annotation);
boolean lockFlag =false;
Object proceed =null;
try {
lockFlag=distributedLocker.tryLock(key,annotation.timeUnit(),annotation.waitTime(),annotation.leaseTime());
if (lockFlag) {
log.info("success to get distributed lock with key {}",key);
proceed = joinPoint.proceed();
}
} catch (Exception exception) {
log.error("exception occurred while getting distributed lock ",exception);
return null;
}finally {
if (lockFlag){
distributedLocker.unlock(key);
log.info("lock {} has been released",key);
}
}
return proceed;
}
/**
* 獲取攔截到的方法,解析分佈式鎖key值(如果包含el表達式,則從中解析出內容)
*
* @param joinPoint 切點
* @return redisKey
*/
private String getLockKey(Method targetMethod,
ProceedingJoinPoint joinPoint, DistributedLock targetAnnotation) {
Object target = joinPoint.getTarget();
Object[] arguments = joinPoint.getArgs();
StringBuilder stringBuilder=new StringBuilder();
for (int i=0;i<targetAnnotation.keys().length;i++){
String subKey=targetAnnotation.keys()[i];
if (StringUtils.isNotBlank(subKey) && StringUtils.contains(subKey, Constants.Symbol.SHARP)) {
stringBuilder.append(SpelUtil.parse(target, subKey , targetMethod, arguments));
}else {
stringBuilder.append(subKey);
}
}
if (StringUtils.isNotBlank(targetAnnotation.prefix())){
return StringUtil.concat(targetAnnotation.prefix(),targetAnnotation.separator(),stringBuilder);
}else{
return StringUtil.concat(target.getClass().getName(),targetAnnotation.separator(),targetMethod.getName(),targetAnnotation.separator(),stringBuilder);
}
}
}
EL解析工具(此段代碼爲引用工具,出處未知):
public class SpelUtil {
public static String parse(String spel, Method method, Object[] args) {
//獲取被攔截方法參數名列表(使用Spring支持類庫)
LocalVariableTableParameterNameDiscoverer u =
new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
//使用SPEL進行key的解析
ExpressionParser parser = new SpelExpressionParser();
//SPEL上下文
StandardEvaluationContext context = new StandardEvaluationContext();
//把方法參數放入SPEL上下文中
if (paraNameArr != null) {
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
}
return parser.parseExpression(spel).getValue(context, String.class);
}
/**
* 支持 #p0 參數索引的表達式解析
* @param rootObject 根對象,method 所在的對象
* @param spel 表達式
* @param method ,目標方法
* @param args 方法入參
* @return 解析後的字符串
*/
public static String parse(Object rootObject,String spel, Method method, Object[] args) {
//獲取被攔截方法參數名列表(使用Spring支持類庫)
LocalVariableTableParameterNameDiscoverer u =
new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
//使用SPEL進行key的解析
ExpressionParser parser = new SpelExpressionParser();
//SPEL上下文
StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,u);
//把方法參數放入SPEL上下文中
if (paraNameArr != null) {
for (int i = 0; i < paraNameArr.length; i++) {
context.setVariable(paraNameArr[i], args[i]);
}
}
return parser.parseExpression(spel).getValue(context, String.class);
}
}
編寫鎖及實現
接口DistributedLocker
/**
* 分佈式鎖
*
* @author 小菜瓜
* @since 2020/6/8
*/
public interface DistributedLocker {
/**
* 以默認配置獲取鎖,等待鎖默認5s,鎖租期默認3s
*
* @param lockKey lockKey
* @return RLock
*/
RLock lock(String lockKey) ;
/**
* 自定義超時時間的鎖,最多等待timeout秒,
*
* @param lockKey lockKey
* @param waitTime 超時時間,單位爲秒
* @return lock
*/
RLock lock(String lockKey, long waitTime);
/**
* 自定義超時時間和時間單位的鎖,
*
* @param lockKey lockKey
* @param waitTime 超時時間
* @param unit 時間單位
* @return lock
*/
RLock lock(String lockKey, TimeUnit unit, long waitTime);
/**
* 嘗試加鎖。自定義時間單位和鎖等待時間,租期默認3s
*
* @param lockKey lockKey
* @param unit 時間單位
* @param waitTime 超時時間
* @return boolean
*/
boolean tryLock(String lockKey, TimeUnit unit, long waitTime);
/**
* 嘗試加鎖,自定義時間單位和鎖等待時間以及鎖過期時間
*
* @param lockKey key
* @param timeUnit 時間單位
* @param waitTime 等待超時時間
* @param leaseTime 鎖租期,超過leaseTime自動釋放
* @return boolean
*/
boolean tryLock(String lockKey,TimeUnit timeUnit, long waitTime , long leaseTime);
/**
* 公平鎖,多個Redisson客戶端線程同時請求加鎖時,優先分配給先發出請求的線程。
* 嘗試加鎖,最多等待waitTime後強制獲取鎖,上鎖以後leaseTime自動解鎖
*
* @param lockKey 鎖key
* @param unit 鎖時間單位
* @param waitTime 等到最大時間,強制獲取鎖
* @param leaseTime 鎖自動時間,
* @return 如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false
*/
boolean fairLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime);
/**
* 獲取公平鎖,單位爲秒
*
* @param lockKey lockKey
* @param waitTime 等待時間
* @param leaseTime 租期
* @return boolean
*/
boolean fairLock(String lockKey, long waitTime,long leaseTime);
/**
* 解鎖
*
* @param lockKey lockKey
*/
void unlock(String lockKey);
/**
* 解鎖RLock
*
* @param lock
*/
void unlock(RLock lock);
}
實現類DistributedLockImpl :在這裏我們依賴RedissonClient 進行鎖的實現
@Slf4j
@Component
public class DistributedLockImpl implements DistributedLocker{
/**
* 默認時間單位:秒
*/
public static final TimeUnit DEFAULT_TIME_UNIT= TimeUnit.SECONDS;
/**
* 默認鎖等待超時時間
*/
public static final int DEFAULT_TIMEOUT=8;
/**
* 默認鎖過期時間
*/
public static final int DEFAULT_LEASE_TIME=3;
@Resource
private RedissonClient redissonClient;
@Override
public RLock lock(String lockKey) {
RLock lock =redissonClient.getLock(lockKey);
try {
lock.tryLock(DEFAULT_TIMEOUT,DEFAULT_LEASE_TIME,DEFAULT_TIME_UNIT);
} catch (InterruptedException e) {
log.error("get lock with key {} failed,cause ",lockKey,e);
return null;
}
return lock;
}
@Override
public RLock lock(String lockKey, long timeout) {
return lock(lockKey,DEFAULT_TIME_UNIT,timeout);
}
@Override
public RLock lock(String lockKey, TimeUnit unit, long timeout) {
RLock lock =redissonClient.getLock(lockKey);
try {
lock.tryLock(timeout,DEFAULT_LEASE_TIME,unit);
} catch (InterruptedException e) {
log.error("get lock with key {} failed. cause",lockKey,e);
return null;
}
return lock;
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, long timeout) {
return tryLock(lockKey,unit,timeout,DEFAULT_LEASE_TIME);
}
@Override
public boolean tryLock(String lockKey, TimeUnit timeUnit, long waitTime, long leaseTime) {
RLock lock=redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime,leaseTime,timeUnit);
} catch (InterruptedException e) {
log.error("get lock with key {} failed. cause",lockKey,e);
return false;
}
}
@Override
public boolean fairLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {
RLock lock=redissonClient.getFairLock(lockKey);
try {
return lock.tryLock(waitTime,leaseTime,unit);
} catch (InterruptedException e) {
log.error("get lock with key {} failed. cause",lockKey,e);
return false;
}
}
@Override
public boolean fairLock(String lockKey, long waitTime, long leaseTime) {
return fairLock(lockKey,DEFAULT_TIME_UNIT,waitTime,leaseTime);
}
@Override
public void unlock(String lockKey) {
RLock lock=redissonClient.getLock(lockKey);
lock.unlock();
}
@Override
public void unlock(RLock lock) {
lock.unlock();
}
}
完畢!
以上這一套分佈式鎖可以拿來就用,具體的 waitTime() default 8; leaseTime() default 3;等參數還需要各位根據自己的業務場景去進行調整。
如果有更好的改進方案和不對的地方還請各位大佬糾正和指教,感興趣也可以可以一起討論一下。