在企业的项目中,经常会碰到多线程安全的问题。特别是在涉及到金钱方面的,安全问题更是重中之重。如何保证多线程下的安全就成了必须要解决的问题。
在之前负责的某个项目中,有几个地方就被人恶意攻击过。用户申请提现的时候,通过接口快速访问,可以跳过钱包余额的校验达到多次提现。在微信小程序支付订单的时候,小程序支付完之后,瞬时调用多次检查订单状态的接口,也会导致多线程的问题,导致钱包余额增加多次。
最开始是用synchronized锁来解决这一问题的。不过synchronized锁意味的同一时刻该接口只能被一个人访问,在用户量很大的时候,有可能会造成用户等待时间过长,体验不好。所以使用了redis的分布式锁来解决高并发下的线程安全问题。
1.RedisLock.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
/** 锁的资源,redis的key, */
String value() default "default";
/** 持锁时间,单位毫秒*/
long keepMills() default 2000;
/** 当获取失败时候动作*/
LockFailAction action() default LockFailAction.CONTINUE;
public enum LockFailAction{
/** 放弃 */
GIVEUP,
/** 继续 */
CONTINUE;
}
/** 重试的间隔时间,设置GIVEUP忽略此项*/
long sleepMills() default 150;
/** 重试次数*/
int retryTimes() default 30;
}
2.RedisLockAspect.java
@Aspect
@Component
public class RedisLockAspect {
public static Logger logger = LoggerFactory.getLogger(RedisLockAspect.class);
@Pointcut("@annotation(xin.dayukeji.common.annotation.RedisLock)")
public void point() {
}
@Autowired
private RedisLockService orderLockService;
@Around(value = "point()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
// 获取方法名称
String methodName = joinPoint.getSignature().getName();
// 获取所有参数类型
Class<?>[] par = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
// 获取目标类
Class<?> classTarget = joinPoint.getTarget().getClass();
// 获取目标方法
Method methodTarget = classTarget.getMethod(methodName, par);
RedisLock redisLock = methodTarget.getDeclaredAnnotation(RedisLock.class);
if (redisLock != null) {
String key = (String) AnnotationResolver.newInstance().resolver(joinPoint, redisLock.value());
System.out.println(key);
int retryTimes = redisLock.action().equals(RedisLock.LockFailAction.CONTINUE) ? redisLock.retryTimes() : 0;
boolean lock = orderLockService.lock(key, redisLock.keepMills(), retryTimes, redisLock.sleepMills());
if (!lock) {
logger.info("get lock failed : " + key);
return null;//获取锁失败,不执行逻辑
}
try {
logger.debug("get lock success : " + key);
//得到锁,执行方法
return joinPoint.proceed();
} finally {
//释放锁
boolean releaseLock = orderLockService.releaseLock(key);
logger.error("release lock : " + key + (releaseLock ? " success" : " failed"));
}
}
return null;
}
}
3.RedisLockService.java
@Service
public class RedisLockService {
@Autowired
private RedisTemplate<String,Object> template;
@Autowired
private Env env;
private static final long LOCK_EXPIRE = 600;
/**
* 锁操作,对某个key加锁,
* @param key 需要加锁的key
* @param keepTime 保持锁存在的时间(即多少时间后会失效),
* @param retryTimes 线程没有取到锁需要重试去取锁,的重试次数
* @param sleepTime 线程没有取到锁后,距离下次取锁操作的时间间隔。
* @return
*/
public boolean lock(String key, long keepTime, int retryTimes, long sleepTime) {
boolean lock = lock(key, keepTime);
while (!lock && retryTimes-- > 0){//当没有取到锁,并且还有重试次数,继续取锁。取到锁或没有重试次数,跳出循环。
try {
Thread.sleep(sleepTime);
lock = lock(key, keepTime);
} catch (InterruptedException e) {
e.printStackTrace();
return false;
}
}
return lock;
}
public boolean lock(String key){
return lock(key, LOCK_EXPIRE);
}
/**
* 加锁并设置失效时间
* @param key 需要加锁的key
* @param keepTime 保持锁存在的时间(即多少时间后会失效),
* @return
*/
public boolean lock(String key, Long keepTime){
String lock = env.getProject() + ":" + key;
return template.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
//取当前时间戳,存入value
long expire = System.currentTimeMillis();
//取出redis原始操作
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
/* NX: 表示只有当此key值不存在时才能存入成功
* PX: 设置此key值的失效时间,单位为毫秒
* 这样当key不存在时,第一个线程能取到锁,其他线程无法set key,取不到锁,
* 等到第一个线程被释放或者锁过了失效时间,才可以取到锁
*/
String acquire = commands.set(lock, String.valueOf(expire + keepTime), "NX", "PX", keepTime);
return "OK".equals(acquire);
}
});
}
/**
* 当业务逻辑完成后,需要将锁释放。
* @param key
* @return
*/
public boolean releaseLock(String key){
String lock = env.getProject() + ":" + key;
Boolean delete = template.delete(lock);
return delete==null?false:delete;
}
/**
* 用于测试的方法
*/
@RedisLock(value = "#{id}")
public void testLock(String id) {
for (int i = 1; i < 10; i++) {
System.out.println(i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.AnnotationResolver.java
package xin.dayukeji.common.util;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
/**
* 该类的作用可以把方法上的参数绑定到注解的变量中,注解的语法#{变量名}
* 能解析类似#{task}或者#{task.taskName}或者{task.project.projectName}
*
* @author liuxg
* date 2016年4月13日 下午8:42:34
*/
public class AnnotationResolver {
private static AnnotationResolver resolver;
public static AnnotationResolver newInstance() {
if (resolver == null) {
return resolver = new AnnotationResolver();
} else {
return resolver;
}
}
/**
* 解析注解上的值
*
* @param joinPoint 就是joinPoint
* @param str 需要解析的字符串
* @return object
*/
public Object resolver(JoinPoint joinPoint, String str) {
if (str == null) return null;
Object value = null;
if (str.matches(".*#\\{.*\\}.*")) {// 如果name匹配上了#{},则把内容当作变量
String newStr = str.replaceAll(".*#\\{", "").replaceAll("\\}.*", "");
if (newStr.contains(".")) { // 复杂类型
try {
value = complexResolver(joinPoint, newStr);
} catch (Exception e) {
e.printStackTrace();
}
} else {
value = simpleResolver(joinPoint, newStr);
}
String[] split = str.split("#\\{.*\\}");
switch (split.length) {
case 1:
value = split[0] + value;
break;
case 2:
value = split[0] + value + split[1];
break;
default:
}
} else { //非变量
value = str;
}
return value;
}
private Object complexResolver(JoinPoint joinPoint, String str) throws Exception {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] names = methodSignature.getParameterNames();
Object[] args = joinPoint.getArgs();
String[] strs = str.split("\\.");
for (int i = 0; i < names.length; i++) {
if (strs[0].equals(names[i])) {
Object obj = args[i];
Method dmethod = obj.getClass().getDeclaredMethod(getMethodName(strs[1]), null);
Object value = dmethod.invoke(args[i]);
return getValue(value, 1, strs);
}
}
return null;
}
private Object getValue(Object obj, int index, String[] strs) {
try {
if (obj != null && index < strs.length - 1) {
Method method = obj.getClass().getDeclaredMethod(getMethodName(strs[index + 1]), null);
obj = method.invoke(obj);
getValue(obj, index + 1, strs);
}
return obj;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private String getMethodName(String name) {
return "get" + name.replaceFirst(name.substring(0, 1), name.substring(0, 1).toUpperCase());
}
private Object simpleResolver(JoinPoint joinPoint, String str) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] names = methodSignature.getParameterNames();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < names.length; i++) {
if (str.equals(names[i])) {
return args[i];
}
}
return null;
}
}
5.TestController.java
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private RedisLockService redisLockService;
/**
* 获取分布式redis
*/
@GetMapping("/redisLock")
@ResponseBody
@ExcludeInterceptor
public Report testRedisLock() {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
redisLockService.testLock("test Lock");
}
}).start();
}
return ReportFactory.S_0_OK.report();
}
}
启动项目,调用/test/redisLock接口测试该注解,控制台打印
test Lock
test Lock
test Lock
test Lock
test Lock
test Lock
test Lock
test Lock
1
2
3
4
5
6
7
8
9
2019-09-04 19:03:29.864 ERROR 5313 --- [ Thread-46] x.d.common.aspect.RedisLockAspect : release lock : test Lock success
test Lock
1
2
3
4
5
6
7
8
9
2019-09-04 19:03:29.993 ERROR 5313 --- [ Thread-53] x.d.common.aspect.RedisLockAspect : release lock : test Lock success
test Lock
...
...
...
本文详细介绍了在高并发场景下,如何利用Redis实现分布式锁来保障多线程安全,尤其是在涉及金钱交易的敏感场景中。通过具体的代码示例,展示了如何在Spring框架中集成RedisLock注解,以及其在实际项目中的应用。
1094

被折叠的 条评论
为什么被折叠?



