51

浅谈订单号生成设计方案

 5 years ago
source link: https://www.tuicool.com/articles/VZb6Zv6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

j6NFZnu.jpg!web

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

最简单的方式

基于数据库 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 问题。

本文小结

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

FraiYjR.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK