文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

浅谈订单号生成的设计方案

2024-12-24 23:53

关注

[[274637]]

最简单的方式

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

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

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

  1. <insert timeout="30" id="update" parameterType="Seq"> 
  2.     UPDATE sequence 
  3.     SET 
  4.     current_value = LAST_INSERT_ID(current_value + _increment) 
  5.     WHERE 
  6.     seq_name = #{seqName} 
  7. <selectKey resultType="long" keyProperty="id" order="AFTER"> 
  8.          
  9. selectKey> 
  10. insert> 

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

  1. @Autowired 
  2. private SeqDao seqDao; 
  3.  
  4. @Autowired 
  5. private PlatformTransactionManager transactionManager; 
  6.  
  7. @Override 
  8. public long getSeq(final String seqName) throws Exception { 
  9.     TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); 
  10. // 事务行为,独立于外部事物独立运行 
  11.     transactionTemplate 
  12.             .setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); 
  13. return (Long) transactionTemplate.execute(new TransactionCallback() { 
  14. public Object doInTransaction(TransactionStatus status) { 
  15. try { 
  16.                 Seq seq = new Seq(); 
  17.                 seq.setSeqName(seqName); 
  18. if (seqDao.update(seq) == 0) { 
  19. throw new RuntimeException("seq update failure."); 
  20.                 } 
  21. return seq.getId(); 
  22.             } catch (Exception e) { 
  23. throw new RuntimeException("seq update error."); 
  24.             } 
  25.         } 
  26.     }); 

稍复杂一点的方法

上述的方法的问题,想必大家都知道,就是每次获取 ID 都要调用数据库,在高并发的情况下会对数据库产生极大的压力,我们的改进方法也很简单,就是一次申请一个段的 ID,然后发到内存里,每次获取 ID 先从内存里取,当内存中的 ID 段全部被获取完毕,则再一次调用数据库重新申请一个新的 ID 段。同样有数据库表的设计,通过 Name 区分业务,用 ID 标明已经申请到的最大值。当然如果是分布式架构,也可以通过增加步长属性来实现。

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

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

  1. class Step { 
  2. private long currentValue; 
  3. private long endValue; 
  4.  
  5.     Step(long currentValue, long endValue) { 
  6. this.currentValue = currentValue; 
  7. this.endValue = endValue; 
  8.     } 
  9.  
  10. public void setCurrentValue(long currentValue) { 
  11. this.currentValue = currentValue; 
  12.     } 
  13.  
  14. public void setEndValue(long endValue) { 
  15. this.endValue = endValue; 
  16.     } 
  17.  
  18. public  long incrementAndGet() { 
  19. return ++currentValue; 
  20.     } 

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

  1. private Map<String,Step> stepMap = new HashMap<String, Step>(); 
  2.  
  3. public synchronized long get(String sequenceName) { 
  4.     Step step = stepMap.get(sequenceName); 
  5. if(step ==null) { 
  6.         step = new Step(startValue,startValue+blockSize); 
  7.         stepMap.put(sequenceName, step); 
  8.     } else { 
  9. if (step.currentValue < step.endValue) { 
  10. return step.incrementAndGet(); 
  11.         } 
  12.     } 
  13. if (getNextBlock(sequenceName,step)) { 
  14. return step.incrementAndGet(); 
  15.     } 
  16. throw new RuntimeException("No more value."); 
  17.  
  18. private boolean getNextBlock(String sequenceName, Step step) { 
  19. // "select id from sequence_value where name = ?"; 
  20.     Long value = getPersistenceValue(sequenceName); 
  21. if (value == null) { 
  22. try { 
  23. // insert into sequence_value (id,name) values (?,?) 
  24. value = newPersistenceValue(sequenceName); 
  25.         } catch (Exception e) { 
  26. value = getPersistenceValue(sequenceName);  
  27.         } 
  28.     } 
  29. // update sequence_value set id = ?  where name = ? and id = ? 
  30.     boolean b = saveValue(value,sequenceName) == 1; 
  31. if (b) { 
  32.         step.setCurrentValue(value); 
  33.         step.setEndValue(value+blockSize); 
  34.     } 
  35. return b; 

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

本文小结

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

【本文是51CTO专栏作者张开涛的原创文章,作者微信公众号:开涛的博客,id:kaitao-1234567】

戳这里,看该作者更多好文

 

来源:51CTO专栏内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯