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

一、背景

随着O2O线上线下事务的不断扩展,电商渠道也在逐步完善买卖侧相关的产品功用。在最近的需求版本中,事务方为进一步提高用户的运用体会,规划了取货码生成及订单核销相关逻辑,目的是让线上的用户在付完款之后能够到店取货或许组织导购派送。

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

  • 取货码长度相对较短,比起动辄十几二十位订单号,几位的数字码更便利记忆和输入;

  • 除了数字取货码,还提供二维码,便利终端进行扫描并核销。

取货码运用起很简略,可是像“冰山”相同,隐藏在简略外表下面却需求谨慎的规划和细致的逻辑,能够说麻雀虽小五脏俱全。本文介绍的规划也比较有趣,而且按此思路能够完结市面上大多数核销类券码的生成,同时也能满意事务的SaaS化,算是一个相对通用的才能,在此把整个规划共享给咱们。

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

(图片来源:pixabay.com)

二、简略体系的单表事务

假如事务的体量不大,店肆流量比较小,未形成渠道的规模,比方给个体经营者运用的体系。那么取货码或券码的完结就比较简略,跟订单共享一张大横表或许运用扩展表跟订单进行相关就行了,这个阶段也无需做过度规划。

表的规划如下图:

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

不过需求留意的是一般订单号都是比较长的,一般都在十几二十位(当然也有比较短的订单号,假如订单号比较短,取货码也可采用订单号)咱们假定订单号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能正确地在各自订单归属的店肆完结核销,明显这个计划是带有危险的!

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

下图所示的这种状况下,用户A、B也能正常核销,不过串单了,原本归于用户A的订单被用户B核销了。这种问题出现的本质原因在于纯粹的数字码无法带有用户的标识,虽然能够在核销前做人为的核验身份来避免,但仍然归于高危险的体系规划,所以门店仅有计划不可取!

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

3.3 大局仅有计划

大局仅有计划危险小,但完结难度稍高一点。核心问题在于怎么判定随机生成的取货码是大局仅有的,当然假如体系自身依靠ES这类存储介质,能够在插入前先查询ES,不过查询和写入ES关于实时性接口来说略微有点重,没有直接查库表来得直接。假定某事务方分成了4个库4张表,总计16表,取货码的长度确定为8位,那如安在多库多表的Mysql中查询并确保大局仅有呢?遍历表的办法必定不可取!

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

为处理上述的疑问,咱们在规划的时分能够在取货码的编排上做点文章,如下过程做具体详解:

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

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

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

这儿也有两套计划:

**【计划一】**能够挑选2位库表的首位作为库编号,末位作为表编号。优点是映射较为简略,可是容量不够大,假如分的库或表>9,扩展就会有点麻烦。如下图,咱们把结尾“12”逻辑映射到了“1库的编号为2的表”;

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

**【计划二】**将4库4表二维结构转成一维,以0为初始值进行递加,(0库, 0表) → 00, (0库, 1表) → 01… , (3库, 3表) → 15。优点是容量变大了,最大支撑99张表,不受库或表单一条件的限制,缺陷便是映射逻辑写起来麻烦点,不过这不是问题。

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

取货码经过简略编排,咱们完结了取货码的到库表的映射逻辑,处理了取货码存取的问题。其实细心想想,关于大局仅有的问题其实也处理掉了,咱们只要确保前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 全球商城:架构演进之路