浅谈订单号生成设计方案

640?wx_fmt=jpeg

今天讨论分享下订单号生成的简单实现方案,为实际场景中需要用到订单号生成服务提供解决思路。

最简单的方式

基于数据库 auto_increment_increment 来获取 ID。首先在数据库中创建一张 sequence 表,其中 seq_name 用以区分不同业务标识,从而实现支持多种业务场景下的自增 ID,current_value 为当前值,_increment 为步长,可支持分布式数据库的哈希策略。

CREATE TABLE `sequence` (	
  `seq_name` varchar(200) NOT NULL,	
  `current_value` bigint(20) NOT NULL,	
  `_increment` int(4) NOT NULL,	
  PRIMARY KEY (`seq_name`)	
) ENGINE=InnoDB DEFAULT CHARSET=utf8

通过 SELECT LAST_INSERT_ID() 方法,更新 sequence 表,进行 ID 递增,并同时获取上次更新的值。这里注意,current_value = LAST_INSERT_ID(current_value + _increment) 将更新的 ID 赋值给了 LAST_INSERT_ID,否则返回的将是行 id。

<insert timeout="30" id="update" parameterType="Seq">	
    UPDATE sequence	
    SET	
    current_value = LAST_INSERT_ID(current_value + _increment)	
    WHERE	
    seq_name = #{seqName}	
    <selectKey resultType="long" keyProperty="id" order="AFTER">	
        <![CDATA[SELECT LAST_INSERT_ID() ]]>	
    </selectKey>	
</insert>

最后 Dao 提供服务,需要提醒的是注意数据库的事务隔离级别,如果将 getSeq() 方法放到 Service 中有事务的方法里,将出现问题,因为数据库事务开启会创建一张视图,在事务没有提交之前,更新的 ID 还没有被提交到数据库中,这在多线程并发操作的情况下,如果事务里的其他方法导致性能慢了,可能出现两个请求获取到相同的 ID,所以解决方法一是不要将 getSeq() 方法放到有事务的方法里,另一种就是将 getSeq() 方法的隔离界别为 PROPAGATION_REQUIRES_NEW,实现开启新事务,外层事务不会影响内部事务的提交。

@Autowired	
private SeqDao seqDao;	

	
@Autowired	
private PlatformTransactionManager transactionManager;	

	
@Override	
public long getSeq(final String seqName) throws Exception {	
    TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);	
    // 事务行为,独立于外部事物独立运行	
    transactionTemplate	
            .setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);	
    return (Long) transactionTemplate.execute(new TransactionCallback() {	
        public Object doInTransaction(TransactionStatus status) {	
            try {	
                Seq seq = new Seq();	
                seq.setSeqName(seqName);	
                if (seqDao.update(seq) == 0) {	
                    throw new RuntimeException("seq update failure.");	
                }	
                return seq.getId();	
            } catch (Exception e) {	
                throw new RuntimeException("seq update error.");	
            }	
        }	
    });	
}


稍复杂一点的方法

上述的方法的问题,想必大家都知道,就是每次获取 ID 都要调用数据库,在高并发的情况下会对数据库产生极大的压力,我们的改进方法也很简单,就是一次申请一个段的 ID,然后发到内存里,每次获取 ID 先从内存里取,当内存中的 ID 段全部被获取完毕,则再一次调用数据库重新申请一个新的 ID 段。

同样有数据库表的设计,通过 Name 区分业务,用 ID 标明已经申请到的最大值。当然如果是分布式架构,也可以通过增加步长属性来实现。

CREATE TABLE `sequence_value` (	
  `Name` varbinary(50) DEFAULT NULL,	
  `ID` int(11) DEFAULT NULL	
) ENGINE = InnoDB DEFAULT CHARSET = utf8

Step 是 ID 段的内存对象,有两个属性,其中 currentValue 当前的使用到的值,endValue 是内存申请的最大值。

class Step {	
    private long currentValue;	
    private long endValue;	

	
    Step(long currentValue, long endValue) {	
        this.currentValue = currentValue;	
        this.endValue = endValue;	
    }	

	
    public void setCurrentValue(long currentValue) {	
        this.currentValue = currentValue;	
    }	

	
    public void setEndValue(long endValue) {	
        this.endValue = endValue;	
    }	

	
    public  long incrementAndGet() {	
        return ++currentValue;	
    }	
}

代码的实现稍微复杂一点,获取 ID 会根据业务标识 sequencename,先从内存获取 Step 的 ID 段,如果为 null,则从数据库中读取当前最新的值,并根据步长计算 Step,然后返回请求 ID。如果从内存中直接获取到 Step,则直接取 ID,并对 currentValue 进行加一。当 currentValue 的值超过 endValue 时,则更新数据库的 ID,重新计算 Step。

private Map<String,Step> stepMap = new HashMap<String, Step>();	

	
public synchronized long get(String sequenceName) {	
    Step step = stepMap.get(sequenceName);	
    if(step ==null) {	
        step = new Step(startValue,startValue+blockSize);	
        stepMap.put(sequenceName, step);	
    } else {	
        if (step.currentValue < step.endValue) {	
            return step.incrementAndGet();	
        }	
    }	
    if (getNextBlock(sequenceName,step)) {	
        return step.incrementAndGet();	
    }	
    throw new RuntimeException("No more value.");	
}	

	
private boolean getNextBlock(String sequenceName, Step step) {	
    // "select id from sequence_value where name = ?";	
    Long value = getPersistenceValue(sequenceName);	
    if (value == null) {	
        try {	
            // insert into sequence_value (id,name) values (?,?)	
            value = newPersistenceValue(sequenceName);	
        } catch (Exception e) {	
            value = getPersistenceValue(sequenceName); 	
        }	
    }	
    // update sequence_value set id = ?  where name = ? and id = ?	
    boolean b = saveValue(value,sequenceName) == 1;	
    if (b) {	
        step.setCurrentValue(value);	
        step.setEndValue(value+blockSize);	
    }	
    return b;	
}

使用该方法获取 ID 可以减少对数据库的访问量,以降低数据库的压力,但是同样需要注意,获取 ID 同样关注数据库事务问题,因为当系统重启的时候,stepMap 为 null,所以会取数据库查询当前 ID,更计算更新 Step,然后更新数据库的 ID。如果该方法被放到数据库事务里,由于其他方法性能慢了,导致查询之后没有及时更新,并发情况下另一个线程查询的时候,可能会获取到该线程未提交的 ID,因而出现两个线程获取到相同的 ID 问题。

本文小结

订单号生成是一个非常简单的功能,但是在高并发的场景下,高性能和高可用就成为了需要关注的要点。所以,实际工作中的每一个小细节都值得我们去深思。

640?wx_fmt=png

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值