共计 5456 个字符,预计需要花费 14 分钟才能阅读完成。
一、背景
官网商城在双 11、双 12 等大促期间经营同学会精心设计许多给到用户福利的促销流动,当促销流动花色越来越多后就会波及到很多的经营配置工作(如指定流动有效期,指定流动启停状态,指定流动参加商品等等)。
如果因为某些起因导致其中局部配置未按预期配置,等到大促那一刻才发现配置没有正确配置,这样大概率会散失不少订单,同样也可能会呈现错配优惠导致一些本不该享受的优惠也被用户享受到,可能会给商城带来比拟大的损失,因而为了尽量减小后面这些状况的产生的概率,咱们就想能不能提供一种能力,让经营同学在重要的电商大促正式开始前,提前去校验所有期待的优惠是否配置正确。
二、构思
想让经营同学能去校验所配的大促优惠是否失常,同时又心愿不会减少多余的额定工作,如何做到呢?
思考到电商业务的特殊性,所配置的各种大促优惠最终次要都会体现在优惠后的价格上,因而咱们思考从这个角度去实现。
在电商的外围链路上,次要有商详页、购物车、确认订单、提交订单这几个外围场景,那么只需在这几个场景中实现提前看到优惠后的价格即可判断大促优惠是否配置正确。
那当初的关键问题是如何做到 「提前」 看到呢?在前序的促销系列文章咱们介绍了计价核心的建设,计价核心对立收口了所有的优惠价的计算能力,因而咱们只有让计价核心能提供 「提前」 的能力即可。
计价核心计算优惠价失常只会实时计算以后工夫商品可能享受的各种优惠,并将最终优惠价通知上游业务方,所以咱们能让计价核心可能计算「将来某个工夫点」的优惠价即可,而计价核心在计算优惠价时,依赖的一个要害信息是 「以后工夫」,因而咱们只有将所谓的「以后工夫」 进行 「篡改」 变成将来的某个工夫点,实现咱们所谓的穿梭的目标。
还有一个极为重要的点须要关注,也是这个穿梭能力的大前提,就是不能影响线上失常交易,即不能让失常的普通用户也 「提前」 看到将来的优惠价。
因而如何做到既让经营体验又不影响失常用户呢?咱们思考采纳白名单机制,只针对已登录且用户 id 在白名单中的用户能力进行所谓的穿梭体验。
在确定大体思路后,还有一些问题须要确认:
对于穿梭的残缺体验是否只须要商城购物流程?
如果需体验大促期间整个官网商城的所有气氛,可能波及改变的点较为多,比方大促宣传流动页面、专属聚合类商品页面,简化版的只关注整个购物下单流程。
整个穿梭过程是否须要真的要实在创立订单?
因为穿梭时光后,用户的下单工夫和确认订单的工夫是统一的,因而确认订单页的所有优惠及最终的价格是真正的所见即所得,无需实在下单即可获知所有优惠活动信息
所以在提交订单的时候倡议间接阻断并揭示用户“您以后处于时空穿梭,请回到事实中再下单哦”,并不作真正的创立订单,也就不会作后续许多写资源的连锁操作,同时这种状况下也会缩小很多不必要的改变点。
对于穿梭过程中支付的用户非凡券是否须要作非凡标识?
a)穿梭过程中支付的券,如果作了非凡标识,那么退出时光机后,到了优惠券实在可用期后,应倡议不作应用,避免占用普通用户资源,同时这种状况下也不倡议减少优惠券已发券数量。
b)穿梭过程中支付的券,不作非凡标识,那么退出时光机后应用该券与其余失常支付的券并无差别,这种状况算是占用了普通用户资源,那么相应的也倡议减少优惠券已发券数量上。
a 计划须要优惠券零碎作相干的适配改变,但线上实在资源无任何净化或占用;b 计划无需作任何改变,但会强占极少量实在资源,如果经营方感觉问题不大倡议采纳 b 计划,从我的项目角度老本最小。
三、实现
3.1 外围流程图
依据前述的构思计划,得出如下商城穿梭外围购物流程:
3.2 革新重点
从上述流程图中能够看出革新的重点:
- 白名单信息的保护
- 获取「以后工夫」
3.2.1 白名单信息保护
为不便后续穿梭用户工夫信息共享,咱们将此信息(openId: travelTime)存储在配置核心中,并提供相应的治理台不便设置穿梭用户及穿梭工夫点。
3.2.2 获取「以后工夫」
整个上下游关联系统可能都会须要获取 「以后工夫」,而获取「以后工夫」 须要能获取到配置的白名单信息以及以后用户信息。显然为了各个业务零碎能尽可能减少代码变动,获取 「以后工夫」 适宜做到一个公共模块中,各个业务零碎依赖这个公共模块主动具备能获取所期待的「以后工夫」。
因而集成了时光机模块后的整个业务零碎链路关系如下所示:
3.2.3 时光机模块
从前述内容,咱们能够得出时光机模块(vivo-xxx-time-travel)中须要蕴含的次要能力:
- a )穿梭用户白名单信息
- b )获取「以后工夫」
- c )读取、设置上下文 openId
其中 a、b 的实现都比较简单,只需失常接入公司的配置核心,并依据指定 openId 获取 「以后工夫」 即可,比拟麻烦一点的是获取 「以后工夫」 时的这个用户 openId 信息。
之前的各个业务零碎间的接口调用可能是不须要用户 openId 信息的,但当初穿梭用户是指定白名单用户的,所以必须要将入口链路检测到的用户 openId 信息一路向下传递到上游的各个业务零碎中。
计划一:各个业务零碎间接口调用耦合 openId 信息,须要各个业务零碎全副都革新一遍,显然这个计划比拟高级原始也对各业务方十分不敌对,十分不倡议采纳。计划二:因为咱们后端各个业务零碎间都应用 dubbo 进行接口调用,因而咱们能够利用 dubbo 基于 spi 插件机制的定制业务过滤器将 openId 当作附加接口调用时的附加信息进行透传。(如果是其余接口调用形式的,也倡议采纳相似原理的解决形式)
上面咱们就看下时光机模块中一些外围的代码实现:(以后业务零碎作为生产方时执行的过滤器)
以后业务零碎作为生产方时执行的过滤器
/**
* 以后业务零碎作为生产方时执行的过滤器
*/
@Activate(group = Constants.CONSUMER)
public class BizConsumerFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {if (invocation instanceof RpcInvocation) {String openId = invocation.getAttachment("tc_xxx_travel_openId");
if (openId == null && TimeTravelUtil.getContextOpenId() != null) {
// 作为生产方在发动调用前,如果缺失 openId, 则设置上下文的 openId
((RpcInvocation) invocation).setAttachment(openIdAttachmentKey, TimeTravelUtil.getContextOpenId());
}
}
return invoker.invoke(invocation);
}
}
以后业务零碎作为服务提供方执行的过滤器;
/**
* 以后业务零碎作为服务提供方时执行的过滤器
*/
@Activate(group = Constants.PROVIDER)
public class BizProviderFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {if (invocation instanceof RpcInvocation) {String openId = invocation.getAttachment("tc_xxx_travel_openId");
if (openId != null) {// 作为上游服务提供方,获取上游零碎设置的上下文 openId
TimeTravelUtil.setContextOpenId(openId);
}
}
try {return invoker.invoke(invocation);
} finally {TimeTravelUtil.removeContextOpenId();
}
}
}
穿梭工夫获取工具类;
/**
* 穿梭工夫获取工具类
*/
public final class TimeTravelUtil {private static final ThreadLocal<TimeTravelInfo> currentUserTimeTravelInfoThreadLocal = new ThreadLocal<>();
private static final ThreadLocal<String> contextOpenId = new ThreadLocal<>();
public static void setContextOpenId(String openId) {contextOpenId.set(openId);
setUserTravelInfoIfExists(openId);
}
public static String getContextOpenId() {return contextOpenId.get();
}
public static void removeContextOpenId() {contextOpenId.remove();
removeUserTimeTravelInfo();}
/**
* 设置以后上下文用户穿梭信息,如果存在的话
* @param openId
*/
public static void setUserTravelInfoIfExists(String openId) {
// TimeTravellersConfig 会接入配置核心,承载所有白名单穿梭用户信息配置,并将每一个穿梭用户信息转换为 TimeTravelInfo
TimeTravelInfo userTimeTravelInfo = TimeTravellersConfig.getUserTimeTravelInfo(openId);
if (userTimeTravelInfo.isInTravel()) {currentUserTimeTravelInfoThreadLocal.set(userTimeTravelInfo);
}
}
/**
* 移除以后上下文用户穿梭信息
*/
public static void removeUserTimeTravelInfo() {currentUserTimeTravelInfoThreadLocal.remove();
}
/**
* 以后链路上下文是否处于穿梭中
* @return
*/
public static boolean isInTimeTravel() {return currentUserTimeTravelInfoThreadLocal.get() != null;
}
/**
* 获取「以后」工夫,单位:毫秒。* 若以后是穿梭中,则返回设置的穿梭工夫, 否则返回理论零碎工夫
* @return
*/
public static long getNow() {TimeTravelInfo travelInfo = currentUserTimeTravelInfoThreadLocal.get();
return travelInfo != null ? travelInfo.getTravelTime() : System.currentTimeMillis();
}
}
用户穿梭信息
/**
* 用户穿梭信息
*/
public class TimeTravelInfo {
/**
* 以后是否处于穿梭中
*/
private boolean isInTravel = false;
/**
* 以后穿梭工夫点,仅在 isInTravel=true 时无效
*/
private Long travelTime;
public boolean isInTravel() {return isInTravel;}
public void setInTravel(boolean inTravel) {isInTravel = inTravel;}
public Long getTravelTime() {return travelTime;}
public void setTravelTime(Long travelTime) {this.travelTime = travelTime;}
}
在业务零碎依赖这个 vivo-xxx-time-travel 模块后,但凡须要获取以后工夫的中央将原来的 System.currentTimeMillis()改为 TimeTravelUtil.getNow()即可。
3.4 问题
在时光机能力建设过程中碰到一个比拟重要的问题,就是上下文传递 openId 信息时,会呈现跨线程传递失落问题。
如果底层是 Java 线程池间接实现异步调用,那通过对线程池相干拦挡能够实现上下文复制拷贝传递,咱们外部的全链路零碎曾经通过相干代理技术对线程上下文信息已作了相干解决。如果应用 Hystrix 实现异步调用,能够看下笔者另一篇专门介绍的文章《Hystrix 中如何解决 ThreadLocal 信息失落》。
四、最初
本文介绍的时光机相干能力次要利用在官网商城,但并不局限于电商场景,时光机模块在设计的时候就没有与某个具体业务耦合,因而对于其余一些业务场景也能够实用或者有一些借鉴意义。
另外本文中电商场景中关注的是优惠价格是否失常,根本波及到的是读操作,如果有些场景须要穿梭后进行残缺的业务性能操作,如进行理论下单,那么就会波及到一些写操作,此时能够借助影子库的相干能力去实现残缺的穿梭操作之旅。
作者:vivo 官网商城开发团队 -Wei Fuping