劳怡月 发表于 2025-6-1 21:17:33

每日从0开始生成递增流水号

在实际的业务开发中,我们经常会遇到需要生成具有唯一性的业务编号的场景,例如订单编号、留言编号等。这些编号通常是由日期部分和一个递增的序列号部分组成,以确保在同一天内的编号是唯一的,并且能够反映出业务的增长趋势。本文将详细介绍如何在Java中实现这样一个每天随业务量递增的流水号生成器。
需求分析

假设我们需要为一个订单系统生成唯一的订单编号,编号格式要求为yyyyMMdd加上一个四位或者五位的递增流水号,例如:

[*]当天第一条订单编号为202410120001
[*]第二条订单编号为202410120002
[*]当订单数量达到万级以上时,例如第10000条订单,编号应变为2024101210000
[*]当流水号超过五位数时(例如第100000条),计数器重置为1,从下一条留言开始重新计数,如202410120001
解决方案

可以利用对数据库的读写控制,在数据库中存储当日订单量。每次取值时先更新再取值,写和读作为原子操作。这里采用纯代码方式解决,以免对数据库进行频繁读写。
我们可以使用Java中的AtomicInteger类来保证计数器的原子性递增操作,并通过单例模式来确保生成器在整个应用程序中只有一个实例,从而避免多线程环境下的计数器冲突。同时,我们还需要在每天的开始时对计数器进行初始化,以确保编号的日期部分是准确的。
实现步骤

1. 引入依赖

在实现之前,我们需要确保项目中已经引入了必要的依赖项。这里我们使用了Hutool工具包中的DateUtil和StrUtil类来简化日期和字符串的操作,因此需要在项目的pom.xml文件中添加以下依赖:
<dependency>
    <groupId>cn.hutool</groupId>
    hutool-all</artifactId>
    <version>5.8.11</version>
</dependency>2. 创建MessageIdGenerator工具类

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.util.concurrent.atomic.AtomicInteger;

public class MessageIdGenerator {

    private AtomicInteger sequence = new AtomicInteger(0);
    private String lastDate; // 用于记录上次生成编号的日期

    // 单例模式
    private static volatile MessageIdGenerator instance = null;

    private MessageIdGenerator() {}

    public static MessageIdGenerator getInstance() {
      if (instance == null) {
            synchronized (MessageIdGenerator.class) {
                if (instance == null) {
                  instance = new MessageIdGenerator();
                }
            }
      }
      return instance;
    }

    // 生成编号
    public synchronized String generateId() {
      DateTime now = DateUtil.date();
      String todayDate = DateUtil.format(now, "yyyyMMdd");

      // 检查日期是否发生变化
      if (lastDate == null || !lastDate.equals(todayDate)) {
            // 日期变化,重置计数器
            sequence.set(0);
            lastDate = todayDate;
      }

      int count = sequence.incrementAndGet();

      // 如果计数超过99999,重置计数
      if (count > 99999) {
            sequence.set(1);
      }

      String sequenceStr = StrUtil.padPre(String.valueOf(count), count > 9999 ? 5 : 4, '0');

      return todayDate + sequenceStr;
    }

    // 初始化当天的计数
    public void initSequence(MessageRecordMapper messageRecordMapper) {
      DateTime now = DateUtil.date();
      String todayDate = DateUtil.format(now, "yyyyMMdd");
      QueryWrapper<MessageRecord> queryWrapper = new QueryWrapper<>();
      queryWrapper.like("message_id", todayDate);
      int count = messageRecordMapper.selectCount(queryWrapper);
      sequence.set(count);
      lastDate = todayDate;
    }
}3. 在Service中使用生成器

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.stereotype.Service;

@Service
public class MessageRecordService {

    private final MessageRecordMapper messageRecordMapper;
    private final MessageIdGenerator messageIdGenerator = MessageIdGenerator.getInstance();

    public MessageRecordService(MessageRecordMapper messageRecordMapper) {
      this.messageRecordMapper = messageRecordMapper;
      // 初始化当天的计数
      messageIdGenerator.initSequence(messageRecordMapper);
    }

    public void addMessage(MessageRecord messageRecord) {
      String messageId = messageIdGenerator.generateId();
      messageRecord.setMessageId(messageId);
      messageRecordMapper.insert(messageRecord);
    }
}代码详解

1.单例模式

在MessageIdGenerator类中,我们使用了双重检查锁(Double-Checked Locking)的懒汉式单例模式来确保生成器只有一个实例。这种方式既保证了延迟初始化,又能够在多线程环境下安全地创建唯一的实例:
private static volatile MessageIdGenerator instance = null;

public static MessageIdGenerator getInstance() {
    if (instance == null) {
      synchronized (MessageIdGenerator.class) {
            if (instance == null) {
                instance = new MessageIdGenerator();
            }
      }
    }
    return instance;
}2. 计数器初始化

在生成器的initSequence方法中,我们根据当天的日期查询数据库中已有的记录数量,并设置初始计数。这一步确保了在应用程序重启后,计数器能够从当天的最新计数继续递增,而不会重复生成编号。
public void initSequence(MessageRecordMapper messageRecordMapper) {
    String todayDate = DateUtil.format(DateUtil.date(), "yyyyMMdd");
    QueryWrapper<MessageRecord> queryWrapper = new QueryWrapper<>();
    queryWrapper.like("message_id", todayDate);
    int count = messageRecordMapper.selectCount(queryWrapper);
    sequence.set(count);
}3. 编号生成

在generateId方法中,我们首先获取当前日期格式化为yyyyMMdd,然后递增计数器,并根据计数器的值来判断是否需要补全为五位数字。使用synchronized关键字保证了在多线程环境下生成编号的唯一性和正确性。
public synchronized String generateId() {
    DateTime now = DateUtil.date();
    String todayDate = DateUtil.format(now, "yyyyMMdd");

    int count = sequence.incrementAndGet();

    // 如果计数超过99999,重置计数
    if (count > 99999) {
      sequence.set(1);
    }

    String sequenceStr = StrUtil.padPre(String.valueOf(count), count > 9999 ? 5 : 4, '0');

    return todayDate + sequenceStr;
}4.测试验证

为了验证生成器的正确性和线程安全性,我们可以编写一个简单的测试用例:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class MessageRecordTest {

    @Autowired
    private MessageRecordService messageRecordService;

    @Test
    void testAddMessage() {
      MessageRecord messageRecord = new MessageRecord();
      messageRecord.setContent("测试留言内容");
      messageRecord.setUserId(1L);

      messageRecordService.addMessage(messageRecord);

      // 可以多次调用addMessage方法来模拟多条留言的添加
    }
}总结

通过上述实现,我们成功地创建了一个能够生成每天随业务量递增的流水号的生成器。在实际应用中,可以根据具体需求对生成器进行进一步的优化和扩展,例如增加对不同业务类型的编号生成支持,或者将生成器封装为一个通用的工具类,供多个模块使用。
希望这篇技术博客对你有所帮助!如果你有任何问题或建议,欢迎在评论区留言。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 每日从0开始生成递增流水号