概述
如果你从事过类似B端进销存系统相关的开发工作,一定会遇到一个需求:
业务单据号每天重置,后几位每天从1开始递增。
比如说门店每日的订货单:
- DH
20240701
0001 - DH
20240701
0002 - DH
20240701
0003 - DH
20240701
0004
头两个字母DH表示订货;
中间6位是年份和日期
后四位是自增的
到了第二天的时候,订货单据号会重置重新从1开始:
- DH
20240702
0001 - DH
20240702
0002 - DH
20240702
0003 - DH
20240702
0004
有线下门店的各个餐饮、茶饮店门店端系统的单据,基本都是使用如上的规则。
解决方案讨论
处理这个需求之前,需要先了解这个需求背后隐含的2个问题:
- 单据号当天不能重复,就算在【分布式环境】且有【并发】的情况下也不能重复;
- 单据号每天都需要重置。
如果你到网络上查询解决方案,可能搜索到的大概方案如下:
- 使用Redis或其他缓存系统中的原子操作;
- 使用分布式ID生成器,如Snowflake算法;
- 使用存储过程、触发器等;
- 等等。。。。。。。
这些我都觉得要么太复杂要么太贵了,就比如说使用Redis和MQ的,完全没有必要因为这个小需求,把重量级的Redis和MQ引入进来,且买Redis和MQ示例也是需要钱的。
需要结合当时的业务实际情况和技术团队技术栈情况,使用合适的技术。
解决方案:使用JAVA+mysql+定时任务
由于公司的门店不多,几百家,流量不大,就算有并发,瞬间并发数也是非常低的。是可以直接使用mysql来实现的。
主要的设计思路如下:
- 用一张mysql表,建立自增id,确保在【当天】不重复;
- 使用定时任务,每天凌晨的时候清理掉(TRUNCATE)数据,确保第二天id又从1开始。
这套方案虽然简单粗暴,但它已稳定运行快一年了,暂时未出现过任何问题。算是一套价格便宜又实惠稳定的技术方案了。
具体的实操代码
目前团队用的技术是基于SpringCloud Alibaba +DDD的,会使用到阿里相关的技术组件和涉及到DDD相关的内容。
建立docs_day_id_generator表以及PO对象
CREATE TABLE `docs_day_id_generator` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增id',
`created_date` date NOT NULL COMMENT '创建日期',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_create_date` (`created_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='单据id生成'
对应的po对象:
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 单据id生成器PO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("docs_day_id_generator")
public class DocsDayIdGeneratorPO {
/**
* 自增id
*/
private Long id;
/**
* 创建日期
*/
private Date createdDate;
}
DDD仓储层和领域层实现
剩下的只要在DDD的仓储层和domain层定义相关的接口和实现即可。
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
/**
* 单据id生成器-仓储接口层实现类
*/
@RequiredArgsConstructor
@Repository
public class DocsDayIdGeneratorRepositoryImpl implements DocsDayIdGeneratorRepository {
private final DocsDayIdGeneratorMapper docsDayIdGeneratorMapper;
@Override
public Long createDocsSequence(Date currentDate) {
DocsDayIdGeneratorPO poInsert = DocsDayIdGeneratorPO.builder().createdDate(currentDate).build();
docsDayIdGeneratorMapper.insert(poInsert);
return poInsert.getId();
}
}
仓储层的实现比较简单,只需要构建一个DocsDayIdGeneratorPO记录,插入到表里即可,生成的自增id会存储在po对象的id字段里,直接返回即可。
领域服务层则需要接收一个模块编码,并生成对应模块的单据号,比如订货单据号,盘点单据号,退货单据号等。
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 模块枚举
*/
@Getter
@AllArgsConstructor
public enum ModuleEnum {
ORDER("order", "订货","DH"),
CHECK("check", "盘点","PD"),
REFUND("refund", "退货","TH");
/**
* 编码
*/
private final String code;
/**
* 描述
*/
private final String desc;
/**
* 单据前缀
*/
private final String prefix;
}
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* id生成器-领域层接口实现类
*/
@RequiredArgsConstructor
@Slf4j
@Service
public class DocsDayIdGeneratorDomainServiceImpl implements DocsDayIdGeneratorDomainService {
public final DocsDayIdGeneratorRepository docsDayIdGeneratorRepository;
@Override
public String createDocsCode(ModuleEnum moduleEnum) {
Long code = docsDayIdGeneratorRepository.createDocsSequence(new Date());
/*
单据号规则:前两位是模块编码,接下来六位是日期,后四位是自增id,不足4位,则在前面补零
*/
return moduleEnum.getPrefix() + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + String.format("%04d", code);
}
}
定时任务每天凌晨重置
市面上定时任务的产品非常多,出于统一性,我这边的团队用的是阿里的Schedulex2.0。只要阿里有相关的产品,都是优先选择阿里体系的。
定时任务处理器
import com.alibaba.schedulerx.worker.domain.JobContext;
import com.alibaba.schedulerx.worker.processor.JavaProcessor;
import com.alibaba.schedulerx.worker.processor.ProcessResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 重置单据号自增id,每天从1开始
*/
@RequiredArgsConstructor
@Component
@Slf4j
public class DocsIdResetJobProcessor extends JavaProcessor {
private final DocsDayIdGeneratorDomainService docsDayIdGeneratorDomainService;
@Override
public ProcessResult process(JobContext context) {
try {
docsDayIdGeneratorDomainService.resetdDocsSequence();
return new ProcessResult(true);
}
catch (Exception e){
return new ProcessResult(false,e.getMessage());
}
}
}
resetdDocsSequence方法的实现超级简单,就是一个truncate一下table即可。
<update id="resetdDocsSequence">
truncate table docs_day_id_generator;
</update>
定时任务的cron表达式如下:
0 0 0 * * ?
至于阿里Schedulex2.0界面上相关的配置,这个在网上很容易找到,这里就不赘述了。
总结
选择符合当前自己团队实际情况的技术即可,不一定要高大上的。