背景
最近在做***项目,我负责服务端开发,因为涉及到订单(乘车刷码订单,每天都会有大量的订单进入系统),所以肯定需要生成订单号,因为之前没有接触过订单相关的开发,对于如何生成一个唯一的订单号不是很了解。最开始的解决思路是,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;
}