分布式下获取单号

获取单号的几种方式:

  1. UUID 乱序,且很长,不利于数据库做索引查询 和 空间浪费。
  2. 数据库自增序列,每次都要访问数据库,IO开销大,高并发时几乎不可用。
  3. Redis 自增,优点是速度快,缺点是持久化不可靠,有可能造成重复单号。
  4. 结合 数据库 和 redis 可以获得持久化和性能的双重优势,缺点是依赖服务器时间,如果服务器时间回调,会出现重复单号,另外依赖了额外的中间件。这是本文想要具体介绍的一种方式。

示例环境:

中间件: MySQL + Redis Java 工具(必要): 分布式锁(示例中是使用了封装后的Redission,实际可自己实现分布式锁或使用Redission)

思路流程图:


代码:

  • 数据库表结构
CREATE TABLE `my_sequence` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `seq_name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '序列名字',
  `current_val` bigint(20) unsigned NOT NULL COMMENT '当前值',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='序列表';
  • JAVA实体
/**
 * 序列表
 *
 */
@Data
@TableName("my_sequence")
public class MySequence {
    private static final long serialVersionUID = 1L;

    /**
     * 自增主键
     */
    @TableId
    private Long id;
    /**
     * 序列名字
     */
    private String seqName;
    /**
     * 当前值
     */
    private Long currentVal;
}
  • service类 (spring + RedisTemplate + Redission + mybatis(本例子用了mybatisPlus))
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
/** 这里省略了一些内部包,不影响代码逻辑,根据自己的业务替换 **/
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Optional;

@Service
public class SequenceServiceImpl extends ServiceImpl<MySequenceMapper, MySequence> implements SequenceService {

    private static final String PLATFORM_NO_LOCK_KEY = "PLATFORM_NO_LOCK_";

    private static final String PLATFORM_NO_STACK_KEY = "PLATFORM_NO_STACK_KEY";

    private static final Long SEQ_VAL_STEP_SIZE = 500L;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private DistributedLock distributedLock;

    /**
     *  从redis获取已经生成好的序号
     * 不加入业务的事务,避免随业务回滚,序列号只增加,不回滚
     * @param c CommDeclType枚举类
     * @return
     */
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.NOT_SUPPORTED)
    @Override
    public String getPlatformNoRedis(CommDeclType c) {
        Long nextSeqVal = (Long) redisTemplate.opsForList().leftPop(PLATFORM_NO_STACK_KEY+ seqType);
        if (null == nextSeqVal) {
            nextSeqVal = pushAndGetSeqVal(c);
        }
        return formatStrNo(c.getRemarks(), String.valueOf(nextSeqVal), null);
    }

    /**
     * 如果 redis 序号已经用完,则查库追加 指定步长数量的序号
     *
     * @param seqType 业务类型标志 - 数据库存了多个业务类型的序列号记录
     * @return {@link Long}
     */
    private Long pushAndGetSeqVal(String seqType) {
        Long nextSeqVal;
        // 第一个本地同步锁,防止分布式锁过多的争抢
        synchronized (SequenceUtils.class) {
            nextSeqVal = (Long) redisTemplate.opsForList().leftPop(PLATFORM_NO_STACK_KEY+ seqType);
            if (null == nextSeqVal) {
                nextSeqVal = distributedLock.locked(PLATFORM_NO_LOCK_KEY + seqType, () -> {
                    Long nextPlatformNoTemp = (Long) redisTemplate.opsForList().leftPop(PLATFORM_NO_STACK_KEY+ seqType);
                    if (null == nextPlatformNoTemp) {
                        // 取出指定的序列号持久化数据
                        // 这里用了 mybatisPlus 的模板代码,可换成自己的ORM取数据方式
                        MySequence sequence = sequenceService
                        .getOne(new LambdaQueryWrapper<MySequence>().eq(MySequence::getSeqName, seqType));
                        // 如果序列号还没有初始化,则进行初始化
                        MySequence lSequence = Optional.ofNullable(sequence).orElseGet(() -> {
                            MySequence mySequence = new MySequence();
                            mySequence.setSeqName(seqType);
                            mySequence.setCurrentVal(0L);
                            sequenceService.save(mySequence);
                            return mySequence;
                        });

                        Long oldVal = lSequence.getCurrentVal();
                        lSequence.setCurrentVal(oldVal + SEQ_VAL_STEP_SIZE);
                        // 先持久化性新的序列号值 - 这里必须能接受redis上传序列号失败造成的部分序列号浪费,
                        // 所以 SEQ_VAL_STEP_SIZE 步长不要太大,不然可能提前耗尽序列号
                        sequenceService.updateById(lSequence);
                        // 利用 redis 的管道批量上传序列号
                        redisTemplate.executePipelined(new SessionCallback<Long>() {
                            @Override
                            public <K, V> Long execute(RedisOperations<K, V> operations) throws DataAccessException {
                                RedisTemplate<String, Long> thisRedisTemplate = (RedisTemplate<String, Long>) operations;
                                // 在步长范围内,递增+1
                                for (int i = 1; i <= SEQ_VAL_STEP_SIZE; i++) {
                                    thisRedisTemplate.opsForList().rightPush(PLATFORM_NO_STACK_KEY+ seqType, oldVal + i);
                                }
                                return null;
                            }
                        });
                        // 上传之后,取出序列号
                        nextPlatformNoTemp = (Long) redisTemplate.opsForList().leftPop(PLATFORM_NO_STACK_KEY+ seqType);
                    }
                    return nextPlatformNoTemp;
                });
            }
        }
        return nextSeqVal;
    }

        /**
     * 格式化序列号
     *
     * @param type     业务类型
     * @param sequence 自然数序列
     * @param str      有值就是前缀+str+自然数五位,没有就是str用日期
     * @return
     */
    public static String formatStrNo(String type, String sequence, String str) {
        String pregfix = type.toUpperCase();
        sequence = String.format("%05d", Integer.valueOf(sequence));
        if (sequence.length() > 5) {
            sequence = StrUtil.sub(sequence, sequence.length() - 5, sequence.length());
        }
        String result = LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd"));
        if (StrUtil.isNotEmpty(str)) {
            result = str;
        }
        String template = "{}" + result + "{}";
        return StrUtil.format(template, pregfix, sequence);
    }
}
暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇