基于Redis生成递增序号

一. Spring Boot + Redisson 生成运单号

  <!--整合redission框架start-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.12.5</version>
        </dependency>
        <!--整合redission框架end-->
public abstract class StrategyHandler<T> {
    public abstract Response<T> handler(T t);
    public abstract T handler();
}
import java.util.HashMap;
import java.util.Map;

public class TransOrderSequenceStrategy extends StrategyHandler<Map<String,String>> {

    private static final String SEQUENCE_REDIS_KEY = "trans:order:sequence:%s:%s";

    @Override
    public Response<Map<String, String>> handler(Map<String, String> map) {
        return null;
    }

    @Override
    public Map<String, String> handler() {
        Map<String,String> redisValueMap = new HashMap<>();
        redisValueMap.put("nameSpace",SEQUENCE_REDIS_KEY);
        redisValueMap.put("prefix","D");
        return redisValueMap;
    }
}
import org.redisson.api.RAtomicLong;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author wanghong
 * @desc: 发号器
 * @date 2021/07/22:47
 **/
@Service
public class SequenceManagerBusiness {

    @Autowired
    private RedissonClient redissonClient;
    private static Map<String, StrategyHandler<Map<String,String>>> REDIES_PREFIX_MAP;
    private static String SEQUENCE_REDIS_KEY;
    static {
        REDIES_PREFIX_MAP = new HashMap<>();
        REDIES_PREFIX_MAP.put("order",new TransOrderSequenceStrategy());
    }

    public String generateSequence(String key){
        StrategyHandler<Map<String,String>> taskStrategy = REDIES_PREFIX_MAP.get(key);
        Map<String,String> sequenceMap = taskStrategy.handler();
        String prefix = sequenceMap.get("prefix");
        SEQUENCE_REDIS_KEY = sequenceMap.get("nameSpace");
        return generateSequence0(prefix);
    }

    private String generateSequence0(String prefix) {
        StringBuilder sb = new StringBuilder();
        sb.append(prefix);
        SimpleDateFormat sdf = new SimpleDateFormat("yyMMddHHmm");
        String day = sdf.format(new Date());
        sb.append(day);
        Long sequence = getSequence(prefix, day);
        String sequenceStr = sequence.toString();
        int count;
        if (sequenceStr.length() < 5) {
            count = 5 - sequenceStr.length();
        } else {
            return sb.append(sequence).toString();
        }
        for (int i = 0; i < count; i++) {
            sb.append("0");
        }
        return sb.append(sequence).toString();
    }

    private Long getSequence(String prefix, String day) {
        long sequence;
        String key = String.format(SEQUENCE_REDIS_KEY, prefix, day);
        RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
        if (!atomicLong.isExists()) {
            sequence = atomicLong.incrementAndGet();
            atomicLong.expire(1L, TimeUnit.MINUTES);
        } else {
            sequence = atomicLong.incrementAndGet();
        }
        return sequence;
    }
}
package com.hong;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.hong.redis.SequenceManagerBusiness;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.*;

/**
 * @author wanghong
 * @desc:
 * @date 2021/07/22:50
 **/
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTest {

    private static final int CONCURRENCY_LEVEL = 100;
    private static final CountDownLatch cdl = new CountDownLatch(CONCURRENCY_LEVEL);
    private static ThreadFactory threadFactory = new ThreadFactoryBuilder()
            .setNameFormat("RedisTest-pool-%d").build();


    @Autowired
    private SequenceManagerBusiness sequenceManagerBusiness;

    private static final ExecutorService executorService = new ThreadPoolExecutor(
            Runtime.getRuntime().availableProcessors() + 1, 10, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());

    /**
     * 测试递增发号器
     */
    @Test
    public void testSequence0() throws Exception{
        String key = "order";
        generateSequence0(key);
    }

    private void generateSequence0(String key) throws Exception{
        long start = System.currentTimeMillis();
        for (int i = 0; i < CONCURRENCY_LEVEL; i++) {
            // 使用 线程池+CountDowmLatch 模拟多线程并发请求
            executorService.submit(() -> {
                try {
                    String seq = sequenceManagerBusiness.generateSequence(key);
                    System.out.println(Thread.currentThread().getName() + "generate seq=" + seq);
                } catch (Exception e) {
                    System.out.println(Thread.currentThread().getName() + "执行异常:" + e.getMessage());
                } finally {
                    // 线程启动后,倒计数器-1,表示有一个线程就绪了
                    cdl.countDown();
                }
            });
        }

        // 主线程一直等待 所有获取序列的线程执行完毕
        cdl.await();
        System.out.println("发号结束,耗时:" + (System.currentTimeMillis()-start)/100 + "s");
        executorService.shutdown();
        System.out.println("=========================================================");
    }
}

// 打印结果:
RedisTest-pool-5generate seq=D210731090300003
RedisTest-pool-8generate seq=D210731090300099
。。。
RedisTest-pool-1generate seq=D210731090300100
发号结束,耗时:1s
=========================================================

二. 现在运单号规则生成调整

    我们做的是一个TMS SAAS平台,引入了租户的概念,但数据库表层面,数据未作物理隔离,只做了逻辑隔离,即表加上了一个tenantId,运单表租户的数据都是在一张表上的,当每次查询时,会从请求的上下文中获取tenantId,作为必须的查询条件,防止数据越权。现在产品需求:运单号生成规则调整:以租户+当天日期为维度,生成的运单号以 YD 为前缀,然后拼接上 yyMMdd,然后最少三位开始的递增序号,示例:YD210731001,超过3位自动向上加1。

public abstract class SequenceStrategy<T> {
    public abstract T handle();
    public abstract String getMaxSeqFromDB(Long tenantId,String prefix);
}
public class TmsTransOrderSequenceStrategy extends SequenceStrategy<Map<String, String>> {

    private static final String SEQUENCE_REDIS_KEY = "tms:saas:trans:order:sequence:%s:%s:%s";

    private static final String PREFIX = "YD";

    public static final String BIZ_FLAG = "transOrder";

    @Override
    public Map<String, String> handle() {
        Map<String, String> redisValueMap = new HashMap<>();
        redisValueMap.put("nameSpace", SEQUENCE_REDIS_KEY);
        redisValueMap.put("prefix", PREFIX);
        return redisValueMap;
    }

    @Override
    public String getMaxSeqFromDB(Long tenantId, String prefix) {
        return SpringUtils.getBean(TmsTransOrderMapper.class).selectMaxSeqFromDB(tenantId, prefix);
    }
}
import org.redisson.api.RAtomicLong;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @description: 递增发号器
 * @author: 王宏
 * @email hong.wang8@amh-group.com
 * @date: 2021/6/25 14:17
 * @version: 1.0
 */
@Component
public class SequenceGenerateBusiness {

    // SimpleDateFormat非线程安全,使用 java8 的 DateTimeFormatter 替代
    private static DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyMMdd");

    @Autowired
    private RedissonClient redissonClient;

    private static Map<String, SequenceStrategy<Map<String, String>>> REDIES_PREFIX_MAP;

    private static String SEQUENCE_REDIS_KEY;

    static {
        REDIES_PREFIX_MAP = new HashMap<>();
        REDIES_PREFIX_MAP.put(TmsTransOrderSequenceStrategy.BIZ_FLAG, new TmsTransOrderSequenceStrategy());
    }

    /**
     * 多租户序列号生成,不同租户并发生成隔离
     */
    public String generateSequence(Long tenantId, String key) {
        if (Objects.isNull(tenantId)) {
            throw new RuntimeException("SequenceGenerateBusiness.generateSequence 请求参数tenantId不能为空");
        }

        SequenceStrategy<Map<String, String>> taskStrategy = REDIES_PREFIX_MAP.get(key);
        Map<String, String> sequenceMap = taskStrategy.handle();
        String prefix = sequenceMap.get("prefix");
        SEQUENCE_REDIS_KEY = sequenceMap.get("nameSpace");

        StringBuilder sb = new StringBuilder(prefix);
        LocalDate now = LocalDate.now();
        String day = now.format(dtf);
        sb.append(day);
        Long sequence = getSequence(taskStrategy,tenantId, prefix, day);
        String sequenceStr = sequence.toString();
        int len = sequenceStr.length();
        if (len >= 3) {
            return sb.append(sequenceStr).toString();
        }

        int diffBitCount = 3 - len;
        while (diffBitCount > 0) {
            sb.append("0");
            diffBitCount--;
        }

        return sb.append(sequenceStr).toString();
    }

    // 运单号规则调整: YD+2位年+2位月+2位日+3位流水号;   生成运单号的效果: YD210624001  ; 如果超过流水号超过3位,自动加1;例如 YD210624999,自动加1后 YD2106241000;
    private Long getSequence(SequenceStrategy<Map<String, String>> taskStrategy,Long tenantId, String prefix, String day) {
        Long sequence;
        String key = String.format(SEQUENCE_REDIS_KEY, tenantId, prefix, day);
        RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
        if (!atomicLong.isExists()) {
            /**
             * 增加 redis key被异常删除的情况判断:
             * 如果该key被意外删掉了,则会再次进入这里,生成的单号又是从 1开始,如果当天同一租户有创建运单,就会造成运单号重复
             * 因为之前设置的单号递增时间格式为  yyMMddHHmm SequenceManagerBusiness.java,时间粒度到分,设置的key有效期为1分钟,
             * 即使key被意外删除,也只是影响 key 被删除时的那1分钟内可能有运单号重复;
             * 现在的单号时间格式为 yyMMdd,生成的粒度一下子放大到了天,就会造成 key被意外删除的当天都有部分运单号重复;
             *
             * 比如:在 2021-07-02 00:00:00 ~ 12:00:00 租户 10001L 已经产生了单号 YD210702001,YD210702002,
             * 然后在 12:00:01 时 key被意外删除,这时,该租户再来创建运单,就会产生重复的 单号:YD210702001,YD210702002,
             * 直到 YD210702003之后才没有问题;
             */
            String maxSeqFromDB = taskStrategy.getMaxSeqFromDB(tenantId, prefix);
            if (StringUtils.isNotEmpty(maxSeqFromDB)){ // 数据库中已经产生了运单号,但未知原因,又重新从头开始,需要重置序号
                String dbSeq = maxSeqFromDB.substring(prefix.length() + day.length());
                if (dbSeq.startsWith("0")){
                    int index = 0;
                    char[] chars = dbSeq.toCharArray();
                    for (char c:chars){
                        if ('0' == c){
                            index++;
                        }else {
                            break;
                        }
                    }
                    dbSeq = dbSeq.substring(index);
                }

                atomicLong.set(Long.valueOf(dbSeq));
            }

            sequence = atomicLong.incrementAndGet();
            // 设置有效期为 当前时间 到 当天零点的 剩余时间
            LocalTime midnight = LocalTime.MIDNIGHT;
            LocalDateTime todayMidnight = LocalDateTime.of(LocalDate.now(), midnight);
            LocalDateTime tomorrowMidnight = todayMidnight.plusDays(1);
            long seconds = TimeUnit.NANOSECONDS.toSeconds(Duration.between(LocalDateTime.now(), tomorrowMidnight).toNanos());
            atomicLong.expire(seconds, TimeUnit.SECONDS);
        } else {
            sequence = atomicLong.incrementAndGet();
        }
        return sequence;
    }
}
<select id="selectMaxSeqFromDB" resultType="java.lang.String">
    SELECT max(trans_order_no)
    FROM tms_trans_order
    WHERE tenant_id=#{tenantId}
    AND trans_order_no like concat(#{prefix},'%')
    AND TO_DAYS(create_time)=TO_DAYS(NOW())
    for update
</select>

三. 并发下是否会出现重复单号?

批量导入运单,同一租户下运单号偶发出现重复。
线上查询到的日志如下:

-- 同一个机器上的日志
trace 2021-08-18 15:39:08.755 INFO [pool-36-thread-2] com.saas.tms.trans.server.business.SequenceGenerateBusiness SequenceGenerateBusiness.getSequence init base db,tenantId=100000000001577327,prefix=YD,day=210818,maxSeqFromDB=YD210818005
trace 2021-08-18 15:39:08.638 INFO [pool-36-thread-1] com.saas.tms.trans.server.business.SequenceGenerateBusiness SequenceGenerateBusiness.getSequence init base db,tenantId=100000000001577327,prefix=YD,day=210818,maxSeqFromDB=YD210818003

trace 2021-08-18 15:39:08.756 INFO [pool-36-thread-2] com.saas.tms.trans.server.business.SequenceGenerateBusiness SequenceGenerateBusiness.getSequence init finish,tenantId=100000000001577327,prefix=YD,day=210818
trace 2021-08-18 15:39:08.638 INFO [pool-36-thread-1] com.saas.tms.trans.server.business.SequenceGenerateBusiness SequenceGenerateBusiness.getSequence init finish,tenantId=100000000001577327,prefix=YD,day=210818
trace 2021-08-18 15:39:08.523 INFO [pool-36-thread-3] com.saas.tms.trans.server.business.SequenceGenerateBusiness SequenceGenerateBusiness.getSequence init finish,tenantId=100000000001577327,prefix=YD,day=210818
trace 2021-08-18 15:39:08.363 INFO [pool-36-thread-4] com.saas.tms.trans.server.business.SequenceGenerateBusiness SequenceGenerateBusiness.getSequence init finish,tenantId=100000000001577327,prefix=YD,day=210818
select d.tenant_id,d.trans_order_id,d.trans_order_no,d.create_time,d.import_id  from tms_trans_order d 
where d.tenant_id=100000000001577327 and  DATE_FORMAT(d.create_time,'%Y-%m-%d')='2021-08-18'  order by d.trans_order_no asc;

在这里插入图片描述
出现问题的方法就是上面的 SequenceGenerateBusiness.getSequence()。
    该方法依赖redis ,考虑到redis本身的可用性,有可能重启或宕机,导致初始化错误而发生运单号重复,所以加了从数据库查询该租户当前最大的运单号,然后手动set到redis中,保证初始化数据的正确性。
    但批量导入运单时(批量导入运单为 @Async(“AsyncTaskThreadExecutor”) 异步导入),开启了多线程导入,这样在初始化中就存在并发查库,比如批量导入的运单有8笔,在2021-08-18 15:39:08这1秒内有4个线程同时进入 if (!atomicLong.isExists()) 判断为true,进入初始化代码中,然后内部执行时序:

1.2021-08-18 15:39:08.363 时间点,pool-36-thread-4线程查询 String maxSeqFromDB = taskStrategy.getMaxSeqFromDB(tenantId, prefix);结果为空,sequence = atomicLong.incrementAndGet(); = YD210818001 ,第一条数据入库;
2. 同上,在2021-08-18 15:39:08.523时间点,第二条数据入库;
3.2021-08-18 15:39:08.638时间点,pool-36-thread-1线程走到 String maxSeqFromDB = taskStrategy.getMaxSeqFromDB(tenantId, prefix);查询到结果 maxSeqFromDB = YD210818003,说明在2021-08-18 15:39:08.523 ~ 2021-08-18 15:39:08.638 时间段内有线程走到了 if (!atomicLong.isExists()) {},因为之前已初始化过,所以iffalse,走到 else{},生成sequence=YD210818003,然后入库第三条数据;
4. 接着pool-36-thread-1在 atomicLong.set(Long.valueOf(dbSeq)); 即 atomicLong.set(3);此时此刻,正好有另外一个线程执行到了 else{。。。},生成运单号 YD210818004;紧接着pool-36-thread-1 执行到sequence = atomicLong.incrementAndGet();也生成运单号 YD210818004,出现第一次重复单号;
5.2021-08-18 15:39:08.638~2021-08-18 15:39:08.755时间段内,一条线程走else{...}生成运单号YD210818005,然后入库;
6.2021-08-18 15:39:08.755时间点,pool-36-thread-2还在if (!atomicLong.isExists()) {}分支里,并且查询到了maxSeqFromDB=YD210818005;然后接下来的情况就和 3一样,pool-36-thread-2 走到了atomicLong.set(Long.valueOf(dbSeq));即atomicLong.set(5),同一时间点,另一个线程走到了 else{。。。},sequence = atomicLong.incrementAndGet();=YD210818006;紧接着pool-36-thread-2走到if (!atomicLong.isExists()) { 。。。sequence = atomicLong.incrementAndGet(); } = YD210818006,出现了第二次重复;

优化方案:
    通过上面的分析,主要原因就是 atomicLong.set(Long.valueOf(dbSeq)); 和 sequence = atomicLong.incrementAndGet(); 无法保证整体操作的原子性,在两个操作之间,并发操作可能会造成生成的序列号重复。为了避免此问题,增加分布式锁控制序号的初始化逻辑:

// 运单号规则调整: YD+2位年+2位月+2位日+3位流水号;   生成运单号的效果: YD210624001  ; 如果超过流水号超过3位,自动加1;例如 YD210624999,自动加1后 YD2106241000;
private Long getSequence(SequenceStrategy<Map<String, String>> taskStrategy, Long tenantId, String prefix, String day) {
    Long sequence = null;
    String key = String.format(SEQUENCE_REDIS_KEY, tenantId, prefix, day);
    RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
    if (!atomicLong.isExists()) {
        /**
         * 当并发多个线程同时判断key不存在,需要排队,保证只有一个线程进行初始化
         */
        RLock lock = redissonClient.getLock("saas_tms_trans:sequence_generate:" + key);
        try {
            /**
             * 当第一次初始化时,并发多个线程同时进到这里,租户维度上锁
             *  tryLock(long waitTime, long leaseTime, TimeUnit unit)
             * 尝试加锁,最多等待1秒,上锁以后3秒自动解锁
             */
            if (lock.tryLock(1, 3, TimeUnit.SECONDS)) {
                // 类似单例模式双检锁思想
                if (!atomicLong.isExists()) {
                    String maxSeqFromDB = taskStrategy.getMaxSeqFromDB(tenantId, prefix);
                    if (StringUtil.isNotEmpty(maxSeqFromDB)) {
                        String dbSeq = maxSeqFromDB.substring(prefix.length() + day.length());
                        if (dbSeq.startsWith("0")) {
                            int index = 0;
                            char[] chars = dbSeq.toCharArray();
                            for (char c : chars) {
                                if ('0' == c) {
                                    index++;
                                } else {
                                    break;
                                }
                            }
                            dbSeq = dbSeq.substring(index);
                        }

                        atomicLong.set(Long.valueOf(dbSeq));
                        log.info("SequenceGenerateBusiness.getSequence init base db,tenantId={},prefix={},day={},maxSeqFromDB={}", tenantId, prefix, day, maxSeqFromDB);
                    }
                    sequence = atomicLong.incrementAndGet();
                    // 设置有效期为 当前时间 到 当天零点的 剩余时间
                    LocalTime midnight = LocalTime.MIDNIGHT;
                    LocalDateTime todayMidnight = LocalDateTime.of(LocalDate.now(), midnight);
                    LocalDateTime tomorrowMidnight = todayMidnight.plusDays(1);
                    long seconds = TimeUnit.NANOSECONDS.toSeconds(Duration.between(LocalDateTime.now(), tomorrowMidnight).toNanos());
                    atomicLong.expire(seconds, TimeUnit.SECONDS);
                    log.info("SequenceGenerateBusiness.getSequence init finish,tenantId={},prefix={},day={}", tenantId, prefix, day);
                } else {
                    sequence = atomicLong.incrementAndGet();
                    log.info("SequenceGenerateBusiness.getSequence current thread come into init method but found other thread has already init,tenantId={},prefix={},day={},sequence={}", tenantId, prefix, day, sequence);
                    return sequence;
                }
            }
        } catch (Exception e) {
            log.error("SequenceGenerateBusiness.getSequence init base db error={},tenantId={},prefix={},day={},sequence={}", e, tenantId, prefix, day, sequence);
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    } else {
        sequence = atomicLong.incrementAndGet();
        log.info("SequenceGenerateBusiness.getSequence has already init,tenantId={},prefix={},day={},sequence={}", tenantId, prefix, day, sequence);
    }
    return sequence;
}
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
通过Redis生成唯一序号的逻辑需要考虑以下几个方面,以确保唯一性: 1. 使用Redis的原子操作:Redis支持原子操作,这意味着多个客户端同时对Redis进行操作时,Redis会保证这些操作同时执行。这可以避免多线程环境下的并发问题。 2. 使用Redis的自增操作:Redis提供了自增操作INCR和INCRBY,它们可以原子地对一个key进行自增操作,并返回增量后的值。可以利用这个特性生成唯一序号。 3. 利用Redis的分布式锁:在生成唯一序号时,可能会有多个客户端同时生成序号的需求,这时可以利用Redis的分布式锁机制,确保只有一个客户端能够生成序号。 4. 利用Redis的过期时间:为了避免序号一直累加下去,可以在生成序号时为其设置一个过期时间。当序号过期后,下一个生成序号的客户端将重新开始计数。 综合以上几点,一个简单的通过Redis生成唯一序号的逻辑可以如下: 1. 获取Redis的分布式锁,确保只有一个客户端能够生成序号。 2. 通过INCR命令对一个指定的key进行自增操作,得到增量后的值,即为唯一序号。 3. 为该序号设置一个适当的过期时间,以避免序号累加过大。 4. 释放Redis的分布式锁,让其他客户端能够生成序号。 需要注意的是,虽然通过Redis生成唯一序号可以保证在分布式环境下的唯一性,但在单机环境下,如果多个应用进程同时访问Redis生成序号,也需要考虑分布式锁的机制来确保唯一性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值