Redis 分散式鎖

Fang思楠發表於2024-06-21

最近抽空優化了之前已有的redis分散式鎖,主要用於解決高併發的問題,比如搶紅包,多個人同時操作紅包庫存,當在庫存只剩下1個的時候,一個人的減庫存的操作事務沒提交,另一個人的查庫存操作剛好同步執行,這樣就會出現很尷尬的事情,1個紅包會被2個人搶走,這個時候,我們就要依託鎖,將請求入口鎖住,當然鎖有很多種方式,這邊就記錄一下比較好用的redis分散式鎖。

方式有很多setNX 、set、incr等等,setNX只要通過邏輯防止死鎖就可以了

直接上程式碼:

public boolean keyLock(final String key, final long keepMin) {

boolean obj = false;

try {

obj = (boolean) redisTemplateSerializable.execute(new RedisCallback() {

@Override

public Object doInRedis(RedisConnection connection)

throws DataAccessException {

try{

Long incr = connection.incr(key.getBytes());

if(incr == 1){

connection.setEx(key.getBytes(), keepMin, incr.toString().getBytes());

return true;

}else{

Long ttl = connection.ttl(key.getBytes());

if(ttl == -1){

//設定失敗,重新設定過期時間

connection.setEx(key.getBytes(), keepMin, incr.toString().getBytes());

return true;

}

}

}catch (Exception e) {

logger.error("加鎖異常", e);

connection.del(key.getBytes());

return true;

}

return false;

}

});

}catch (Exception e) {

logger.error(e.getMessage());

}

return obj;

}

註解

package com.tp.soft.common.interceptor;

import java.lang.annotation.Documented;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

import java.util.concurrent.TimeUnit;

/**

* redis鎖註解

*

* @author taop

*/

@Retention(RetentionPolicy.RUNTIME)

@Target({ ElementType.METHOD })

@Documented

public @interface RedisLock {

String lockName() default ""; // 鎖名

int retryTimes() default 0; // 重試次數

long retryWait() default 200; // 重試等待時間,單位 : ms

int keeyMinTime() default 1; //鎖自動失效時間 1秒

}

aop

package com.tp.soft.aop.redis;

import java.lang.reflect.Method;

import java.util.HashMap;

import java.util.Map;

import javax.annotation.Resource;

import org.apache.commons.lang.StringUtils;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.Signature;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Pointcut;

import org.aspectj.lang.reflect.MethodSignature;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.core.LocalVariableTableParameterNameDiscoverer;

import org.springframework.expression.ExpressionParser;

import org.springframework.expression.spel.standard.SpelExpressionParser;

import org.springframework.expression.spel.support.StandardEvaluationContext;

import org.springframework.stereotype.Component;

import cn.hutool.core.lang.Assert;

import com.tp.soft.common.interceptor.Cacheable;

import com.tp.soft.common.interceptor.RedisLock;

import com.tp.soft.redis.RedisCacheSvc;

@Aspect

@Component

public class RedisLockAop {

private static final Logger log = LoggerFactory.getLogger(RedisLockAop.class);

private static final String LOCK_NAME = "lockName";

private static final String RETRY_TIMES = "retryTimes";

private static final String RETRY_WAIT = "retryWait";

private static final String KEEP_MIN_TIME = "keepMinTime";

@Resource

private RedisCacheSvc redisCacheSvc;

@Pointcut("@annotation(com.tp.soft.common.interceptor.RedisLock)")

public void redisLockAspect() {

}

@Around("redisLockAspect()")

public Object lockAroundAction(ProceedingJoinPoint pjp) throws Throwable {

Method method = returnMethod(pjp);

Map annotationArgs = this.getAnnotationArgs(pjp);

String lockPrefix = (String) annotationArgs.get(LOCK_NAME);

Assert.notNull(lockPrefix, "分散式,鎖名不能為空");

int retryTimes = (int) annotationArgs.get(RETRY_TIMES);

long retryWait = (long) annotationArgs.get(RETRY_WAIT);

int keepMinTime = (int) annotationArgs.get(KEEP_MIN_TIME);

String keyName = parseKey(lockPrefix, method, pjp.getArgs());

// 獲取redis鎖,防止死鎖

boolean keyLock = redisCacheSvc.keyLock(keyName, keepMinTime);

if(keyLock){

//執行主程式

return pjp.proceed();

}else{

if(retryTimes <= 0){

log.info(String.format("{%s}已經被鎖, 不重試", keyName));

throw new RuntimeException(String.format("{%s}已經被鎖, 不重試", keyName));

}

int failCount = 1;

while (failCount <= retryTimes) {

// 等待指定時間ms

try {

Thread.sleep(retryWait);

} catch (InterruptedException e) {

e.printStackTrace();

}

if (redisCacheSvc.keyLock(keyName, keepMinTime)) {

// 執行主邏輯

return pjp.proceed();

} else {

log.info(String.format("{%s}已經被鎖, 正在重試[ %s/%s ],重試間隔{%s}毫秒", keyName, failCount, retryTimes, retryWait));

failCount++;

}

}

throw new RuntimeException("系統繁忙, 請稍等再試");

}

}

/**

* 獲取鎖引數

*

* @param proceeding

* @return

*/

private Map getAnnotationArgs(ProceedingJoinPoint proceeding) {

Class target = proceeding.getTarget().getClass();

Method[] methods = target.getMethods();

String methodName = proceeding.getSignature().getName();

for (Method method : methods) {

if (method.getName().equals(methodName)) {

Map result = new HashMap();

RedisLock redisLock = method.getAnnotation(RedisLock.class);

result.put(LOCK_NAME, redisLock.lockName());

result.put(RETRY_TIMES, redisLock.retryTimes());

result.put(RETRY_WAIT, redisLock.retryWait());

result.put(KEEP_MIN_TIME, redisLock.keeyMinTime());

return result;

}

}

return null;

}

private Method returnMethod(ProceedingJoinPoint pjp)

throws NoSuchMethodException {

Signature signature = pjp.getSignature();

Class cls = pjp.getTarget().getClass();

MethodSignature methodSignature = (MethodSignature) signature;

Method targetMethod = methodSignature.getMethod();

Method method = cls.getDeclaredMethod(signature.getName(),

targetMethod.getParameterTypes());

return method;

}

/**

* 獲取快取的key key 定義在註解上,支援SPEL表示式

*

* @param pjp

* @return

*/

private String parseKey(String key, 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上下文中

for (int i = 0; i < paraNameArr.length; i++) {

context.setVariable(paraNameArr[i], args[i]);

}

return parser.parseExpression(key).getValue(context, String.class);

}

}

搭建完成後直接在需要鎖住的介面上註解

@RedisLock(lockName="'lock_'+#tbbId",retryTimes=5)

模擬高併發測試

for (int i = 0; i < 2; i++) {

threadPoolTaskExecutor.execute(new StartTaskThread(redisCacheSvc, i, threadPoolTaskExecutor));

}

14110732-98b0a61772464fad

效果就是這樣了

覺得還不錯的朋友可以加我的交流群:454377428 一起交流學習

相關文章