打通电商多模式支持的“任督二脉”

你听说过任督二脉吗?像这样~

咳咳~今天不讲武功,讲电商平台设计的功夫~

背景

当今的电商可不仅仅是B2C商城,接下来还会有O2O,往后可能还会有商超、奥莱、二手交易。。。且称之为业务模式~而每个业务模式下还会有预售、竞拍、拼团等不同组合的子模式。

可是我商城的商品列表页不想展示O2O的商品啊,商品列表的数据希望按一定规则相互隔离。其他模块,有的出于操作习惯的考虑不隔离,有的出于用户行为的考虑需要隔离。

各模块数据隔离需求如下


 


列表页


商详页


商品组


优惠券


活动


订单


...


原商城


隔离


隔离


隔离


暂时不隔离


暂时不隔离


隔离


O2O


隔离


隔离


隔离


暂时不隔离


暂时不隔离


隔离

各模块流程差异


 


新建商品


列表页


购物车


订单


...


原商城


店铺创建,门店设置库存


基于item建es文档


跨门店


状态流转走快递


O2O


门店创建(沿用原模型但弱化店铺的概念)


基于item建es文档


单个门店


状态流转走配送

于是我们就会面临不同的改造的场景。

场景1,新建商品就是新建商品啊!!!

例如商品的新建保存,是基础服务,已经具备通用存储模型。为了支持新模式我还得改服务接口、发布二方包?咱可不可以这样?

商品服务

Integer bizMode = BizModeContext.getBizMode();
itemDO.setBizMode(bizMode);
// ...
itemDAO.save(itemDO);

场景2,下单就是下单啊!!!

例如创建订单,虽然商品维度、订单类型、优惠方式有很多,但我修改一下B2C下单的字段计算,还要引发O2O模式的回归测试?咱可不可以这样?

甚至这样~

实现类路由

@BizModeService( bizMode=BizMode.B2C, srvClz=OrderTradeService.class )
public class MallOrderTradeServiceImpl extends AbstractOrderTradeService { }

//使用时
Integer bizMode = BizModeContext.getBizMode();
OrderTradeService srv = BizModeRouter.routeBean(bizMode, OrderTradeService.class);

眼尖的小哥哥可能已经发现,要是能再搭配个热加载的bean容器,都可以做成插件了!emmm...那是远景~

如何打通任督二脉?

首先要舌尖抵住上颚,再来三个深呼吸~然后拿起一本《Thinking In Java》或《Core Java》假装在修炼。。。等等。。。什么是任督二脉?

Java老司机都知道,我们通常会把ApplicationContext比作Spring的任督二脉,它贯穿始终,管理着bean的生命周期和传递。

所以电商平台的任督二脉就是BizModeContext啦!它的经脉图大概长这样~

文章出处

所以我们通过下面一二三四,入口处打标、dubbo服务间传递、RocketMQ传递、本机线程池内传递,一步一步打通整个标的透传。

步骤1-打标

aop按包路径切面+注解覆盖,满足你不同的定制需求~于是,在用户点击页面操作的那一刻,每个接口都被打上了“模式标”。

注解打标

@Configuration
public class ControllerConfig {
        @Aspect
        @Component
        public static class CxcAdvice implements BizModeControllerAspect {
                 @Override
                 public Integer getBizMode() {
                         return 300;
                 }
                 @Override
                 @Pointcut("execution(* com.mall.web.controller..*(..))")
                 public void pointcut() {
                 }
        }
}

@Slf4j
@RestController
@MarkBizMode(bizMode = 200)
public class AdminOldController2 {
        @RequestMapping("/admin_anno_byclass")
        public String annoByClass() {
                 log.info("annoByClass got bizmode: " + BizModeContext.getBizMode());
                 return "this is " + this.getClass().toString();
        }
        @RequestMapping("/admin_anno_bymethod")
        @MarkBizMode(bizMode = 100)
        public String annoByMethod() {
                 log.info("annoByMethod got bizmode: " + BizModeContext.getBizMode());
                 return "this is " + this.getClass().toString();
        }
}

步骤2-dubbo服务传递

借助dubbo自带的Filter和RpcContext可以轻松实现。那是因为dubbo的设计中已经充分考虑了。

Filter的使用

filter定义

@Activate(group = Constants.CONSUMER)
public class BizModeDubboConsumerFilter implements Filter { }

filter配置扫描发现: /src/main/resources/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter

filter的装配原理: List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);

dubbo的SPI扩展机制就不具体展开啦~

RpcContext的生命周期

RpcContext -> RpcInvocation ---服务调用--- RpcInvocation -> RpcContext

业务扩展的调用:RpcContext.getContext().setAttachment("bizMode", (bizMode.toString()));

RpcContext.java

//创建一个线程隔离的上下文实例
    private static final InternalThreadLocal<RpcContext> LOCAL = new InternalThreadLocal<RpcContext>() {
        @Override
        protected RpcContext initialValue() {
            return new RpcContext();
        }
    };
    public static RpcContext getContext() {
        return LOCAL.get();
    }

dubbo对attachment的传递:

  • 本机(当前线程)的保存:RpcContext
  • 远程调用的保存和传递:RpcInvocation
  • 将RpcContext存入RpcInvocation:AbstractInvoker
public abstract class AbstractInvoker<T> implements Invoker<T> {
    @Override
    public Result invoke(Invocation inv) throws RpcException {
//节选。。。
        Map<String, String> context = RpcContext.getContext().getAttachments();
        if (context != null) {
          invocation.addAttachmentsIfAbsent(context);
        }
        if (getUrl().getMethodParameter(invocation.getMethodName(), Constants.ASYNC_KEY, false)){
          invocation.setAttachment(Constants.ASYNC_KEY, Boolean.TRUE.toString());
        }
        RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
//节选。。。
// return ...
    }
    protected abstract Result doInvoke(Invocation invocation) throws Throwable;
}
  • 序列化与反序列化:DubboCodec (此处不展开)
  • 从RpcInvocation取出,存入提供方的RpcContext:ContextFilter
@Activate(group = Constants.PROVIDER, order = -10000)
public class ContextFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        Map<String, String> attachments = invocation.getAttachments();
//节选。。。
                RpcContext.getContext().getAttachments().putAll(attachments);
//节选。。。
        try {
            RpcResult result = (RpcResult) invoker.invoke(invocation);
            // pass attachments to result
            result.addAttachments(RpcContext.getServerContext().getAttachments());
            return result;
        } finally {
            RpcContext.removeContext();
            RpcContext.getServerContext().clearAttachments();
        }
    }
}

步骤3-RocketMQ传递

RocketMQ设计时也预留了扩展打标的能力,只需要把模式标存入属性字段,就能跟随MQ把标传递到消费方。

消息体数据结构


org.apache.rocketmq.common.message.Message


private String topic;
private int flag;
private Map<String, String> properties;
private byte[] body;

//填入属性,仅包可见
void putProperty(final String name, final String value);

//填入自定义属性,与其他属性共享map,但对key过滤保留字
public void putUserProperty(final String name, final String value);


org.apache.rocketmq.common.message.MessageExt

是Message的子类


private int queueId;

private int storeSize;

private long queueOffset;
private int sysFlag;
private long bornTimestamp;
private SocketAddress bornHost;

private long storeTimestamp;
private SocketAddress storeHost;
private String msgId;
private long commitLogOffset;
private int bodyCRC;
private int reconsumeTimes;

private long preparedTransactionOffset;

因此,可以在消息体的 Map<String,
String> properties 属性上附加打标信息。

发消息的扩展钩子

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.registerSendMessageHook(SendMessageHook)

收消息的扩展钩子

org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl.registerConsumeMessageHook(ConsumeMessageHook)

但由于收消息是一批一批收的,收到的是消息列表
List<MessageExt>,默认配置下只有一个元素,但允许配置多个,因此不能在这个钩子上做扩展。

因此,对starter做改造,在单个消息消费的位置增加了类似的hook扩展点。

ConsumerHook

public interface ConsumeOneMessageAdvice {
    String hookName();
    void consumeMessageBefore(final MessageExt msg);
    void consumeMessageAfter(final MessageExt msg);
}

步骤4-线程池子线程传递

BizModeContext的原理是用ThreadLocal存储线程范围的上下文,可是实际场景中,总会有些异步和并发的问题,需要使用到线程池。那么问题来了。

父线程context如何传递给子线程

jdk自带InheritableThreadLocal类解决了父子线程传递的问题。

Thread.init()

public class Thread implements Runnable {
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
//节选。。。
        Thread parent = currentThread();
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//节选。。。
    }
}

//子线程创建时会把父线程的ThreadLocalMap复制到子线程中
public class ThreadLocal<T> {
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];
            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
} 

线程池中子线程复用时怎样维护context

但如果使用了线程池,子线程运行完并不会销毁,被另一个父线程复用时不会重新初始化。

这时候我们需要借助一个开源框架 TransmittableThreadLocal  https://github.com/alibaba/transmittable-thread-local

(图片来自官网)

在获取子线程时重新读取父线程的上下文,子线程run()执行结束时清理子线程的上下文。

打通任督二脉后可以练什么武功?

打通模式标的透传后,能怎么使用呢?大家可以尽情发挥下想象力~何时何地只需要 BizModeContext.getBizMode()

  • 日志MDC打标:可以统一给日志记录加入模式标。
  • sql自动追加查询条件:通过mybatis插件扩展或甚至是数据源代理,可以给sql自动追加隔离标条件(虽然具体业务中并不那么好用)。
  • 全链路监控或压测:是的,如果打标的不是bizMode,而是traceId或影子标,就可以通过这个“任督二脉”透传整个系统!
  • 新模式插件化接入:各业务板块逐渐模块化后,可以通过给扩展点开发实现类的形式接入新模式。

远景-多模式插件化部署

我们期望,未来新的业务模式接入,就像安装插件一样无痛无感知。

新模式接入,只需要增加部署新的bizmodeX节点,其他业务不需要回归测试。

某个业务,例如bizmode100,部署重启时,其他业务不受影响。

这还需要一步一步来,目前我们先实现了“任督二脉”的打通,后面的故事,敬请期待哦~

原文地址:https://www.cnblogs.com/syjkfind/p/10897012.html

时间: 2024-07-30 09:44:16

打通电商多模式支持的“任督二脉”的相关文章

网易考拉等巨头的“电商+直播”模式,或是找到了新的突破口?

跨境电商和移动视频直播都是近期颇为热门的话题.跨境电商摊上热点在于一波三折的税改新政:曾经一度的风口产业在历经4月8日的税改后,导致绝大部分的进口商品无法进入中国,很多跨境电商平台面临无货可卖的尴尬境地;而5月24日海关总署的公告,又让整个行业松了一口气,一年的“缓刑期”不仅给足跨境电商平台思考应对的时间,也预告着未来无限的可能性. 后起之秀移动视频直播受到追捧恰恰是跨境电商行业处于“水深火热”期.或许是直播模式极强的传播互动性,广泛的受众和独特的盈利模式,亦或是跨境电商平台想要破局变革,摆脱税

跨境电商技术服务支持

提供跨境电商技术服务支持,海关统一版对接,二合一仓库软件 电子订单数据,支付凭证数据,物流运单数据,物流运单状态数据,清单数据,撤销申请单,退货申请单,入库明细单 软件服务包括接入多电商平台,跨境的进行跨境申报,普贸订单进入仓库出货处理,QQ:196992355,183840232 为跨境电商免去申报技术烦脑.

呆萌短视频app定制开发:抖音已经开启“短视频+电商”新模式已经开始了???

呆萌短视频app定制开发:抖音已经开启"短视频+电商"新模式已经开始了呆萌短视频带您一起分析下从快手到抖音,短短几年,短视频火遍大江南北,短视频已经成为了大众在网上消遣闲暇时光的重要渠道之一,如果说直播还是个风口,那么,呆萌短短视频就处于暴风口上.近年来,快手.秒拍.抖音.火山.梨视频.秒拍等短视频平台发展势头十分迅猛,尤其是字节跳动(今日头条母公司)旗下的抖音,自2016年9月上线至今,月活用户已突破1.2亿,去年末以来长期高居iOS应用排行榜前两名.当下正是直播平台呈爆发式的趋势增

打通任督二脉---原来这就是计算机(持续更新)

笔者是一个计算机爱好者,还记得当初还是个刚上小学的孩子,那时,我第一次听说世界上还有计算机这种东西(那时都叫电脑),当时是90年代,估计那时全村也没有一个人见过电脑,所以电脑就被传的神乎其神:有了电脑,任何犯人都逃不过警察的追捕,因为电脑能算出来犯人在哪里,电脑能一瞬间算出当时觉得比登天还难的5位数的加减乘除,电视里的天气预报也是电脑算的,什么火箭,卫星,原子弹,氢弹都是电脑算出来的......  能想象出我当时对电脑崇拜的心情吗,当时,在我的眼里,电脑不是机器,而是像神一样的虚幻的存在,甚至都

案例干货|用友罗涛:打通产品开发的任督二脉

[精彩预告]用友集团开发管理部总经理罗涛将于5月21日在上海MPD工作坊进行<破解4小时上线传说>的3小时分享.通过一个故事引入互联网+产品开发的迭代思路.价值发掘和发布规划等核心思想和工具,将结组利用小图团队的力量使用影响地图.用户故事地图.无代码验证等演练手段在3个小时的工作坊内快速发布一个产品,带领学员在操作中理解精益和敏捷.文章来源:公众号 :msup(ID:msupclub)关注回复“体验工坊”有惊喜. 导读:在面对需求的变化无常.人员的变动和技术的更新时,对客户价值的识别尤其重要,

K2 BPM_当K2遇上医药,用流程打通企业的任督二脉_业务流程管理系统

据调查,如今仍有60%的医药企业,存在合规经营和利润下降的困扰,在“研”.“产”.“供”.“销”的运营过程中,时时伴随着严苛的管理政策和法规.如何加强企业跨部门.跨组织.跨业务线的执行能力,始终是管理层最关注的话题.而流程,则是打通“任督二脉”,提高执行力的最佳解决办法. 当K2遇上医药 K2在医药行业的核心应用 -医疗推广活动管理 -讲者管理 (含讲者资质审核/讲者在线付款流程) -全面费用管理 -经销商管理 -特价及返利管理 -医疗问询及不良反应记录追踪 -招投标管理 -注册证管理 -销售&

天河微信小程序入门《三》:打通任督二脉,前后台互通

原文链接:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=505&extra=page%3D1 天河君在申请到https证书后就第一时间去部署后台环境,但是发现每次访问https都要带上8443端口实在是很坑爹啊,作为一个强迫症晚期,我要做的自然是不带端口直接访问.打开你tomcat下的conf文件夹,编辑里面的server.xml <Connector port="80" protocol=&quo

任督二脉

RMI:https://www.cnblogs.com/xt0810/p/3640167.html 因为真要有技术深度的话,你可能需要花费至少2年的时间,从底层开始研究一些基础性的技术. 在打通你的底层技术任督二脉之后,再去对常见的开源技术进行深入的源码研究,比如说:dubbo.zookeeper.spring cloud.redis.rocketmq.elasticsearch,等等. 有了几年的积累过后,最后你在面试的时候,技术深度的体现,其实都是厚积薄发的. 原文地址:https://ww

Java电商支付系统手把手实现(二) - 数据库表设计的最佳实践

1 数据库设计 1.1 表关系梳理 仔细思考业务关系,得到如下表关系图 1.2 用户表结构 1.3 分类表结构 id=0为根节点,分类其实是树状结构 1.4 商品表结构 注意价格字段的类型为 decimal 1.5 支付信息表结构 1.6 订单表结构 乍一看,有必要搞这么多种的时间嘛?有以下诸多原因 前端显示需要,那就必须存着呀! 方便定位排查问题,比如某用户投诉某订单一直不发货,肯定就需要时间去定位 方便数据分析,比如需要计算从用户支付到最终发出商品的平均时间 根据订单状态确认相应订单时间 1