【页面搭建】模块扩展性业务框架

猪小花1号2018-08-27 10:06

背景

页面搭建系统是一个帮助运营快速搭建多样化的活动落地页的系统,包含运营搭建页面的后台系统和前台落地页呈现系统;为满足运营大小促搭建页面的多样化需求和提升运营搭建活动页的效率,于12月份需要在现有的系统下新增 50 + 模块;

随着模块不断增加,系统以来的服务越来越多(商品、品牌、优惠券、促销、推荐等等),现有各个模块相互耦合,扩展性不强,开发成本较高;为提升开发效率,快速迭代,需要重构梳理模块扩展这部分逻辑

模块扩展性业务框架重新抽象和整理了页面搭建前台页面获取数据的整个流程,按照业务分为业务聚合层、数据适配层和模块业务转换层;各层职责清晰、模块快速迭代、依赖第三方服务共用、性能调优方便

活动落地页请求流程

活动落地页


现状

  • 模块众多,达100+ (商品复杂模块、品牌墙、品牌领券、秒杀模块等等)
  • 各个模块专属业务逻辑
  • 依赖服务众多,大部分考拉服务均有依赖(商品、品牌、优惠券、促销、店铺、推荐等等)
    1. 各个模块依赖不同的服务和独立的业务逻辑,依赖服务复用性不好
    2. 直接依赖第三方服务 DO,第三方服务升级时影响面大
    3. 第三方dubbo服务存在性能瓶颈,一次性批量调用太多响应慢
    4. 目前只有调用商品接口使用了redis 缓存,可以通过disconfig 配置动态开启/关闭 缓存,修改缓存时间

期望效果

  • 模块各种业务相互独立,解耦
  • 依赖外部服务复用
  • 模块扩展方便,不需要了解整体流程,提升开发效率,快速迭代
  • 结构清晰,性能调优方便


业务抽象


  • 数据标识:商品id、品牌id、专辑id、优惠券方案id、落地页url等等
  • 数据适配层:负责通过不同的数据标识调用第三方服务获取数据
    • 支持并行批量获取(不同服务单次阈值可以动态修改)
    • 支持一键开启缓存修改缓存时间:disconfig
    • 支持不同数据VO 的缓存时间修改,随机缓存时间,防止大量失效缓存穿透
  • 模块转换层:暴露数据标识,处理模块数据转换:根据第三方数据和原始素材转换成呈现需要的数据格式(此层无 IO)
    • 暴露数据标识
    • 模块独立业务转换,拼装返回模块呈现需要的数据格式
  • 业务聚合层:负责聚合整体逻辑,并行调用不同服务获取数据


框架实现

核心类图

主要列出服务注册中心和模块注册中心部分类结构

点击此处查看高清类图


扩展方便,快速迭代

  • 依赖服务公用:上图绿色部分的类分别表示批量调用各个服务获取数据,新增一种服务对接只需要新增一个类用@DataProvider 注解标识并实现批量获取的数据的方法
  • 模块快速扩展:上图浅蓝色部分的类分别负责各个模块数据解析,转换参数给前台呈现;(只做业务逻辑处理,无IO操作)


举个栗子

需要新增一个品牌领券,运营录入原始素材:品牌id、优惠券方案id、自定义品牌名称


业务分析

  1. 原始素材中包含的品牌id和优惠券方案id需要分别调用品牌接口和优惠券的接口获取详细数据
  2. 需要拿到品牌和优惠券详情和原始素材转换参数返回给前端呈现


代码示例

先创建业务处理逻辑的类解析模块素材:暴露需要调用外部服务的数据标识(商品id、品牌id、落地页url、优惠券id、店铺等等);通过注解 @ModuleConvert 标识是哪个模块(品牌墙、品牌领券等等)的业务逻辑

@Component
// 通过注解 @ModuleConvert 标识是哪个模块的业务逻辑
@ModuleConvert(moduleMark = BrandCoupon.class)
public class BrandCouponModule extends AbstractIdModulesExecutor {
    @Override
    public Content convert(IdModuleParseDto arg) throws Exception {
        if (arg == null || StringUtils.isEmpty(arg.getContent()) || arg.getData() == null) {
            return null;
        }
        // 业务聚合层批量获取完数据后,把各个模块需要的数据分发到各个模块
        Map<ModuleType, ModulesVo> voMap = arg.getData();
        // 取出品牌信息
        ModulesVo vo = voMap.get(ModuleType.BRAND);
        // 取出优惠券数据
        ModulesVo svo = voMap.get(ModuleType.COUPON);
        if (vo == null) {
            return null;
        }
        Content content = new Content();
        // 品牌信息
        BrandVo brandVo = (BrandVo) vo;
       // 素材信息
        BrandCoupon brandCoupon = JSON.parseObject(arg.getContent(), BrandCoupon.class);

        // =============   一些该模块的业务逻辑,一些不展示的过滤逻辑和参数转换逻辑 ======

        // ============= 完成参数拼装,返回给前台 ======

        content.setData(JSON.toJSONString(brandVo, SerializerFeature.WriteNullStringAsEmpty, SerializerFeature.WriteNullNumberAsZero));
        return content;
    }

    /**
     *  暴露数据标识,分析发现品牌领券模块需要解析品牌id和优惠券方案id
     *  分类暴露品牌id和优惠券id,方便业务聚合层收集id批量从数据适配层获取数据
     */
    @Override
    public Map<ModuleType, Key> getId(String arg) {
        if (StringUtils.isEmpty(arg)) {
            return null;
        }
        BrandCoupon brandCoupon = JSON.parseObject(arg, BrandCoupon.class);
        if (brandCoupon != null) {
            Map<ModuleType, Key> map = Maps.newHashMap();
            // 暴露品牌id
            map.put(ModuleType.BRAND, new Key(null, brandCoupon.getBrandId()));
            // 暴露优惠券方案id
            map.put(ModuleType.COUPON, new Key(null, brandCoupon.getSchemeId()));
            return map;
        }
        return null;
    }
}


找下需要的不同类型的数据标识调用服务是否已经存在:此处假设品牌服务已经对接,但是优惠券服务还未对接,那么需要编写一个批量调用优惠券服务获取优惠券数据的类对接优惠券服务

@Component
/**
 * 调用优惠券服务批量获取优惠券信息
 * 通过注解 @DataProvider 标识是什么类型 {ModuleType} 的服务数据
 * 新增一个 CouponVo (extends ModulesVo)封装需要的参数
 */
@DataProvider(moduleType = ModuleType.COUPON,
        threadPool = ThreadPoolType.COUPONS)
public class FetchCouponsExecutor extends AbstractFetchDataExecutor<CouponVo> {
    @Autowired
    private CouponService couponService;
    private final Log LOGGER = LogFactory.getLog(getClass());

    @Override
    public Map<Long, CouponVo> executeGetData(List<Long> schemaIds) {
        if (CollectionUtils.isEmpty(schemaIds)) {
            LOGGER.info("betch get CouponVo with empty schemaIds:{}");
            return null;
        }
        // 批量获取优惠券数据
        return couponService.batchGetCouponsVo(schemaIds);
    }


易调优

每个服务隔离的配置,可动态修改

disconfig key 后缀 默认值 类型 配置功能说明
page_size 20 Integer 批量接口单次调用第三方 dubbo 接口最大值
cache_switch false Boolean 是否开启缓存
min_expire_time 20 Integer 最小缓存时间
max_expire_time 30 Integer 最大缓存时间


disconfig 前缀 :标识服务的枚举类 ModuleType 的 value,如下:优惠券类 COUPON 的 前缀为 coupon

public enum ModuleType {
    /**
     * {@link FetchBrandsExecutor}
     * 调用品牌接口获取品牌基本信息{@link BrandVo}
     * 包含:品牌基本信息
     */
    BRAND("brand", "品牌类", RecModuleTypeEnum.BRAND.getValue()),
     /**
     * {@link FetchAlbumsExecutor}
     * 专辑基本信息{@link AlbumVo}
     */
    ALBUM("album", "专辑类", RecModuleTypeEnum.ALBUM.getValue()),
    /**
     * {@link FetchCouponsExecutor}
     * 获取优惠券信息{@link CouponVo}
     */
    COUPON("coupon", "优惠券", RecModuleTypeEnum.COUPON.getValue()),
    /**
     * {@link FetchPromotionGoodsExecutor}
     * 拉取促销_商品数据:{@link PromotionGoodsVo}
     * 通用模块:促销类型:秒杀、黑卡专享活动
     */
    PROMOTION_GOODS("promotionGoods", "促销商品类", RecModuleTypeEnum.INVALID.getValue()),
 }


以上述例子为例


  1. 假设优惠券服务批量获取优惠券每次调用量在 30 的时候性能最优,则 coupon_page_size : 30
  2. 假设优惠券服务获取的优惠券可以缓存,调用量大的时候需要开启缓存,则 coupon_cache_switch:true
  3. 默认的缓存时间是在 20s -30s 直接取随机值,可以修改 coupon_min_expire_time 和 coupon_max_expire_time 的值


一些优势

  • 扩展方便,职责清晰:@DataProvider 仅负责数据获取,@ModuleConvert 仅负责模块业务处理(暴露数据标识和模块参数转换)
  • 迪米特法则:统一的并行分页获取数据和缓存:@DataProvider 不需要为每个服务实现缓存,只需要 disconfig 动态修改 key 的值即可完成调优
  • 数据适配层提供统一获取第三方服务的接口,调用不依赖具体的 @DataProvider 类;返回数据封装VO,不强依赖第三方服务,方便适配服务升级
  • 使用了一些设计模式
    1. 代理模式(调度中心代理具体的 Executor,不直接依赖具体的 Executor,方便服务下架和升级)
    2. 模板模式(Executor 一些抽象类封装公共逻辑,暴露一些扩展点给子类:责任单一;比如:批量调用第三方服务做分批处理逻辑和缓存逻辑,并支持 disconfig 动态修改)
    3. 前端控制器模式(类似观察者和监听者,注册中心的实现,让每个 Executor 只需要实现自己关心的部分逻辑,满足开闭原则OCP)


框架效果

  • 效率提升:完成模块扩展性业务框架后,使得开发效率提升 30% +,12月份 50+ 模块迁移时借调的其他组的外接人员在完全不了解业务的情况下也能快速完成模块开发工作
  • 调优方便 : 新增模块压测过程中,发现性能问题时通过修改 disconfig 参数对变化少的服务开启缓存、调整不同服务的调用比例(通过修改服务调用阈值)
  • 结构清晰,维护方便:业务层次清晰,借调人员开发的模块基本不需要交接就能被项目组其他同学接手维护
  • 依赖外部服务复用:数据适配层提供统一方法调用外部服务,搭建呈现中所有需要获取第三方服务数据的地方直接调用数据适配层,复用性高并且方便统一调优
  • 第三方服务升级简单:依赖的商品服务从 goodsample 升级为 goods-front 时仅新增一个新商品服务接入的 @DataProvoder 类封装需要的参数即可,影响面小



网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者柯博文授权发布。