叶子算法-如何在分布式系统中生成不重复的订单号(id)

背景

最近在做***项目,我负责服务端开发,因为涉及到订单(乘车刷码订单,每天都会有大量的订单进入系统),所以肯定需要生成订单号,因为之前没有接触过订单相关的开发,对于如何生成一个唯一的订单号不是很了解。最开始的解决思路是,yyMMdd+9位随机数生成一个全局唯一订单号,当订单量小时,这种方案应该问题不大;当订单数量大时,这种方式肯定会有重复的订单号,而且作为程序员也不能用这么low的方式,所以参考了美团订单号的生成策略,吸收了他们的设计思想,用到了自己的项目中。

之前用过的生成唯一编号的方式:

  • UUID:这种方式虽然可以生成唯一号,但是不适合做订单号,因为订单号是用来做主键的,是全局唯一号。UUID太长,浪费空间。UUID无序不利于索引。信息不安全可能造成MAC地址泄露。
  • 时间:通过时间+随机数的方式生成唯一号。这种方式生成的唯一号当并发量大时很容易出现重复的。

Leaf 实现方案

这个方案是参考美团的订单号实现策略,根据实际业务场景稍作改造,生成满足实际业务需求的订单号。

Leaf 这个名字是来自德国哲学家、数学家莱布尼茨的一句话:

There are no two identical leaves in the world

” 世界上没有两片相同的树叶 “

Leaf-segment 数据库方案

实现思想如下:

  • 使用数据库维护当前最大id和每次获取的id数量。先从数据库中取出step个id放入内存中,同时更新数据中最大id,当使用完时再去数据库里取一次放入内存,一次类推。
  • 各个业务不同的发号需求用 biz_tag 字段来区分,每个 biz-tag 的 ID 获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要复杂的扩容操作,只需要对 biz_tag 分库分表就行。

数据库表设计如下:

 

重要字段说明:

biz_tag 用来区分业务,max_id 表示该 biz_tag 目前所被分配的 ID 号段的最大值,step 表示每次分配的号段长度。原来获取 ID 每次都需要写数据库,现在只需要把 step 设置得足够大,比如 1000。那么只有当 1000 个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从 1 减小到了 1/step,大致架构如下图所示:

 

test_tag 在第一台 Leaf 机器上是 1~1000 的号段,当这个号段用完时,会去加载另一个长度为 step=1000 的号段,假设另外两台号段都没有更新,这个时候第一台机器新加载的号段就应该是 3001~4000。同时数据库对应的 biz_tag 这条数据的max_id 会从 3000 被更新成 4000,更新号段的 SQL 语句如下:

Begin

UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx

SELECT tag, max_id, step FROM table WHERE biz_tag=xxx

Commit

这种模式有以下优缺点:

优点:

  • Leaf 服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。
  • ID 号码是趋势递增的 8byte 的 64 位数字,满足上述数据库存储的主键要求。
  • 容灾性高:Leaf 服务内部有号段缓存,即使 DB 宕机,短时间内 Leaf 仍能正
    常对外提供服务。
  • 可以自定义 max_id 的大小,非常方便业务从原有的 ID 方式上迁移过来。

缺点:

  • ID 号码不够随机,能够泄露发号数量的信息,不太安全。
  • TP999 数据波动大,当号段使用完之后还是会 hang 在更新数据库的 I/O 上,tg999 数据会出现偶尔的尖刺。
  • DB 宕机会造成整个系统不可用。

双 buffer 优化

只采用Leaf-segment方案是有缺陷的,简单的说就是:

Leaf 取号段的时机是在号段消耗完的时候进行的,也就意味着号段临界点的 ID 下发时间取决于下一次从 DB 取回号段的时间,并且在这期间进来的请求也会因为DB 号段没有取回来,导致线程阻塞。如果请求 DB 的网络和 DB 的性能稳定,这种情况对系统的影响是不大的,但是假如取 DB 的时候网络发生抖动,或者 DB 发生慢查询就会导致整个系统的响应时间变慢。

为此,我们希望 DB 取号段的过程能够做到无阻塞,不需要在 DB 取号段的时候阻塞请求线程,即当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。这样做就可以很大程度上的降低系统的TP999 指标。详细实现如下图所示:

 

采用双 buffer 的方式,Leaf 服务内部有两个号段缓存区 segment。当前号段已下发 10% 时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前 segment 接着下发,循环往复。

  • 每个 biz-tag 都有消费速度监控,通常推荐 segment 长度设置为服务高峰期发号 QPS 的 600 倍(10 分钟),这样即使 DB 宕机,Leaf 仍能持续发号10-20 分钟不受影响。
  • 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。

代码实现

内存模型采用Ehcache缓存,数据库使用Mysql。

 

@Service(IDistributedUniqueNoService.STRING_NAME)

public class DistributedUniqueNoServiceImpl implements IDistributedUniqueNoService {

    private static final Logger logger = Logger.getLogger(DistributedUniqueNoServiceImpl.class);



    @Autowired

    private OrderInfoDao orderInfoDao;

    @Autowired

    private Cache cache;





    @Override

    public Long getUniqueNo(String bizTag) {

        synchronized (LockUtil.getInstance()) {

            Element element = cache.get("leaf_segment_1");

            if (element == null) {

                Map<String, Object> uniqueIdMap = orderInfoDao.getUniqueId(bizTag);

                Long maxId = (Long) uniqueIdMap.get("maxId");

                uniqueIdMap.put("id", maxId + 1);// 当前ID

                Element element1 = new Element("leaf_segment_1", uniqueIdMap);

                cache.put(element1);

                return maxId;

            } else {

                Object valObj = element.getObjectValue();

                Map<String, Object> uniqueIdMap = (Map<String, Object>) valObj;

                Long maxId = (Long) uniqueIdMap.get("maxId");

                Integer step = (Integer) uniqueIdMap.get("step");

                Long id = (Long) uniqueIdMap.get("id");



                // 判断是否应该获取第二段

                if ((id - maxId)*10 / step == 1 && (id - maxId)*10 % step == 0) {

                    logger.info("id:"+id+",maxId:"+maxId);

                    element = cache.get("leaf_segment_2");

                    if (element == null) {

                        ExecutorServicePool.qrTranPool.execute(new UniqueNoThread(bizTag));

                    }

                }



                // 判断第一段是否用完

                if (maxId + step == id) {

                    // 把第二段内容给第一段

                    do {

                        element = cache.get("leaf_segment_2");

                    } while (element == null);

                    valObj = element.getObjectValue();

                    Element e = new Element("leaf_segment_1", valObj);

                    cache.put(e);



                    // 删除第二段内容

                    cache.remove("leaf_segment_2");



                    uniqueIdMap = (Map<String, Object>) valObj;

                    maxId = (Long) uniqueIdMap.get("maxId");

                    uniqueIdMap.put("id", maxId + 1);// 当前ID

                    e = new Element("leaf_segment_1", uniqueIdMap);

                    cache.put(e);

                    return maxId;

                }



                uniqueIdMap.put("id", id + 1);// 当前ID

                Element element1 = new Element("leaf_segment_1", uniqueIdMap);

                cache.put(element1);

                return id;

            }

        }

    }



}

 

IDistributedUniqueNoService.java

public interface IDistributedUniqueNoService {   
   String STRING_NAME = "distributedUniqueNoService";    
   Long getUniqueNo(String bizTag);
}

OrderInfoDao.java

<!-- 根据业务类型跟新最大ID -->
  <update id="updateMaxId" parameterType="com.order.entity.DistributedUniqueNoConf">
    update p_distributed_unique_no_conf
    set max_id = max_id + step
    where biz_tag = #{bizTag, jdbcType=VARCHAR}
  </update>
  <select id="selectByBizTag" parameterType="java.lang.String" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from p_distributed_unique_no_conf
    where biz_tag = #{bizTag, jdbcType=VARCHAR}
  </select>
/**
     * 获取全局唯一编号
     * @param bizTag 业务类型
     * @return
     */
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, timeout = 150, rollbackFor = Exception.class)
    public Map<String, Object> getUniqueId(String bizTag) {
        DistributedUniqueNoConf conf = distributedUniqueNoConfMapper.selectByBizTag(bizTag);
        distributedUniqueNoConfMapper.updateMaxId(bizTag);
        Map<String, Object> map = new HashMap<>();
        map.put("maxId", conf.getMaxId());
        map.put("step", conf.getStep());
        return map;
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值