异步链路中的数据透传方案

异步链路中的数据透传方案

问题场景

在异步处理链路中,某些原始信息在中间环节会被"有损转换"(如字符串转数字丢失前导零、精度截断、格式归一化等),但下游节点需要使用原始值。如何在不修改中间存储结构的前提下,将原始数据完整传递到下游?

典型链路:

复制代码
HTTP请求(原始数据) → 写入DB(有损) → 发MQ → 异步消费 → 下游需要原始数据

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

方案一:MQ 消息体附带额外字段

思路:在发送 MQ 消息时,将原始数据作为额外字段塞进消息体。

适用场景:发消息的节点就是持有原始数据的节点,且消费端直接从消息体取值。

局限:如果消费端不是从消息体取值,而是从 DB 重新查询对象,附带字段就丢了。

java 复制代码
// === 消息体定义 ===
@Data
public class OrderMessage implements Serializable {
    private Long orderId;
    private Integer userId;       // DB存的是int,有损
    private String userIdRaw;     // 附带原始字符串,无损
}

// === 生产者 ===
@Service
public class OrderProducer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public void sendOrderMessage(Order order, String rawUserId) {
        OrderMessage msg = new OrderMessage();
        msg.setOrderId(order.getId());
        msg.setUserId(order.getUserId());
        msg.setUserIdRaw(rawUserId);  // 携带原始值

        rocketMQTemplate.convertAndSend("order-topic", msg);
    }
}

// === 消费者 ===
@Component
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer-group")
public class OrderConsumer implements RocketMQListener<OrderMessage> {

    @Override
    public void onMessage(OrderMessage msg) {
        // 优先使用原始值
        String userId = msg.getUserIdRaw() != null
            ? msg.getUserIdRaw()
            : String.valueOf(msg.getUserId());
        // 使用 userId 进行后续处理...
    }
}

缺点:如果链路中间有 DB 回查重建对象的环节(如 confirm 时从 DB 查出再发 MQ),附带字段在 DB 中不存在,重建后为 null。


方案二:Redis 旁路缓存

思路:在持有原始数据时写入 Redis(key 关联业务主键),下游需要时从 Redis 读取。

适用场景:链路中有 DB 回查重建对象的环节,MQ 消息体无法透传;数据有明确的生命周期。

java 复制代码
// === 写入端(创建任务时) ===
@Service
public class TaskService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String RAW_DATA_PREFIX = "task:raw_data:";
    private static final long EXPIRE_HOURS = 48;

    @Transactional
    public TaskDTO createTask(String rawCode, String name) {
        Task task = new Task();
        task.setCode(Integer.valueOf(rawCode));  // 有损:007 → 7
        task.setName(name);
        taskMapper.insert(task);

        // 旁路缓存原始值
        redisTemplate.opsForValue().set(
            RAW_DATA_PREFIX + task.getId(),
            rawCode,
            EXPIRE_HOURS, TimeUnit.HOURS
        );

        return toDTO(task);
    }
}

// === 读取端(异步消费时) ===
@Component
public class TaskProcessor {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private TaskMapper taskMapper;

    private static final String RAW_DATA_PREFIX = "task:raw_data:";

    public void process(Long taskId) {
        // 优先从Redis取原始值
        String rawCode = redisTemplate.opsForValue().get(RAW_DATA_PREFIX + taskId);

        if (rawCode == null || rawCode.isEmpty()) {
            // 兜底:从DB取(有损值)
            Task task = taskMapper.selectById(taskId);
            rawCode = String.valueOf(task.getCode());
        }

        // rawCode 在Redis命中时保留了前导零
        downstream.save(rawCode);
    }
}

注意事项

  • TTL 必须覆盖业务完整生命周期(从创建到消费完成)
  • Redis 宕机时有兜底策略(降级为 DB 有损值)
  • Key 命名需要有环境前缀避免多环境冲突

方案三:数据库扩展字段

思路:在现有表中新增一个 varchar 字段专门存储原始值,或使用 JSON 扩展字段。

适用场景:需要持久化、可查询、无过期问题。

3.1 新增独立字段
sql 复制代码
-- 不改原字段类型,新增一列存原始值
ALTER TABLE task ADD COLUMN code_raw VARCHAR(32) DEFAULT NULL COMMENT '原始编码(保留格式)';
@Data
@TableName("task")
public class Task {
    @TableId(type = IdType.AUTO)
    private Long id;

    @TableField("code")
    private Integer code;          // 有损存储

    @TableField("code_raw")
    private String codeRaw;        // 无损存储
}

// 写入
task.setCode(Integer.valueOf(rawCode));
task.setCodeRaw(rawCode);
taskMapper.insert(task);

// 读取(任何环节都能取到原始值)
String originalCode = task.getCodeRaw();
3.2 JSON 扩展字段
sql 复制代码
ALTER TABLE task ADD COLUMN ext_data JSON DEFAULT NULL COMMENT '扩展数据';
@Data
@TableName("task")
public class Task {
    @TableId(type = IdType.AUTO)
    private Long id;

    private Integer code;

    @TableField(typeHandler = JacksonTypeHandler.class)
    private Map<String, Object> extData;
}

// 写入
Map<String, Object> ext = new HashMap<>();
ext.put("codeRaw", rawCode);
ext.put("source", "API");
task.setExtData(ext);

// 读取
String originalCode = (String) task.getExtData().get("codeRaw");

优点 :持久可靠,不依赖 Redis,任何环节回查都能拿到。

缺点:需要 DDL 变更;JSON 字段查询性能差。


方案四:链路上下文传递(Context Propagation)

思路:通过线程上下文或调用链上下文传递,类似 MDC、TraceId 的方式。

适用场景:同步调用链内的多层透传;结合线程池需要额外处理。

java 复制代码
// === 上下文持有者 ===
public class ImportContext {

    private static final ThreadLocal<Map<String, String>> CONTEXT = 
        ThreadLocal.withInitial(HashMap::new);

    public static void set(String key, String value) {
        CONTEXT.get().put(key, value);
    }

    public static String get(String key) {
        return CONTEXT.get().get(key);
    }

    public static void clear() {
        CONTEXT.remove();
    }
}

// === 入口设置 ===
@RestController
public class TaskController {

    @PostMapping("/create")
    public Result create(@RequestParam String userId, @RequestParam MultipartFile file) {
        try {
            ImportContext.set("rawUserId", userId);
            return taskService.createAndProcess(userId, file);
        } finally {
            ImportContext.clear();  // 必须清理
        }
    }
}

// === 下游读取 ===
@Component
public class BatchCreator {

    public void createBatch(Long taskId) {
        String rawUserId = ImportContext.get("rawUserId");
        batch.setUserId(rawUserId != null ? rawUserId : fallbackFromDB(taskId));
        batchMapper.insert(batch);
    }
}

致命局限:ThreadLocal 无法跨线程。如果中间经过 MQ(异步线程消费),上下文就断了。

跨线程改进:结合 TransmittableThreadLocal(阿里 TTL 库)或手动序列化到 MQ Header:

java 复制代码
// === 通过 MQ Message Properties 透传 ===
public void send(Task task, String rawUserId) {
    Message message = new Message("topic", "tag", JsonUtil.toBytes(task));
    message.putUserProperty("rawUserId", rawUserId);  // 塞到消息属性
    producer.send(message);
}

// === 消费端从 Properties 取 ===
public void consume(MessageExt messageExt) {
    String rawUserId = messageExt.getUserProperty("rawUserId");
    Task task = JsonUtil.fromBytes(messageExt.getBody(), Task.class);
    // rawUserId 完整保留
}

方案对比

维度 MQ消息体附带 Redis旁路缓存 DB扩展字段 链路上下文
是否需要改表
持久可靠性 中(消费即丢) 中(有TTL) 低(线程级)
跨MQ透传 ✓(消息体自带) ✓(按key读) ✓(回查可得) 需借助Header
回查场景适用 ✗(回查拿不到)
额外依赖 Redis
实现复杂度

选型决策树

复制代码
原始数据需要在异步链路中透传?
│
├─ 消费端直接从MQ消息体取值?
│   └─ YES → 方案一(MQ消息体附带)
│
├─ 消费端会从DB回查重建对象?
│   ├─ 能改表结构?
│   │   └─ YES → 方案三(DB扩展字段,最可靠)
│   └─ 不能改表?
│       └─ 方案二(Redis旁路缓存)
│
└─ 全程同步调用链(无MQ)?
    └─ 方案四(ThreadLocal上下文)

总结

数据透传的本质是在有损节点旁边开辟一条无损通道。选择哪种方案取决于:

  1. 中间是否有 DB 回查重建(决定 MQ 消息体方案是否可行)
  2. 是否能修改数据库结构(决定是否用 DB 扩展字段)
  3. 数据的生命周期和可靠性要求(决定 Redis TTL 是否够用)
  4. 链路是同步还是异步(决定 ThreadLocal 是否可行)