关于java:vivo-全球商城电商平台通用取货码设计

39次阅读

共计 4947 个字符,预计需要花费 13 分钟才能阅读完成。

vivo 官网商城开发团队 – Zhou Longjian

一、背景

随着 O2O 线上线下业务的一直扩大,电商平台也在逐步完善交易侧相干的产品性能。在最近的需要版本中,业务方为进一步晋升用户的应用体验,布局了取货码生成及订单核销相干逻辑,目标是让线上的用户在付完款之后可能到店取货或者安顿导购派送。

日常生活中,咱们对取货码、核销这类性能应用的经验大部分都来自:看电影前取票、吃饭后出示券码、快递柜取包裹等等,它们都有一些相似的特点,比方:

  • 取货码长度绝对较短,比起动辄十几二十位订单号,几位的数字码更不便记忆和输出;
  • 除了数字取货码,还提供二维码,不便终端进行扫描并核销。

取货码应用起很简略,然而像“冰山”一样,暗藏在简略表面上面却须要谨严的设计和粗疏的逻辑,能够说麻雀虽小五脏俱全。本文介绍的设计也比拟乏味,而且按此思路能够实现市面上大多数核销类券码的生成,同时也能满足业务的 SaaS 化,算是一个绝对通用的能力,在此把整个设计分享给大家。

(图片起源:pixabay.com)

二、简略零碎的单表业务

如果业务的体量不大,店铺流量比拟小,未造成平台的规模,比方给个体经营者应用的零碎。那么取货码或券码的实现就比较简单,跟订单共享一张大横表或者应用扩大表跟订单进行关联就行了,这个阶段也无需做适度设计。

表的设计如下图:

不过须要留神的是个别订单号都是比拟长的,通常都在十几二十位(当然也有比拟短的订单号,如果订单号比拟短,取货码也可采纳订单号)咱们假如订单号 18 位,取货码 8 位,即订单号的取值范畴远大于取货码,那么在订单号的生命周期内,取货码是有很大几率存在反复的。解决起来绝对简略,咱们只须要保障在任意条件下,未核销 状态的数字码不反复即可,也即已核销的数字码能够回收利用。

那么取货码的生成逻辑就很清晰了,上面用伪代码模仿实在的实现逻辑:

伪代码实现

for (;;) {step1 获取随机码:String code = this.getRandomCode();
   step2 执行 SQL:SELECT COUNT(1) FROM order_main WHERE code = ${code} AND write_off_status = 0;
   step3 判断是否能够插入:if (count > 0) {continue;}
   step4 执行数据写入:UPDATE order_main SET code = ${code}, qr_code = ${qrCode}, write_off_status = 0 WHERE order_no = ${orderNo}
}

* 留神:这里 step2 和 step4 不是原子操作,存在并发问题,理论利用中最好应用分布式锁,把操作锁住。

三、简单平台的分库分表业务

通过简略的单表设计,咱们能管窥一斑,理解取货码大抵的实现逻辑。不过咱们在把简略计划往大型项目上进行落地的时候,就须要思考很多方面,设计也须要更精美。SaaS 化的电商平台会比简略的单表业务简单很多,重点体现在:

  1. SaaS 产品波及的店铺很多且订单量大,须要设计大容量存储,所以订单表根本应用分库分表,显然作为订单从属的取货码表也得应用雷同的策略;
  2. B 端和 C 端用户的体验十分重要,服务端接口的设计须要充分考虑鲁棒性,欠缺最根本的重试及容错能力;
  3. 不同业务方对于取货码的要求可能不太一样,取货码的设计须要具备通用性以及个性化的配置属性。

3.1 具体设计

取货码表的设计举荐应用和订单统一的分库分表策略,益处是:

  1. 和订单一样,撑持海量订单行的存储;
  2. 不便利用同样的分库分表因子进行查问(例如:open\_id、member\_id)。

在思考落地实现上,咱们遇到了第一个探讨的点,那就是取货码是做到“门店惟一”还是“全局惟一”?

3.2 门店惟一计划

刚开始思考应用相似饭馆取餐码相似的逻辑,保障取货码在各自门店放弃惟一就行了。相似如下图交互,图中用户 A 和用户 B 持有雷同的取货码,用户 A、B 别离去他们对应的店铺实现核销,整个交易过程就完结了。然而这得保障用户 A 和 B 能正确地在各自订单归属的店铺实现核销,显然这个计划是带有危险的!

下图所示的这种状况下,用户 A、B 也能失常核销,不过串单了,本来属于用户 A 的订单被用户 B 核销了。这种问题呈现的实质起因在于纯正的数字码无奈带有用户的标识,尽管能够在核销前做人为的核验身份来防止,但仍然属于高风险的零碎设计,所以门店惟一计划不可取!

3.3 全局惟一计划

全局惟一计划危险小,但实现难度稍高一点。外围问题在于如何断定随机生成的取货码是全局惟一的,当然如果零碎自身依赖 ES 这类存储介质,能够在插入前先查问 ES,不过查问和写入 ES 对于实时性接口来说略微有点重,没有间接查库表来得间接。假如某业务方分成了 4 个库 4 张表,总计 16 表,取货码的长度确定为 8 位,那如何在多库多表的 Mysql 中查问并保障全局惟一呢?遍历表的形式必定不可取!

为解决上述的疑难,咱们在设计的时候能够在取货码的编排上做点文章,如下步骤做具体详解:

步骤①:能够将 8 位的取货码分成两个区域,“随机码区域”+“库表地位”,下图示例:

步骤②:随机码区域暂不介绍,咱们来看下 2 位库表如何映射到 4 库 4 表组成的 16 张表中。

这里也有两套计划:

【计划一】能够抉择 2 位库表的首位作为库编号,末位作为表编号。益处是映射较为简单,然而容量不够大,如果分的库或表 >9,扩大就会有点麻烦。如下图,咱们把开端“12”逻辑映射到了“1 库的编号为 2 的表”;

【计划二】将 4 库 4 表二维构造转成一维,以 0 为初始值进行递增,(0 库, 0 表) → 00, (0 库, 1 表) → 01… , (3 库, 3 表) → 15。益处是容量变大了,最大反对 99 张表,不受库或表繁多条件的限度,毛病就是映射逻辑写起来麻烦点,不过这不是问题。

取货码通过简略编排,咱们实现了取货码的到库表的映射逻辑,解决了取货码存取的问题。其实认真想想,对于全局惟一的问题其实也解决掉了,咱们只有保障前 6 位随机码在单表里保障惟一即可,实践上反对单表在未核销状态下范畴为:000000 ~ 999999 条记录,容量是足够的。要害咱们把多库多表的查问就简化成了只跑一个 SQL,效率大大晋升。

3.4 计划落地遇到的问题

既然本篇是介绍 SaaS 化的残缺计划,在落地的时候或多或少会遇到一些问题,这边介绍三个理论遇到的典型问题,并给出一些解决方案:

【问题一】应用 Math.random()生成的 6 位随机码和表里的反复了,如何解决?

【解决】其实反复的状况有两种:

  1. 可能是表里曾经存在数字雷同未核销的取货码;
  2. 另外一种状况就是别的事务在正在操作,正好有个分布式事务锁住了一样的数字码(概率很低,然而是有可能的)。

这两种状况的呈现就须要咱们进行优雅地重试了!大抵思路如下伪代码:

// step1 依据分库分表因子获取库表编号,userCode- 用户编号、tenantId- 租户编号
String suffix = getCodeSuffix(userCode, tenantId);
 
// step2 批量获取 6 位随机码
for (int i=1; i<=5; i++) {
   // 批量获取随机数。每次重试,取 2 的指数级量进行过滤,相比暴力执行 for 循环,这种形式能缩小和 DB 的交互
   List<String> tempCodes = getRandomCodes(2 << i);
   // 过滤掉分布式锁
   filterDistributeLock(tempCodes);
   // 过滤掉数据库存在的随机码
   filterExistsCodes(tempCodes);
   return tempCodes;
}
 
// step3 解决随机码,随机码入库
for (String code : codes) {
   // 加锁,判断加锁是否胜利。举荐应用 Redis 分布式锁
   boolean hasLockd = isLocked(code);
   try {
         // 执行入库
         insert(object);
   } finally {// 解锁}
}
 
// step4 执行后置二维码图片等逻辑

【留神】

  1. 举荐应用指数级重试的形式(2 << i),逐次递增 random 的数量,缩小和 DB 的交互;
  2. 倡议数字码生成结束后加锁并执行 INSERT,生成图片地址等耗时重大的动作能够后置 UPDATE 下来。

【问题二】我的项目中应用了分库分表的组件(比方:ShardingSphere-JDBC),怎么动静批改数据源?也就是同时反对分库分表因子(比方:member\_id、open\_id 等)以及依据取货码计算的库表动静查问。

【解决】咱们以 ShardingSphere-JDBC 作为为案例来给出一些配置及伪代码,具体能够参考:《强制路由::ShardingSphere》,其余开源的分库分表组件或者自研产品不做赘述,能够本人手动写个插件,别怕,即便再难,也要置信有光!

配置及伪代码

// ShardingSphere-JDBC 依赖的配置文件 jdbc-sharding.yaml
...
shardingRule:
  tables:
    ...
    # 取货码表
    order_code:
      actualDataNodes: DS00$->{0..3}.order_pick_up_0$->{0..3}
      # 配置库的计算逻辑
      databaseStrategy:
        hint:
          algorithmClassName: com.xxx.xxxxx.xxx.service.impl.DbHintShardingAlgorithm
      # 配偶之表的计算逻辑
      tableStrategy:
        hint:
          algorithmClassName: com.xxx.xxxxx.xxx.service.impl.DbHintShardingAlgorithm
    ...
 
// java 代码
try (HintManager hintManager = HintManager.getInstance()) {hintManager.addDatabaseShardingValue("order_code"/** 取货码表 */, DbHintShardingAlgorithm.calDbShardingValue(tenantId, code));
    hintManager.addTableShardingValue("order_code"/** 取货码表 */, DbHintShardingAlgorithm.calTabShardingValue(tenantId, code));
     
    Object xxx = xxxMapper.selectOne(queryDTO);
}

【留神】

  1. 这里介绍一种编程式的解决方案,益处是配置简略、比拟灵便,毛病就是代码略微多一点。其实 ShardingSphere 还反对注解的形式,能够本人钻研下;
  2. 第一条说了比拟灵便,体现在本人实现的“DbHintShardingAlgorithm.calDbShardingValue(tenantId, code)”办法上,这个办法能够本人定义,所以咱们的入参能够是通用的分库分表因子,也能够是自定义的取货码的“库表地位”字段,非常灵活。

【问题三】如何做到更强的扩展性,实用 SaaS 平台以及不同的业务场景?

【解决】仔细的小伙伴应该留神到了 “tenantId” 这个字段,这是个租户的编码,在理论编码会进行透传。咱们能够利用这个字段针对不同的租户(或叫业务方)来做不同的配置,比方:取货码的长度、取货码编排的形式、取货码映射库表地位的策略等等做成可配,只有把骨干逻辑进一步形象,并应用策略模式进行个性化编码。

四、总结

实现取货码逻辑的时候,发现网上券码这块的计划、技术文章比拟少,过后萌发了写篇文章抛砖引玉做个分享的想法。事实上,我置信大多数公司可能或多或少也是这么做的,哪怕采取了别的计划也能必由之路。本篇文章整体只是介绍了一个思路,而这个思路相似一个简化版的订单分库分表,但这就是神奇所在,事实上咱们还能够将一些罕用的技术计划落地到不同的利用场景,大胆地做一些尝试,多走一些未曾构想过的路线!

主题系列文章:

  • vivo 寰球商城全球化演进之路—多语言解决方案
  • vivo 寰球商城:商品零碎架构设计与实际
  • vivo 寰球商城 - 营销价格监控计划的摸索
  • vivo 寰球商城:优惠券零碎架构设计与实际
  • vivo 寰球商城:订单核心架构设计与实际
  • vivo 寰球商城:从 0 到 1 代销业务的交融之路
  • vivo 寰球商城:架构演进之路

正文完
 0