引言
许多企业系统都涉及到了订单号的生成。订单号可以帮我们我们标识用户的一次行为。因此它必须是全局唯一的。我们当然可以采用类似UUID这种全时空唯一的字符串来标识一个订单,但是UUID对于用户和我们自己来说都过于复杂了,用户无法记忆甚至无法用它来要求客服查询。一个好的订单号在保证简单、唯一性的情况下,应该具有自解性。根据这个订单号我们可以解读出用户购买的业务、购买的时间等信息。下面介绍几种订单号的生成方案。
解决方案
加锁和时间戳
如果是在小型单台服务的系统上,订单号的生成可以采用简单的时间戳(精确到毫秒)配合JVM级别的锁来实现。但是更常见的情况是,我们出于系统高可用、负载均衡等方面的考虑,部署的架构往往是多服务器的。这种情况下,就必须重新考虑订单号的生成策略了。
数据库自增ID
利用数据库的自增ID,结合当前时间戳或者业务编号,是一种非常常见的订单号生成方式。有两种实现方式:
新建一张表定义自增列,在应用层获取该列的自增值
新建一张序列表,保存自增序列的当前值
这两种实现方式各有优劣:
方式1必须定义该列的字段足够大,否则可能达到最大值后会报错。
方式2序列的增长在应用层,必须保证事物。
它们共同的问题是:
在生成订单号时,我们都需要进行一次数据库操作,在高并发高访问的情况下,容易造成数据库压力过大,形成性能瓶颈。
系统足够庞大,进行分库分表之后。又会带来保证序列唯一性的额外问题。
它们很容易泄漏商品的销量和操作次数这些敏感信息
集中式ID管理
常见的方式有:
将自增序列保存到集中式or分布式缓存中,由于集群服务是共享分布式缓存的,因此很好地解决了订单号的唯一性问题,同时缓存具有足够的伸缩性,也可以做高可用、持久化。
订单号服务,独立的订单号生成管理服务,提供给各业务线进行调用。
集中式ID管理的优势很明显,无论是性能扩展还是高可用方面都有非常好的解决方案。劣势就是系统较为庞大,需要搭建专门的缓存服务(如redis、tair、memcached等),订单号服务为了避免单点故障还需要做冗余。
系统标识的订单号生成方案
我们可以根据系统的一些唯一属性来生成唯一的订单号,这个属性可以是服务器的IP、机器码、MAC地址等,结合JVM锁,既避免了数据库自增ID的性能瓶颈问题,又无需集中式ID管理的大材小用。下面是我的一个简单实现方案。
是通过时间戳+IP后两位+自增序列+随机数来生成的订单号,使用可重入锁代替内置锁(synchronized)以解决在高并发请求下的细微性能问题。
mons.util.order;
.InetAddress;
.UnknownHostException;
importjava.text.SimpleDateFormat;
importjava.util.Date;
importjava.util.Random;
importjava.util.concurrent.TimeUnit;
importjava.util.concurrent.locks.Lock;
importjava.util.concurrent.locks.ReentrantLock;
mons.lang3.StringUtils;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
/**
*订单号生成器
*
*@authorYaphis4月29日下午7:12:44
*/
publicclassOrderGenerater{
privatestaticfinalLoggerLOG=LoggerFactory.getLogger(OrderGenerater.class);
privatevolatilestaticintserialNo=0;
privatestaticfinalStringFORMATSTRING="yyMMddHHmmssSSS";
/**
*使用公平锁防止饥饿
*/
privatestaticfinalLocklock=newReentrantLock(true);
privatestaticfinalintTIMEOUTSECODES=3;
/**
*生成订单号,生成规则时间戳+机器IP最后两位+2位随机数+两位自增序列
*采用可重入锁减小锁持有的粒度,提高系统在高并发情况下的性能
*
*@parambuinessId
*@return
*/
publicstaticStringgenerateOrder(){
StringBuilderbuilder=newStringBuilder();
builder.append(getDateTime(FORMATSTRING)).append(getLastNumOfIP());
builder.append(getRandomNum()).append(getIncrement());
returnbuilder.toString();
}
/**
*获取系统当前时间
*
*@paramformatStr
*@return
*/
privatestaticStringgetDateTime(StringformatStr){
SimpleDateFormatformat=newSimpleDateFormat(formatStr);
returnformat.format(newDate());
}
/**
*获取自增序列
*
*@return
*/
privatestaticStringgetIncrement(){
inttempSerialNo=0;
try{
if(lock.tryLock(TIMEOUTSECODES,TimeUnit.SECONDS)){
if(serialNo>=99){
serialNo=0;
}else{
serialNo=serialNo+1;
}
tempSerialNo=serialNo;
}else{
//指定时间内没有获取到锁,存在激烈的锁竞争或者性能问题,直接报错
LOG.error("cannotgetlockin:{}seconds!",TIMEOUTSECODES);
thrownewRuntimeException("generateOrdercannotgetlock!");
}
}catch(Exceptione){
LOG.error("tryLockthrowsException:",e);
thrownewRuntimeException("tryLockthrowsException!");
}finally{
lock.unlock();
}
if(tempSerialNo
return"0"+tempSerialNo;
}else{
return""+tempSerialNo;
}
}
/**
*返回两位随机整数
*
*@return
*/
privatestaticStringgetRandomNum(){
intnum=newRandom(System.nanoTime()).nextInt(100);
if(num
return"0"+num;
}else{
returnnum+"";
}
}
/**
*获取IP的最后两位数字
*
*@return
*/
privatestaticStringgetLastNumOfIP(){
Stringip=getCurrentIP();
returnip.substring(ip.length()-2);
}
/**
*获取本机IP
*
*@return
*/
privatestaticStringgetCurrentIP(){
Stringip="";
try{
ip=InetAddress.getLocalHost().getHostAddress();
}catch(UnknownHostExceptione){
LOG.error("getLocalHostthrowsUnknownHostException:",e);
thrownewRuntimeException("cannotgetip!");
}
if(StringUtils.isBlank(ip)){
LOG.error("ipisblank!");
thrownewRuntimeException("ipisblank!");
}
returnip;
}
}
附一段测试代码
mons.util.order;
importjava.util.ArrayList;
importjava.util.List;
importorg.junit.Assert;
importorg.junit.Test;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
/**
*订单号生成器单元测试
*
*@authorYaphis5月1日下午3:17:51
*/
publicclassOrderGeneraterTest{
privatestaticfinalLoggerLOG=LoggerFactory.getLogger(OrderGeneraterTest.class);
/**
*验证订单号生成是否会重复和耗时(单线程)
*/
@Test
publicvoidtestGenerateOrder(){
ListorderList=newArrayList();
longstartTime=System.currentTimeMillis();
for(inti=0;i
StringorderId=OrderGenerater.generateOrder();
if(orderList.contains(orderId)){
LOG.info("orderId:{}",orderId);
LOG.info("orderList:{},list:{}",newObject[]{orderList.size(),orderList});
Assert.fail("订单号重复!");
}
orderList.add(orderId);
}
LOG.info("generateOrdercost:{}",System.currentTimeMillis()-startTime);
}
/**
*验证订单号生成是否会重复和耗时(多线程)
*/
publicstaticvoidmain(String[]args){
Listlist=newArrayList();
longstartTime=System.currentTimeMillis();
//模拟1000个并发请求
for(intj=0;j
newThread(newThreadTest(list)).start();
}
LOG.info("testGenerateOrderMultithreadcost:{}",System.currentTimeMillis()-startTime);
}
/**
*测试线程
*
*@authorYaphis5月1日下午3:59:19
*/
publicstaticclassThreadTestimplementsRunnable{
privateListlist;
publicThreadTest(Listlist){
this.list=list;
}
publicvoidrun(){
for(inti=0;i
StringorderId=OrderGenerater.generateOrder();
if(list.contains(orderId)){
LOG.error("订单号重复!");
break;
}
list.add(orderId);
}
}
}
}
其他:
以上实现其实还比较简陋,许多情况没有考虑。例如多网卡情况下的多IP问题等、欢迎大家交流指正!