在SpringBoot中使用redis实现分布式锁

本文详细介绍了在高并发场景下,如何利用Redis实现分布式锁来保障多线程安全,尤其是在涉及金钱交易的敏感场景中。通过具体的代码示例,展示了如何在Spring框架中集成RedisLock注解,以及其在实际项目中的应用。

在企业的项目中,经常会碰到多线程安全的问题。特别是在涉及到金钱方面的,安全问题更是重中之重。如何保证多线程下的安全就成了必须要解决的问题。

在之前负责的某个项目中,有几个地方就被人恶意攻击过。用户申请提现的时候,通过接口快速访问,可以跳过钱包余额的校验达到多次提现。在微信小程序支付订单的时候,小程序支付完之后,瞬时调用多次检查订单状态的接口,也会导致多线程的问题,导致钱包余额增加多次。

最开始是用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
...
...
...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值