前言
实际开发中,我们会碰到流水号相关功能
流水号,就是和流水线一样,有序往后增加,到了我们的数据库中,就是每创建一个新的单子,我们对应的数字会增加1
简单流水号案例:第一个单子号 001,那么第二个单子就是 001,以此类推
通过一个单子的流水号码,我们可以甚至了解单子是什么类型、什么时间的,一般流水号由类型、时间、递增数字组成(当然有些可能就是要求没有时间)
时间简易流水号
下面以有没有时间逻辑实现流水号功能(以 typeorm 为了)
创建一个模型 order,用于生成测试流水号
@Entity()
export class Order {
@PrimaryGeneratedColumn({
type: 'bigint'
})
id: number
//流水号(不含时间)OG_自增数字,一直累加
@Index()
@Column({ unique: true })
code: string
//流水号(包含时间)OG_时间戳到日_自增数字,第二日自增数字会归零
//@Index()
@Column({ unique: true })
code_ex: string
@Column()
name: string
@CreateDateColumn()
created_time: Date
}
实现生成不带时间流水号(通过模糊查询,定位查找最大的流水号,提取出需要的数字)
//处理没有日期的流水号,默认5为数字,如果超出后自动在前方补位
//按照降序查找第一个,就是最大流水号,流水号又不能重复,所以成功后会自增
const cOrder = await this.orderRepository.findOne({
select: ['code'],
where: {
code: Like('OM%'),
},
order: {
code: 'DESC',
},
});
let newCode: string;
if (cOrder) {
const num = cOrder.code.substring(2, cOrder.code.length);
newCode = String(parseInt(num) + 1).padStart(5, '0');
console.log('newCode', newCode);
} else {
newCode = '00001';
}
//这个就是不带时间的流水号了
const code = `OM${newCode}`;
实现生成带时间流水号(通过模糊查询,定位到当天,查找最大的流水号,提取出需要的数字)
//处理有日期且精确到日的流水号,默认3为数字,如果超出后自动在前方补位
//仍然是按照降序查找第一个,就是最大流水号,流水号又不能重复,所以成功后会自增
//需要注意的是,需要对比时间,如果时间到了第二日,则重头开始
//时间戳8位,我们设置成8位
const now = dayjs.utc().add(32, 'hour');
const nowStr = now.format('YYYYMMDD');
//直接查找当天中最大的,也可以避免有手动填写到第二天的bug
const cxOrder = await this.orderRepository.findOne({
select: ['code_ex'],
where: {
code_ex: Like(`OM${nowStr}%`),
},
order: {
code_ex: 'DESC',
},
});
let codeExNum: string;
if (cxOrder) {
const num = cxOrder.code_ex.substring(10, cxOrder.code_ex.length);
codeExNum = String(parseInt(num) + 1).padStart(3, '0');
console.log('codeExNum', codeExNum);
} else {
codeExNum = '001';
}
//这个就是带时间的流水号了
const code_ex = `OM${nowStr}${codeExNum}`;
整个逻辑
//这里假设流水号以 OM 开头
const order = new Order();
order.name = '订单' + dayjs().valueOf();
//处理没有日期的流水号,默认5为数字,如果超出后自动在前方补位
//按照降序查找第一个,就是最大流水号,流水号又不能重复,所以成功后会自增
const cOrder = await this.orderRepository.findOne({
select: ['code'],
where: {
code: Like('OM%'),
},
order: {
code: 'DESC',
},
});
let newCode: string;
if (cOrder) {
const num = cOrder.code.substring(2, cOrder.code.length);
newCode = num ? String(parseInt(num) + 1).padStart(5, '0') : '00001';
console.log('newCode', newCode);
} else {
newCode = '00001';
}
order.code = `OM${newCode}`;
console.log(order.code);
//处理有日期且精确到日的流水号,默认3为数字,如果超出后自动在前方补位
//仍然是按照降序查找第一个,就是最大流水号,流水号又不能重复,所以成功后会自增
//需要注意的是,需要对比时间,如果时间到了第二日,则重头开始
//时间戳8位,我们设置成8位
const now = dayjs.utc().add(32, 'hour');
const nowStr = now.format('YYYYMMDD');
//直接查找当天中最大的,也可以避免有手动填写到第二天的bug
const cxOrder = await this.orderRepository.findOne({
select: ['code_ex'],
where: {
code_ex: Like(`OM${nowStr}%`),
},
order: {
code_ex: 'DESC',
},
});
let codeExNum: string;
if (cxOrder) {
const num = cxOrder.code_ex.substring(10, cxOrder.code_ex.length);
codeExNum = num ? String(parseInt(num) + 1).padStart(3, '0') : '001';
console.log('codeExNum', codeExNum);
} else {
codeExNum = '001';
}
order.code_ex = `OM${nowStr}${codeExNum}`;
console.log(order.code_ex);
await this.orderRepository.save(order);
return ResponseData.ok();
上面的情况,即使存在手动输入流水号的情况,也不会出现什么问题,如果手动输入且不参与这个过程,那么加个字段忽略自己填写的即可,
如果是手动生成跟正常流水号一样,那么第一种方案是找最大的,如果输入更大的,则以输入的为基准罢了,似乎也没什么事;对于第二种,直接查询当日的流水号中最大的,这样会乱写的一般不会匹配到当日,或者不符合规则直接不影响我们的流水号策略,如果他填写的日子,如果填写的日期明显是比较靠后的,那么到达当日,则会顺着流水号自增,一般手动填写的,是按照之前的日期补上的吧😂
流水号功能优化简介
上面的案例,看着也不复杂,查询都是模糊查询,这个过程可能会全表扫描,降低查询效率
优化查询效率手段很多,例如:更改表结构、加入索引、减少字段等
除了减少查询字段,我们可以通过更改表结构加入索引的方式优化(仅仅是个人理解的一种方案,实际应该还有不少)
没有时间的流水号
- 将该字段设置为索引即可优化完成,实际上这种时间都不带的,一般不会频繁创建数量有限,看情况加不加索引
有时间的流水号
- 发现直接设置流水号为索引不合适了,毕竟他有两个有效部分,就是需要模糊查询,数据量少,这个就挺好
- 如果每日(每月)数据量庞大需要分表,那么直接按照日期分表即可,那样对应库就是对应日期,此时就变成了一个有效部分,直接使用索引即可
- 如果数据量不小不大也要优化,那么不妨试试将手动的限制规则,不能设置未来的某一天,然后将流水号字段设置成索引,此时最大的一定是我们要参考的流水号,然后按照正常逻辑处理即可,不跨日递增、跨日重置即可
还有另外一种,分两个表维护流水号的,但是个人不推荐,
- 有人分出第二张表,专门维护各个表的流水号,里面保存类型、流水日期、累计数字等信息,其有一个好处就是直接查看这张表就可以确定下次需要生成的流水号了,对于第二种看着,效率稍高一些
- 但是存在一点问题,这两个表操作流程无论加事务还是不加,并发下都会存在一些问题
- 当同时要生成两个流水号时,如果有事务回滚,由于流水号重复,会出现一个成功一个回滚情况,由于读取数字一致,那么保存流水号状态的表信息会回滚到两个操作以前,也就意味着下一次创建一定会出现重复,需要做后续处理
- 如果不加事务,在没有并发情况下,如果后续创建流水单号失败,则可能会出现下次流水号仍然增加的情况,需要后续处理,此时手动回滚仍然会出现事务并发时出现的问题
- 比较稳妥的手段,就是这个两个数据操作要在一个同步队列中运行(串行运行),也就以为这同一台服务器要加锁,多态服务器要上分布锁,相当于丢了西瓜捡芝麻,因此不推荐
加入索引后改进
需要注意的是带时间的流水号,需要自行限制时间不超过最大值的时间
//这里假设流水号以 OM 开头
const order = new Order();
order.name = '订单' + dayjs().valueOf();
//处理没有日期的流水号,默认5为数字,如果超出后自动在前方补位
//按照降序查找第一个,就是最大流水号,流水号又不能重复,所以成功后会自增
//有了索引,能够快速帮我们查询出最大的那个值
const cOrder = await this.orderRepository
.createQueryBuilder('order')
.select('MAX(order.code)', 'code')
.getRawOne();
let newCode: string;
if (cOrder) {
const num = cOrder.code.substring(2, cOrder.code.length);
newCode = num
? String(parseInt(num) + 1).padStart(5, '0')
: '00001';
console.log('newCode', newCode);
} else {
newCode = '00001';
}
order.code = `OM${newCode}`;
//处理有日期且精确到日的流水号,默认3为数字,如果超出后自动在前方补位
//仍然是按照降序查找第一个,就是最大流水号,流水号又不能重复,所以成功后会自增
//需要注意的是,需要对比时间,如果时间到了第二日,则重头开始
//时间戳8位,我们设置成8位
const now = dayjs.utc().add(32, 'hour');
const nowStr = now.format('YYYYMMDD');
//直接查找当天中最大的,也可以避免有手动填写到第二天的bug
//如果有了索引,能够快速帮我们查询出最大的那个值
const cxOrder = await this.orderRepository
.createQueryBuilder('order')
.select('MAX(order.code_ex)', 'code_ex')
.getRawOne();
let codeExNum: string;
if (cxOrder) {
const num = cxOrder.code_ex.substring(10, cxOrder.code_ex.length);
codeExNum = num
? String(parseInt(num) + 1).padStart(3, '0')
: '001';
console.log('codeExNum', codeExNum);
} else {
codeExNum = '001';
}
order.code_ex = `OM${nowStr}${codeExNum}`;
await this.orderRepository.save(order);
最后
流水号的实现很简单,需要优化也很简单,频繁的思考和学习也能让我们看到更多方案和问题,帮我们在工作和学习上更近一步