springboot + Redisson分布式锁生成唯一流水号

Redisson分布式锁

小编最近在排查一个流水编号重复问题的BUG,使用到Redisson分布式锁,今天有时间就特意写下文档记录一下.

问题分析

  1. 首先简单说一个流水号的设计思路:

通过mysql数据库表记录流水号,表中主要有几个关键字段大致如下:
flag varchar(50),
version int DEFAULT NULL,
num int DEFAULT NULL,`
flag表示关键字; version表示版本; num是我们的流水号
根据flag获取表中的最大num,然后进行+1操作,并在表中新增一条数据(哎,其实通过version进行更新就可,为了记录这次问题我就按照这种操作模拟了一下)

  1. 代码分析:

最初是通过synchronized关键字锁住代码块获取表中最大的num编号,然后在进行insert数据库操作;

上面的操作不难发现问题,通过synchronized关键字锁,在单机情况下并发不高还能玩玩,但是如果在集群或者高并发情况下就会出现重复编号的问题.(说到这里有人可能提出疑问,你用mysql记录最大编号,如果分库分表或者多数据源了怎么办???,给大家抛出来一个问题环境评论区留言)

  • Redisson
    针对上面的问题,我首先想到的就是通过分布式锁来解决这种问题,让我直接想到了Redisson框架,封装好的给予redis的分布式锁框架,简单好用;

springboot+redisson实现

  • 首先引入配置
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>


        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
        </dependency>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.17.1</version>
        </dependency>
  • 配置Redisson
package com.jsoft.per.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 1. @Author: swang
 2. @Description:
 3. @Date: 2023/5/3 下午7:13
 */
@Configuration
public class RedisConfig {

    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient(){
        // 创建配置 指定redis地址及节点信息
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}
  1. aop切面配置Redisson

简单说一下为啥采用aop切面加锁: 我的流水号操作在事物方法中使用, spring的事物底层是采用动态代理方式实现的, 按照正常的加锁就会出现一种情况: 当我的redisson锁释放了但是我的事物还没有提交,在并发情况就会导致重复编号问题, 所以加锁采用aop切面方式,并设置优先级高于事物 ----> 事物提交后我在解锁.
代码如下:

自定义注解

package com.jsoft.per.annotation;

import java.lang.annotation.*;

/**
 * @Author: swang
 * @Description:
 * @Date: 2023/5/3 下午10:29
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisLock {

    String lockName() default "num";

}

切面实现

package com.jsoft.per.aspectj;

import com.jsoft.per.annotation.RedisLock;
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.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;

/**
 * @Author: swang
 * @Description:
 * @Date: 2023/5/3 下午10:31
 */
@Aspect
@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class LockAsp {

    @Resource
    private RedissonClient redissonClient;

    @Pointcut("@annotation(com.jsoft.per.annotation.RedisLock)")
    public void pointCut() {

    }

    @Around("pointCut()")
    public Object doLock(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RedisLock lock = method.getAnnotation(RedisLock.class);
        String lockName = lock.lockName();
        RLock rLock = redissonClient.getLock(lockName);
        try {
            rLock.lock();
            joinPoint.proceed();
        } catch (Exception e) {
            e.printStackTrace();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        } finally {
            rLock.unlock();
        }
        return joinPoint.proceed();
    }

}

使用方式

@Override
    @Transactional(rollbackFor = Exception.class)
    public void save() {

        StockPO po = StockPO.builder()
                .name("白菜")
                .num(100)
                .build();
        stockMapper.insert(po);
        String key = "";
        stockService.getNum(key);
    }

    /**
     * @Description 获取流水号
     **/
    @Override
    @RedisLock(lockName = "lock")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void getNum(String key) {
            int maxNum = getMaxNum();
            NumPO po = NumPO.builder()
                    .flag("KC")
                    .version(1)
                    .num(maxNum)
                    .build();
            numMapper.insert(po);
    }

    private int getMaxNum() {
        QueryWrapper<NumPO> queryWrapper = new QueryWrapper();
        queryWrapper.eq("flag", "KC");
        queryWrapper.orderByDesc("num");
        List<NumPO> list = numMapper.selectList(queryWrapper);
        if(!CollectionUtils.isEmpty(list)) {
            NumPO numPO = list.stream().findFirst().get();
            return numPO.getNum() + 1;
        } else {
            return 1;
        }
    }

以上代码是我写的一个简单案例, 都已经通过压测,并发情况下也没有出现重复编号,请大胆验证;
细心的人会发现,我加锁的名称是写死的,那如果都用这个注解加锁的时候,是不是就会出现一个问题,不同的业务加锁时候使用的是同一个名称,就会导致我的锁竞争比较大. 有问题就有解决方案.

  • 配置sePL表达式
    private final SpelExpressionParser parser = new SpelExpressionParser();

    private final LocalVariableTableParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer();

    private String getSPL(ProceedingJoinPoint joinPoint, String spl) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        String[] parameterNames = nameDiscoverer.getParameterNames(method);
        StandardEvaluationContext context = new StandardEvaluationContext();
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < parameterNames.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }
        return Objects.requireNonNull(parser.parseExpression(spl).getValue(context, String.class));
    }

最终注解使用方式如下:

	@Override
    @RedisLock(lockName = "lock", spEL = "#key")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void getNum(String key) {
            int maxNum = getMaxNum();
            NumPO po = NumPO.builder()
                    .flag("KC")
                    .version(1)
                    .num(maxNum)
                    .build();
            numMapper.insert(po);
    }
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值