Java如何生成序列号/订单号

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Java如何生成序列号/订单号

背景

在我们的日常开发中,经常会遇到需要生成订单号的需求,比如商品编号、交易单号、快递单号等等,下面就来介绍几种常见的序列号生成方式。

数据库生成序列号

  • 创建数据表
-- ----------------------------
-- Table structure for sequence
-- ----------------------------
DROP TABLE IF EXISTS `sequence`;
CREATE TABLE `sequence` (
  `key` varchar(64) COLLATE utf8mb4_bin NOT NULL COMMENT '主键',
  `current_no` int NOT NULL COMMENT '序列号',
  `step` int NOT NULL COMMENT '自增步长',
  PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
复制代码

1.在业务系统中,可能会存在多个业务需要生成序列号的需求,所以我们在创建表结构的时候,需要区分出不同业务对应的序列号,所以才创建key这个字段;

2.current_no字段值就是我们需要的目标值;

3.step是我们自增的步长,默认是1;

  • 编写存储过程
CREATE DEFINER=`root`@`%` PROCEDURE `get_sequence_proc`(IN sequence_key varchar(64))
BEGIN
  # 开启事务
  START TRANSACTION;
  # 如果主键不存在,就插入一条新数据;如果主键存储,就更新current_no=current_no+step,超过999999重置为1
  INSERT INTO sequence VALUES (sequence_key,1,1) ON DUPLICATE KEY UPDATE current_no=IF(current_no+step>999999,1,current_no+step);
  # 查询current_no
  SELECT current_no FROM sequence WHERE `key`=sequence_key;
  # 提交事务
  COMMIT;
END
复制代码

1.为了方便起见,我们直接创建一个存储过程,这样在我们的java应用端只需要直接调用即可;

2.存储过程名称为get_sequence_proc,输入参数为sequence_key,字符串类型;

3.为了保证生成序列号的原子性,我们需要开启事务;

4.使用INSERT INTO ON DUPLICATE KEY UPDATE语法特性,如果数据表中不存在对应的key,那么就插入一条新记录,如果已经存在对应的key,那么就更新指定的current_no字段值;

5.我们要避免current_no字段值无限制地自增,所以需要设置一个上限值;

6.最后我们查询出自增后的current_no字段值并返回;

  • Mybatis调用存储过程

SequenceMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.awesomeaccount.dao.mapper.SequenceMapper">
    <select id="nextSequence" statementType="CALLABLE" resultType="long">
         CALL get_sequence_proc(#{sequenceKey,mode=IN,jdbcType=VARCHAR})
    </select>
</mapper>
复制代码

1.和平常的select一样,需要注意的是,statementType="CALLABLE"才表示调用存储过程;

2.SQL语法就是直接CALL 存储过程名称(输入参数)即可;

3.因为最后的返回值是current_no,所以我们在java应用端用long类型接收;

import org.apache.ibatis.annotations.Param;
/**
 * @author zouwei
 * @className WalletEnhanceMapper
 * @date: 2022/9/27 21:33
 * @description:
 */
public interface SequenceMapper {
  Long nextSequence(@Param("sequenceKey") String sequenceKey);
}
复制代码

这样我们就可以愉快地在业务逻辑中使用序列号生成器了。

Redis生成序列号

在redis中生成序列号,我们最先想到的就是使用自增语法incr实现,然后再使用查询语句将对应的结果查询出来,但是有一个问题就是,我们需要保证原子性,这时候我们就要想到Lua脚本了;另外为了提升获取序列号的性能,我们最好应用池化的思想创建一个序列号池:

  • Lua脚本:
--[不同的业务不同的key]
local key=KEYS[1]
--[初始值]
local initValue=tonumber(ARGV[1])
--[最大值]
local maxValue=tonumber(ARGV[2])
--[池化最小数量]
local minCount=tonumber(ARGV[3])
--[池化初始化数量]
local initCount=tonumber(ARGV[4])
--[池化扩容步长]
local expansionStep=tonumber(ARGV[5])
--[自增步长]
local incrStep=tonumber(ARGV[6])
--[获取队列长度]
local len=redis.call('llen',key)
--[是否需要初始化,0:第一次调用只初始化,1:第一次调用初始化并返回序列号]
local isInit=tonumber(ARGV[7])<=0
--[循环次数默认为池化最小数量]
local loop=initCount
--[初始化nextValue]
local nextValue=initValue
--[如果队列长度大于最小数量,直接返回队列最左值]
if len>minCount
  then
  return redis.call('lpop',key)
end
--[如果队列长度小于最小数量,那么要准备补充序列号]
if len>0
  then
  --[计算需要循环的次数,当前长度+扩容步长]
  loop=len+expansionStep
  nextValue=tonumber(redis.call('rpop',key))
end
--[循环创建,直至最大值后重置为初始值]
while(len<loop)
  do
    if nextValue>maxValue
      then
      nextValue=initValue
    end
    redis.call('rpush',key,nextValue)
    nextValue=nextValue+incrStep
    len=len+1
  end
--[如果只进行初始化,那么返回success,不返回序列号]
if isInit
  then
    return 'success'
end
--[返回序列号]
return redis.call('lpop',key)
复制代码

1.在redis中生成一个队列,指定初始化长度,第一个初始值,最大值,队列最小数量,每次扩容的数量,自增的步长;

2.如果队列不存在,就初始化队列,按照给定的初始化长度,初始值,自增步长,最大值等参数创建一个队列;

3.如果队列中值的数量超过队列最小数量,那么直接pop出一个值;

4.如果小于最小数量,那么直接循环生成指定步长的自增ID;

5.如果表明此次是初始化的话,会返回success,否则就直接pop出第一个ID;

  • java代码调用Lua脚本
private static final String LUA_SCRIPT = "LUA脚本";
private String executeLua(
            String key,
            int initValue,
            int maxValue,
            int minCount,
            int initCount,
            int expansionStep,
            int incrStep,
            int isInit) {
        // 执行lua脚本
        DefaultRedisScript<String> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setResultType(String.class);
        defaultRedisScript.setScriptText(LUA_SCRIPT);
        RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
        String result =
                CastUtil.castString(
                        redisTemplate.execute(
                                defaultRedisScript,
                                serializer,
                                serializer,
                                Lists.newArrayList(key),
                                CastUtil.castString(initValue),
                                CastUtil.castString(maxValue),
                                CastUtil.castString(minCount),
                                CastUtil.castString(initCount),
                                CastUtil.castString(expansionStep),
                                CastUtil.castString(incrStep),
                                CastUtil.castString(isInit)));
        return result;
    }
复制代码

1.我们在java中通过RedisTemplate来执行Lua脚本;

2.该方法提供了两个功能,一个是初始化序列号池,另一个是初始化并返回序列号;当我们传递的isInit=0时仅仅会初始化序列号池,并返回success告知初始化成功;isInit=1时,会真正地返回我们需要的序列号;



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6月前
|
Java
JAVA生成根据用户id唯一订单号,并发下唯一
JAVA生成根据用户id唯一订单号,并发下唯一
130 1
|
Java
java 获取一定长度的随机字符串 可用于生成交易订单号交易号等
java 获取一定长度的随机字符串 可用于生成交易订单号交易号等
138 0
|
Java 容器
我的Java开发学习之旅------&gt;Java使用ObjectOutputStream和ObjectInputStream序列号对象报java.io.EOFException异常的解决方法
今天用ObjectOutputStream和ObjectInputStream进行对象序列化话操作的时候,报了java.io.EOFException异常。 异常代码如下: java.
1325 0
|
Java Maven
java工具使用-01-使用commons.lang3生成随机的20位的订单号
如何使用commons.lang3生成随机的20位的订单号: 第一步:引入maven依赖或者去官网下载相应的jar包下载地址:http://apache.fayea.
1301 0
|
Java
java 生成订单号
public class Order { private static int orderNum = 0001; public static String getOrderNo(){ long No = 0; SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); String nowdate = sdf.
992 0
|
2天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
4天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
4天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
4天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
20 3