Redis分布式锁,彻底解决接口幂等性问题。

什么是接口幂等性以及接口不具备幂等性会带来哪些危害,请参考上一篇文章。今天主要来介绍分布式环境下,如何解决接口幂等性的问题,当然单机也没有问题,主要是更适用分布式系统。

本篇围绕接口幂等性。讲解SpringBoot+AOP+Redisson实现分布式锁。

何为Redisson

Redisson - 是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象,Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端,Jedis、Lettuce 的 API 更侧重对 Reids 数据库的 CRUD(增删改查),而 Redisson API 侧重于分布式开发。

Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

Lua脚本是什么?

Lua脚本是redis已经内置的一种轻量小巧语言,其执行是通过redis的eval/evalsha命令来运行,把操作封装成一个Lua脚本,无论如何都是一次执行的原子操作。而Redisson底层命令就是Lua脚本实现。

Maven引入依赖

引入的时候注意版本对应,可去官网查询,其中SpringBoot版本为2.7.x最高使用3.18.0版本。

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.18.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

配置Redisson


//host:ip地址, port:端口, auth:密码
@Bean(destroyMethod = "shutdown")
    RedissonClient redissonClient() throws IOException {
        Config config = new Config();
        config.useSingleServer()
                .setPassword(auth)
                .setAddress("redis://" + host + ":" + port);
        return Redisson.create(config);
        /*Config config = new Config();
        config.useClusterServers()
                .setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
                //可以用"rediss://"来启用SSL连接
                .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
                .addNodeAddress("redis://127.0.0.1:7002");

        RedissonClient redisson = Redisson.create(config);*/
    }

Redisson工具类

实现各种分布式锁

package com.rhjt.RzdzBid.config;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @author: mkj
 */
@Component
public class RedissonUtil {
    @Autowired
    private RedissonClient redissonClient;

    /**
     * 加锁
     * @param lockKey
     * @return
     */
    public RLock lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        return lock;
    }

    /**
     * 带超时的锁
     * @param lockKey
     * @param timeout 超时时间 单位:秒
     */
    public RLock lock(String lockKey, int timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, TimeUnit.SECONDS);
        return lock;
    }

    /**
     * 带超时的锁
     * @param lockKey
     * @param unit 时间单位
     * @param timeout 超时时间
     */
    public RLock lock(String lockKey, TimeUnit unit ,int timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
        return lock;
    }


    /**
     * 尝试获取锁
     * @param lockKey
     * @param waitTime 最多等待时间
     * @param leaseTime 上锁后自动释放锁时间
     * @return
     */
    public  boolean tryLock(String lockKey, long waitTime, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            return false;
        }
    }

    /**
     * 尝试获取锁
     * @param lockKey
     * @param unit 时间单位
     * @param waitTime 最多等待时间
     * @param leaseTime 上锁后自动释放锁时间
     * @return
     */
    public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, unit);
        } catch (InterruptedException e) {
            return false;
        }
    }

    /**
     * 释放锁
     * @param lockKey
     */
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }


    /**
     * 释放锁
     * @param lock
     */
    public void unlock(RLock lock) {
        lock.unlock();
    }
}

实现AOP+分布式锁进行统一接口拦截

1、创建自定义注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface DistributedLock {
    // 自定义业务keys
    String[] keys() default {};

    // 租赁时间 单位:毫秒
    long leaseTime() default 30000;

    // 等待时间 单位:毫秒
    long waitTime() default 3000;
}
2、实现AOP切面

用接口参数和方法名等生成锁的唯一key,访问接口时尝试获取锁,如果获取成功,代表可以访问,如果失败则返回错误,代表锁正在使用。


package com.rhjt.RzdzBid.config;

import com.rhjt.core.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @author: mkj
 */
@Slf4j
@Aspect
@Order(1000)
@Component
public class LockAspect {
    @Autowired
    private RedissonUtil redissonUtil;
    //代表有 DistributedLock 注解的接口都会被扫描
    @Around("@annotation(distributedLock)")
    public Object lockMethod(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {

        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();

        // 获取自定义业务keys
        String[] keys = distributedLock.keys();
        // 租赁时间
        long leaseTime = distributedLock.leaseTime();
        // 等待时间
        long waitTime = distributedLock.waitTime();

        // 创建参数名发现器
        ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

        // 获取方法参数名
        String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);

        // 创建 SpEL 解析器
        ExpressionParser expressionParser = new SpelExpressionParser();

        // 创建 SpEL 上下文
        EvaluationContext evaluationContext = new StandardEvaluationContext();

        // 设置方法参数值到 SpEL 上下文中
        Object[] args = joinPoint.getArgs();
        if (parameterNames != null && args != null && parameterNames.length == args.length) {
            for (int i = 0; i < parameterNames.length; i++) {
                evaluationContext.setVariable(parameterNames[i], args[i]);
            }
        }
        // 构建完整的锁键名
        StringBuilder lockKeyBuilder = new StringBuilder();
        if (keys.length > 0) {
            for (String key : keys) {
                if (StringUtils.hasText(key)) {
                    try {
                        // 解析 SpEL 表达式获取属性值
                        Object value = expressionParser.parseExpression(key).getValue(evaluationContext);
                        lockKeyBuilder.append(value).append(":");
                    } catch (SpelEvaluationException ex) {
                        // 如果解析失败,则使用原始字符串作为属性值
                        LiteralExpression expression = new LiteralExpression(key);
                        lockKeyBuilder.append(expression.getValue()).append(":");
                    }
                }
            }
        }
        // 使用方法名作为最后一部分键名
        lockKeyBuilder.append(methodSignature.getName());
        String fullLockKey = lockKeyBuilder.toString();
        // 尝试获取分布式锁
        boolean success = redissonUtil.tryLock(fullLockKey,waitTime, leaseTime);
        if (success) {
            try {
                // 执行被拦截的方法
                return joinPoint.proceed();
            } finally {
                // 释放锁
                redissonUtil.unlock(fullLockKey);
            }
        } else {
            log.error("Failed to acquire distributed lock");
            // 获取锁超时,抛出异常
            throw new RuntimeException("Failed to acquire distributed lock");
        }
    }

}
3、拟测试方法
package com.rhjt.RzdzBid.newnumone.lockservice;

import com.rhjt.RzdzBid.config.DistributedLock;
import com.rhjt.RzdzBid.newnumone.model.UserTest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @author: mkj
 */
@Controller
@RequestMapping(value="/api/locktest")
public class LockTest {
    int i = 0;

    @RequestMapping(value="/test01")
    @DistributedLock
    public void test01() {
        System.out.println("执行方法1 , 当前线程:" + Thread.currentThread().getName() + "执行的结果是:" + ++i);
        sleep();
    }
    @RequestMapping(value="/test02")
    @DistributedLock(keys = "myKey",leaseTime = 30L)
    public void test02() {
        System.out.println("执行方法2 , 当前线程:" + Thread.currentThread().getName() + "执行的结果是:" + ++i);
        sleep();
    }
    @RequestMapping(value="/test03")
    @DistributedLock(keys = "#UserTest.id")
    public UserTest test03(UserTest UserTest ) {
        System.out.println("执行方法3 , 当前线程:" + Thread.currentThread().getName() + "执行的结果是:" + ++i);
        sleep();
        return UserTest ;
    }
    @RequestMapping(value="/test04")
    @DistributedLock(keys = {"#UserTest.id", "#UserTest.name"}, leaseTime = 5000, waitTime = 5000)
    public UserTest test04(UserTest UserTest ) {
        System.out.println("执行方法4 , 当前线程:" + Thread.currentThread().getName() + "执行的结果是:" + ++i);
        sleep();
        return UserTest ;
    }

    private void sleep() {
        // 模拟业务耗时
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

  • 17
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

missterzy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值