md_files/项目/拼团交易系统.md

38 KiB
Raw Blame History

拼团交易系统

系统设计

功能流程

image-20250619190759804

库表设计

image-20250624134726763

  • 首先,站在运营的角度,要为这次拼团配置对应的拼团活动。那么就会涉及到;给哪个渠道的什么商品ID配置拼团这样用户在进入商品页就可以看到带有拼团商品的信息了。之后要考虑这个拼团的商品所提供的规则信息,包括:折扣、起止时间、人数等。还要拿到折扣的一个试算金额。这个试算出来的金额,就是告诉用户,通过拼团可以拿到的最低价格。
  • 之后,站在用户的角度,是参与拼团。首次发起一个拼团或者参与已存在的拼团进行数据的记录,达成拼团约定拼团人数后,开始进行通知。这个通知的设计站在平台角度可以提供回调,那么任何的系统也就都可以接入了。
  • 另外,为了支持拼团库表,需要先根据业务规则把符合条件的用户 ID 写入 Redis并为这批用户打上可配置的人群标签。创建拼团活动时,只需关联对应标签,即可让活动自动面向这部分用户生效,实现精准运营和差异化折扣。
  • 那么,拼团活动表,为什么会把折扣拆分出来呢。因为这里的折扣可能有多种迭代到一个拼团上。比如给一个商品添加了直减10元的优惠又对符合的人群id的用户额外打9折这样就有了2个折扣迭代。所以拆分出来会更好维护。这是对常变的元素和稳定的元素进行设计的思考。

(一)拼团配置表

group_buy_activity 拼团活动

字段名 说明
id 自增ID
activity_id 活动ID
source 来源
channel 渠道
goods_id 商品ID
discount_id 折扣ID
group_type 成团方式【0自动成团到时间后自动成团、1达成目标成团】
take_limit_count 拼团次数限制
target 达成目标3人单、5人单
valid_time 拼单时长20分钟未完成拼团则=》自动成功or失败
status 活动状态 (活动是否有效,运营可临时设置为失效)
start_time 活动开始时间
end_time 活动结束时间
tag_id 人群标签规则标识
tag_scope 人群标签规则范围【多选;可见、参与】
create_time 创建时间
update_time 更新时间

group_buy_discount 折扣配置

字段名 说明
id 自增ID
discount_id 折扣ID
discount_name 折扣标题
discount_desc 折扣描述
discount_type 类型【base、tag】
market_plan 营销优惠计划【直减、满减、N元购】
market_expr 营销优惠表达式
tag_id 人群标签,特定优惠限定
create_time 创建时间
update_time 更新时间

crowd_tags 人群标签

字段名 说明
id 自增ID
tag_id 标签ID
tag_name 标签名称
tag_desc 标签描述
statistics 人群标签统计量 200\10万\100万
create_time 创建时间
update_time 更新时间

crowd_tags_detail 人群标签明细(写入缓存)

字段名 说明
id 自增ID
tag_id 标签ID
user_id 用户ID
create_time 创建时间
update_time 更新时间

crowd_tags_job 人群标签任务

字段名 说明
id 自增ID
tag_id 标签ID
batch_id 批次ID
tag_type 标签类型【参与量、消费金额】
tag_rule 标签规则【限定参与N次】
stat_start_time 统计开始时间
stat_end_time 统计结束时间
status 计划、重置、完成
create_time 创建时间
update_time 更新时间
  • 拼团活动表:设定了拼团的成团规则,人群标签的使用可以限定哪些人可见,哪些人可参与。
  • 折扣配置表:拆分出拼团优惠到一个新的表进行多条配置。如果折扣还有更多的复杂规则,则可以配置新的折扣规则表进行处理。
  • 人群标签表专门来做人群设计记录的这3张表就是为了把符合规则的人群ID也就是用户ID全部跑任务到一个记录下进行使用。比如黑玫瑰人群、高净值人群、拼团履约率90%以上的人群等。

(二)参与拼团表

group_buy_account 拼团账户

字段名 说明
id 自增ID
user_id 用户ID
activity_id 活动ID
take_limit_count 拼团次数限制
take_limit_count_used 拼团次数消耗
create_time 创建时间
update_time 更新时间

group_buy_order 用户拼单

一条记录 = 一个拼团团队team_id 唯一)

字段名 说明
id 自增ID
team_id 拼单组队ID
activity_id 活动ID
source 渠道
channel 来源
original_price 原始价格
deduction_price 折扣金额
pay_price 支付价格
target_count 目标数量
complete_count 完成数量
status 状态0-拼单中、1-完成、2-失败)
create_time 创建时间
update_time 更新时间

group_buy_order_list 用户拼单明细

一条记录 = 某用户在该团队里锁的一笔单

字段名 说明
id 自增ID
user_id 用户ID
team_id 拼单组队ID
order_id 订单ID
activity_id 活动ID
start_time 活动开始时间
end_time 活动结束时间
goods_id 商品ID
source 渠道
channel 来源
original_price 原始价格
deduction_price 折扣金额
status 状态0 初始锁定、1 消费完成
out_trade_no 外部交易单号(确保外部调用唯一幂等)
create_time 创建时间
update_time 更新时间

notify_task 回调任务

字段名 说明
id 自增ID
activity_id 活动ID
order_id 拼单ID
notify_url 回调接口
notify_count 回调次数3-5次
notify_status 回调状态【初始、完成、重试、失败】
parameter_json 参数对象
create_time 创建时间
update_time 更新时间
  • 拼团账户表:记录用户的拼团参与数据,一个是为了限制用户的参与拼团次数,另外是为了人群标签任务统计数据。
  • 用户拼单表当有用户发起首次拼单的时候产生拼单id并记录所需成团的拼单记录另外是写上拼团的状态、唯一索引、回调接口等。这样拼团完成就可以回调对接的平台通知完成了。【微信支付也是这样的设计回调支付结果这样的设计可以方便平台化对接】当再有用户参与后则写入用户拼单明细表。直至达成拼团。
  • 回调任务表:当拼团完成后,要做回调处理。但可能会有失败,所以加入任务的方式进行补偿。如果仍然失败,则需要对接的平台,自己查询拼团结果。

架构设计

MVC架构

image-20250624143253403

DDD架构

image-20250624143304200

价格试算

@Service
@RequiredArgsConstructor
public class IndexGroupBuyMarketServiceImpl implements IIndexGroupBuyMarketService {

    private final DefaultActivityStrategyFactory defaultActivityStrategyFactory;

    @Override
    public TrialBalanceEntity indexMarketTrial(MarketProductEntity marketProductEntity) throws Exception {

        StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> strategyHandler = defaultActivityStrategyFactory.strategyHandler();

        TrialBalanceEntity trialBalanceEntity = strategyHandler.apply(marketProductEntity, new DefaultActivityStrategyFactory.DynamicContext());

        return trialBalanceEntity;
    }

}
IndexGroupBuyMarketService
      │
      │ indexMarketTrial()
      ▼
DefaultActivityStrategyFactory
      │  (return rootNode)
      ▼
RootNode.apply()
      │ doApply()  (执行)
      │ router()   路由到下一node
      ▼
SwitchNode.apply()
      │ ...
      ▼
...  (可能还有其他节点)
      ▼
EndNode.apply()   → 组装结果并返回 TrialBalanceEntity
      ▲
      └────────── 最终一路向上 return

IndexGroupBuyMarketService 是领域服务,整个价格试算的入口

DefaultActivityStrategyFactory 帮你拿到 根节点,真正的“工厂”工作(多线程预处理、分支路由)都在各 Node 里完成。

DynamicContext 是一次性创建的共享上下文:谁需要谁就往里放

人群标签数据采集

步骤 目的 说明
1. 记录日志 标明本次批次任务的开始 方便后续排查、链路追踪
2. 读取批次配置 拿到该批次统计范围、规则、时间窗 若返回 null 通常代表批次号错误或已被清理
3. 采集候选用户 从业务数仓/模型结果里拉取符合条件的用户 ID 列表 真实场景中会:• 调 REST / RPC 拿画像• 或扫离线结果表• 或读 Kafka 流
4. 双写标签明细 将每个用户与标签的关系永久化 & 提供实时校验能力 方法内部两件事:• 插入 crowd_tags_detail 表•
在 Redis BitMap 中把该用户对应位设为 1幂等处理冲突
5. 更新统计量 维护标签当前命中人数,用于运营看板 这里简单按“新增条数”累加,也可改为重新 count(*) 全量回填
6. 结束 方法返回 void 如果过程抛异常,调度系统可重试/报警

一句话总结 这是一个被定时器或消息触发的离线批量打标签任务 拉取任务规则 → (离线)筛出符合条件的用户 → 写库 + 写 Redis 位图 → 更新命中人数。 之后业务系统就能用位图做到毫秒级 isUserInTag(userId, tagId) 判断,实现精准运营投放。

Bitmap位图

概念

  • Bitmap 又称 Bitset是一种用位bit来表示状态的数据结构。
  • 它把一个大的“布尔数组”压缩到最小空间:每个元素只占 1 位,要么 0False、要么 1True

为什么用 Bitmap

  • 超高空间效率1000 万个用户,只需要约 10 MB1000 万 / 8
  • 超快操作:检查某个索引位是否为 1、计数所有“1”的个数BITCOUNT、找出第一个“1”的位置BITPOS都是 O(1) 或者极快的位运算。

典型场景

  • 用户标签 / 权限判断:把符合某个条件的用户的索引位置设置为 1以后实时判断“用户 X 是否在标签 A 中?”就只需读一个 bit。
  • 海量去重 / 布隆过滤器在超大流量场景下判断“URL 是否已访问过”、“手机号是否已注册”等。
  • 统计分析快速统计某个条件下有多少个用户对象符合BITCOUNT

拼团交易锁单

image-20250630124304410

下单到支付中间有一个流程,即锁单,比如淘宝京东中,在这个环节(限定时间内)选择使用优惠券、京豆等,可以得到优惠价,再进行支付;拼团场景同理,先加入拼团,进行锁单,然后优惠试算,最后才付款。

收获

实体对象

实体是指具有唯一标识的业务对象。

在 DDD 分层里,Domain Entity ≠ 数据库 PO。 在 edu.whut.domain.*.model.entity 包下放的是纯粹的业务对象,它们只表达业务语义(团队 ID、活动时间、优惠金额……对「数据持久化细节」保持无感知。因此它们看起来“字段不全”是正常的:

  • 它们不会带 @TableName / @TableId 等 MyBatis-Plus 注解;
  • 也不会出现数据库的技术字段(idcreate_timeupdate_timestatus 等);
  • 只保留聚合根真正需要的业务属性与行为。
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PayActivityEntity {

    /** 拼单组队ID */
    private String teamId;
    /** 活动ID */
    private Long activityId;
    /** 活动名称 */
    private String activityName;
    /** 拼团开始时间 */
    private Date startTime;
    /** 拼团结束时间 */
    private Date endTime;
    /** 目标数量 */
    private Integer targetCount;

}

这个也是实体对象因为多个字段的组合teamId和activityId能唯一标识这个实体。

模板方法

核心思想 在抽象父类中定义算法骨架(固定执行顺序),把某些可变步骤留给子类重写;调用方只用模板方法,保证流程一致。

 Client  ───▶  AbstractClass
                ├─ templateMethod()  ←—— 固定流程
                │    step1()
                │    step2()         ←—— 抽象,可变
                │    step3()
                └─ hookMethod()      ←—— 可选覆盖
                       ▲
                       │ extends
            ┌──────────┴──────────┐
            │ ConcreteClassA/B…   │

示例:

// 1. 抽象模板
public abstract class AbstractDialog {

    // 模板方法:固定调用顺序,设为 final 防止子类改流程
    public final void show() {
        initLayout();
        bindEvent();
        beforeDisplay();       // 钩子,可选
        display();
        afterDisplay();        // 钩子,可选
    }

    // 具体公共步骤
    private void initLayout() {
        System.out.println("加载通用布局文件");
    }

    // 需要子类实现的抽象步骤
    protected abstract void bindEvent();

    // 钩子方法,默认空实现
    protected void beforeDisplay() {}
    protected void afterDisplay() {}

    private void display() {
        System.out.println("弹出对话框");
    }
}

// 2. 子类:登录对话框
public class LoginDialog extends AbstractDialog {
    @Override
    protected void bindEvent() {
        System.out.println("绑定登录按钮事件");
    }
    @Override
    protected void afterDisplay() {
        System.out.println("focus 到用户名输入框");
    }
}

// 3. 调用
public class Demo {
    public static void main(String[] args) {
        AbstractDialog dialog = new LoginDialog();
        dialog.show();
        /* 输出:
           加载通用布局文件
           绑定登录按钮事件
           弹出对话框
           focus 到用户名输入框
         */
    }
}

要点

  • 复用公共流程initLayout()display() 写一次即可。
  • 限制流程顺序show() 定为 final,防止子类乱改步骤。
  • 钩子方法:子类可选择性覆盖(如 beforeDisplay)。

责任链

应用场景:日志系统、审批流程、权限校验——任何需要将请求按阶段传递、并由某一环节决定是否继续或终止处理的地方,都非常适合职责链模式。

单例链

典型的责任链模式要点:

  • 解耦请求发送者和处理者:调用者只持有链头,不关心中间环节。
  • 动态组装:通过 appendNext 可以灵活地增加、删除或重排链上的节点。
  • 可扩展:新增处理逻辑只需继承 AbstractLogicLink 并实现 apply,不用改动已有代码。

接口定义:ILogicChainArmory<T, D, R> 提供添加节点方法和获取节点

//定义了责任链的组装接口:
public interface ILogicChainArmory<T, D, R> {

    ILogicLink<T, D, R> next();	   //在当前节点中获取下一个节点

    ILogicLink<T, D, R> appendNext(ILogicLink<T, D, R> next); //把下一个处理节点挂接上来

}

ILogicLink<T, D, R> 继承自 ILogicChainArmory<T, D, R>,并额外声明了核心方法 apply

public interface ILogicLink<T, D, R> extends ILogicChainArmory<T, D, R> {

    R apply(T requestParameter, D dynamicContext) throws Exception;  //处理请求

}

抽象基类:AbstractLogicLink

public abstract class AbstractLogicLink<T, D, R> implements ILogicLink<T, D, R> {

    private ILogicLink<T, D, R> next;

    @Override
    public ILogicLink<T, D, R> next() {
        return next;
    }

    @Override
    public ILogicLink<T, D, R> appendNext(ILogicLink<T, D, R> next) {
        this.next = next;
        return next;
    }

    protected R next(T requestParameter, D dynamicContext) throws Exception {
        return next.apply(requestParameter, dynamicContext);   //交给下一节点处理
    }

}

子类只需继承它,重写 apply(...),在合适的条件下要么直接处理并返回,要么调用 next(requestParameter, dynamicContext) 继续传递。

使用示例:

public class AuthLink extends AbstractLogicLink<Request, Context, Response> {
    @Override
    public Response apply(Request req, Context ctx) throws Exception {
        if (!ctx.isAuthenticated()) {
            throw new UnauthorizedException();
        }
        // 认证通过,继续下一个环节
        return next(req, ctx);
    }
}

public class LoggingLink extends AbstractLogicLink<Request, Context, Response> {
    @Override
    public Response apply(Request req, Context ctx) throws Exception {
        System.out.println("Request received: " + req);
        Response resp = next(req, ctx);
        System.out.println("Response sent: " + resp);
        return resp;
    }
}

// 组装责任链 放工厂类factory中实现
ILogicLink<Request, Context, Response> chain =
    new AuthLink()
        .appendNext(new LoggingLink())
        .appendNext(new BusinessLogicLink());

//客户端使用
Request req = new Request(...);
Context ctx = new Context(...);
Response resp = chain.apply(req, ctx);

示例图:

AuthLink.apply
   └─▶ LoggingLink.apply
          └─▶ BusinessLogicLink.apply
                 └─▶ 返回 Response

这种模式链上的每个节点都手动 next()到下一节点。

多例链

/**
 * 通用逻辑处理器接口 —— 责任链中的「节点」要实现的核心契约。
 */
public interface ILogicHandler<T, D, R> {

    /**
     * 默认的 next占位实现方便节点若不需要向后传递时直接返回 null。
     */
    default R next(T requestParameter, D dynamicContext) {
        return null;
    }

    /**
     * 节点的核心处理方法。
     */
    R apply(T requestParameter, D dynamicContext) throws Exception;

}

/**
 * 业务链路容器 —— 双向链表实现,同时实现 ILogicHandler从而可以被当作单个节点使用。
 */
public class BusinessLinkedList<T, D, R> extends LinkedList<ILogicHandler<T, D, R>> implements ILogicHandler<T, D, R>{

    public BusinessLinkedList(String name) {
        super(name);
    }

    /**
     * BusinessLinkedList是头节点它的apply方法就是循环调用后面的节点直至返回。
     * 遍历并执行链路。
     */
    @Override
    public R apply(T requestParameter, D dynamicContext) throws Exception {
        Node<ILogicHandler<T, D, R>> current = this.first;
        // 顺序执行,直到链尾或返回结果
        while (current != null) {
            ILogicHandler<T, D, R> handler = current.item;
            R result = handler.apply(requestParameter, dynamicContext);
            if (result != null) {
                // 节点命中,立即返回
                return result;
            }
            //result==null则交给那一节点继续处理
            current = current.next;
        }
        // 全链未命中
        return null;
    }
}
/**
 * 链路装配工厂 —— 负责把一组 ILogicHandler 顺序注册到 BusinessLinkedList 中。
 */
public class LinkArmory<T, D, R> {

    private final BusinessLinkedList<T, D, R> logicLink;

    /**
     * @param linkName      链路名称,便于日志排查
     * @param logicHandlers 节点列表,按传入顺序链接
     */
    @SafeVarargs
    public LinkArmory(String linkName, ILogicHandler<T, D, R>... logicHandlers) {
        logicLink = new BusinessLinkedList<>(linkName);
        for (ILogicHandler<T, D, R> logicHandler: logicHandlers){
            logicLink.add(logicHandler);
        }
    }

    /** 返回组装完成的链路 */
    public BusinessLinkedList<T, D, R> getLogicLink() {
        return logicLink;
    }

}

//工厂类可以定义多条责任链每条有自己的Bean名称区分。
@Bean("tradeRuleFilter")
public BusinessLinkedList<TradeRuleCommandEntity, DynamicContext, TradeRuleFilterBackEntity> tradeRuleFilter(ActivityUsabilityRuleFilter activityUsabilityRuleFilter, UserTakeLimitRuleFilter userTakeLimitRuleFilter) {
     // 1. 组装链
     LinkArmory<TradeRuleCommandEntity, DynamicContext, TradeRuleFilterBackEntity> linkArmory =
                new LinkArmory<>("交易规则过滤链", activityUsabilityRuleFilter, userTakeLimitRuleFilter);

     // 2. 返回链容器(即可作为责任链使用)
     return linkArmory.getLogicLink();
}

示例图:

BusinessLinkedList.apply  ←─ 只有这一层在栈里
while 循环:
   ├─▶ 调用 ActivityUsability.apply → 返回 null → 继续
   ├─▶ 调用 UserTakeLimit.apply   → 返回 null → 继续
   └─▶ 调用 ...                  → 返回 Result → break

链头拿着“游标”一个个跑,节点只告诉“命中 / 未命中”。

规则树流程

8d9ca81791b613eb7ff06b096a3d7c4a

整体分层思路

分层 作用 关键对象
通用模板层 抽象出与具体业务无关的「规则树」骨架,解决 如何找到并执行策略 的共性问题 StrategyMapperStrategyHandlerAbstractStrategyRouter<T,D,R>
业务装配层 基于模板,自由拼装出 一棵 贴合业务流程的策略树 RootNode / SwitchRoot / MarketNode / EndNode …
对外暴露层 通过 工厂 + 服务支持类 将整棵树封装成一个可直接调用的 StrategyHandler,并交给 Spring 整体托管 DefaultActivityStrategyFactoryAbstractGroupBuyMarketSupport

通用模板层:规则树的“骨架”

角色 职责 关系
StrategyMapper 映射器:依据 requestParameter + dynamicContext 选出 下一个 策略节点 AbstractStrategyRouter 调用
StrategyHandler 处理器:真正执行业务逻辑;apply 结束后可返回结果或继续路由 节点本身 / 路由器本身都是它的实现
AbstractStrategyRouter<T,D,R> 路由模板:① 调用 get(...) 找到合适的 StrategyHandler;② 调用该 handler 的 apply(...);③ 若未命中则走 defaultStrategyHandler 同时实现 StrategyMapperStrategyHandler,但自身保持 抽象,把细节延迟到子类

业务装配层:一棵可编排的策略树

RootNode  ->  SwitchRoot  ->  MarketNode  ->  EndNode
                          ↘︎  OtherNode ...
  • 每个节点

    继承 AbstractStrategyRouter

    • 实现 get(...):决定当前节点的下一跳是哪一个节点
    • 实现 apply(...):实现节点自身应做的业务动作(或继续下钻)
  • 组合方式

    比责任链更灵活:

    • 一个节点既可以“继续路由”也可以“自己处理完直接返回”
    • 可以随时插拔 / 替换子节点,形成多分支、循环、早停等复杂流转

对外暴露层:工厂 + 服务支持类

组件 主要职责
DefaultActivityStrategyFactory (@Service) 工厂1. 在 Spring 启动时注入根节点 RootNode2. 暴露统一入口 strategyHandler() → 返回整个策略树顶点(一个 StrategyHandler 实例)
AbstractGroupBuyMarketSupport 业务服务基类:封装拼团场景下共用的查询、工具方法;供每个节点继承使用

这样,调用方只需

TrialBalanceEntity result =
    factory.strategyHandler().apply(product, new DynamicContext(vo1, vo2));

就能驱动整棵策略树,而完全不用关心节点搭建、依赖注入等细节。

策略模式

核心思想 把可互换的算法/行为抽成独立策略类,运行时由“上下文”对象选择合适的策略;对调用方来说,只关心统一接口,而非具体实现。

┌───────────────┐
│   Client      │
└─────▲─────────┘
      │ has-a
┌─────┴─────────┐                 implements
│  Context      │────────────┐  ┌──────────────┐
│ (使用者)      │ strategy    └─▶│ Strategy A   │
└───────────────┘                ├──────────────┤
                                 │ Strategy B   │
                                 └──────────────┘

集合自动注入

常见于策略/工厂/插件场景。

@Autowired       
private Map<String, IDiscountCalculateService> discountCalculateServiceMap;

字段类型Map<String, IDiscountCalculateService>

  • key—— Bean 的名字
    • 默认是类名首字母小写 (mjCalculateService)
    • 或者你在实现类上显式写的 @Service("MJ")
  • value —— 那个实现类对应的实例
  • Spring 机制
    1. 启动时扫描所有实现 IDiscountCalculateService 的 Bean。
    2. 把它们按 “BeanName → Bean 实例” 的映射注入到这张 Map 里。
    3. 你一次性就拿到了“策略字典”。

示例:

@Service("MJ")                  // ★ 关键Bean 名即策略键
public class MJCalculateService extends IDiscountCalculateService {

    @Override
    protected BigDecimal Calculate(String userId, BigDecimal originalPrice,
                                     GroupBuyActivityDiscountVO.GroupBuyDiscount groupBuyDiscount) {
        //忽略实现细节
}

@Component
@RequiredArgsConstructor          // 构造器注入更推荐
public class DiscountContext {

    private final Map<String, IDiscountCalculateService> discountServiceMap;

    public BigDecimal calc(String strategyKey,
                           String userId,
                           BigDecimal originalPrice,
                           GroupBuyActivityDiscountVO.GroupBuyDiscount plan) {
 	    //strategyKey可以是"MJ" ..
        IDiscountCalculateService strategy = discountServiceMap.get(strategyKey); 
        if (strategy == null) {
            throw new IllegalArgumentException("无匹配折扣类型: " + strategyKey);
        }
        return strategy.calculate(userId, originalPrice, plan);
    }
}

多线程异步调用

如果某任务比较耗时(如加载大量数据),可以考虑开多线程异步调用。

// Runnable  ➞ 只能 run(),没有返回值
public interface Runnable {
    void run();
}

// Callable<V> ➞ call() 能返回 V也能抛检查型异常
public interface Callable<V> {
    V call() throws Exception;
}
public class MyTask implements Callable<String> {
    private final String name;
    public MyTask(String name) {
        this.name = name;
    }
    @Override
    public String call() throws Exception {
        // 模拟耗时操作
        TimeUnit.MILLISECONDS.sleep(300);
        return "任务[" + name + "]的执行结果";
    }
}
public class SimpleAsyncDemo {
    public static void main(String[] args) {
        // 创建大小为 2 的线程池
        ExecutorService pool = Executors.newFixedThreadPool(2);

        try {
            // 构造两个任务
            MyTask task1 = new MyTask("A");
            MyTask task2 = new MyTask("B");

            // 用 FutureTask 包装 Callable
            FutureTask<String> future1 = new FutureTask<>(task1);
            FutureTask<String> future2 = new FutureTask<>(task2);

            // 提交给线程池异步执行
            pool.execute(future1);
            pool.execute(future2);

            // 主线程可以先做别的事…
            System.out.println("主线程正在做其他事情…");

            // 在需要的时候再获取结果(可加超时)
            String result1 = future1.get(1, TimeUnit.SECONDS); //设置超时时间1秒
            String result2 = future2.get();   //无超时时间

            System.out.println("拿到结果1 → " + result1);
            System.out.println("拿到结果2 → " + result2);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            System.err.println("任务执行中出错: " + e.getCause());
        } catch (TimeoutException e) {
            System.err.println("等待结果超时");
        } finally {
            pool.shutdown();
        }
    }
}

动态配置(热更新)

注解标记@DCCValue("key:default") 标注需要动态注入的字段,指定对应的 Redis Key带前缀及默认值。

// 标记要动态注入的字段
@Retention(RUNTIME) @Target(FIELD)
public @interface DCCValue {
  String value(); // "key:default"
}
// 业务使用示例
@Service
public class MyFeature {
  @DCCValue("myFlag:0")
  private String myFlag;
  public boolean enabled() { return "1".equals(myFlag); }
}

启动时注入 实现一个 BeanPostProcessor,在每个 Spring Bean 初始化后:

  • 扫描带 @DCCValue 的字段;
  • 拼出完整 Redis Keydcc_prefix_key),若不存在则写入默认值,否则读最新值;
  • 反射把值注入到该 Bean 的私有字段
  • (redisKey → Bean 实例) 记录到内存映射,用于后续热更新。
@Override
public Object postProcessAfterInitialization(Object bean, String name) {
    Class<?> cls = AopUtils.isAopProxy(bean)
      ? AopUtils.getTargetClass(bean)
      : bean.getClass();
    for (Field f : cls.getDeclaredFields()) {
      DCCValue dv = f.getAnnotation(DCCValue.class);
      if (dv==null) continue;
      String[] p = dv.value().split(":");
      String key = PREFIX + p[0], defaultValue = p[1];
      RBucket<String> bucket = redis.getBucket(key);
      String val = bucket.isExists() ? bucket.get() : defaultValue;
      bucket.trySet(defaultValue);           //同步redis内容
      injectField(bean, f, val);    //反射注入
      beans.put(key, bean);
    }
    return bean;
  }

运行时热更新

  • 在同一个组件里,订阅一个 Redis Topic频道比如 "dcc_update"

  • 外部调用发布接口 PUBLISH dcc_update "key,newValue"

    //更新配置
    @GetMapping("/dcc/update")
    public void update(@RequestParam String key, @RequestParam String value) {
      dccTopic().publish(key + "," + value);
    }
    
  • 订阅者收到后:

    1. 同步把新值写回 Redis
    2. 从映射里取出对应 Bean反射更新它的字段。
// 发布/订阅频道
  @Bean
  public RTopic dccTopic() {
    RTopic t = redis.getTopic("dcc_update");
    t.addListener(String.class, (c,msg)->{
      String[] a = msg.split(",");
      String key = PREFIX + a[0], val = a[1];
      RBucket<String> bucket = redis.getBucket(key);
      if (!bucket.isExists()) return;
      bucket.set(val);
      Object bean = beans.get(key);
      if (bean!=null) injectField(bean, a[0], val);
    });
    return t;
  }

HTTP客戶端框架

引入依赖

<dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>okhttp-sse</artifactId>
</dependency>

让Spring 管理 Http客户端

  • 写配置类

    @Configuration
    public class OKHttpClientConfig {
    
        @Bean
        public OkHttpClient httpClient() {
            return new OkHttpClient();
        }
    }
    
  • 在需要使用的地方注入

    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class HttpService {
    
        private final OkHttpClient okHttpClient;
    
        /**
         * 发送 JSON POST 请求并返回响应内容
         *
         * @param apiUrl       接口地址
         * @param jsonPayload  请求体 JSON 字符串
         */
        public String postJson(String apiUrl, String jsonPayload) throws IOException {
            //1.构建参数
            MediaType mediaType = MediaType.get("application/json; charset=utf-8");
            RequestBody body = RequestBody.create(jsonPayload, mediaType);
            Request request = new Request.Builder()
                .url(apiUrl)
                .post(body)
                .addHeader("Content-Type", "application/json")
                .build();
    		//2.调用接口
            try (Response response = okHttpClient.newCall(request).execute()) {
                if (!response.isSuccessful()) {
                    log.error("HTTP 请求失败URL{},状态码:{}", apiUrl, response.code());
                    throw new IOException("Unexpected HTTP code " + response.code());
                }
                ResponseBody responseBody = response.body();
                return responseBody != null ? responseBody.string() : "";
            } catch (IOException e) {
                log.error("调用 HTTP 接口异常:{}", apiUrl, e);
                throw e;
            }
        }
    }
    
  • 优点:

    单例复用,性能更优

    • Spring 默认将 Bean 作为单例管理,整个应用只创建一次 OkHttpClient
    • 内部的连接池、线程池、缓存等资源可以被复用,避免频繁创建、销毁带来的开销。

    统一配置,易于维护

    • 超时、拦截器、连接池、SSL、日志等配置集中在一个地方改动一次全局生效。
    • 避免在代码各处手动 new OkHttpClient()、重复配置。