soda docs v17.0.1

配置项

soda 的配置面很小:一个总线类型开关 + 一组 Redis 参数 + 一个线程池。这一页讲怎么配、配在哪、以及三个最容易踩的坑。逐键的完整清单见 配置参考。

配置写在哪儿

soda 用的是 Spring Boot 的标准机制:application.yml(或 .properties),加 profile 做环境隔离。

yaml
# application.yml —— 所有环境共用
soda:
  event:
    bus-type: simple          # 显式写,别依赖默认值

scoda:
  ddd:
    cqrs:
      async:
        core-pool-size: 8
yaml
# application-prod.yml —— 生产覆盖
soda:
  event:
    bus-type: redis
    redis:
      host: ${SODA_REDIS_HOST}
      password: ${SODA_REDIS_PASSWORD}
重要

凭据走环境变量,不要写进 yml。 soda.event.redis.password 这类值用 ${ENV_VAR} 占位,真实值放部署环境的变量里。yml 是要入库的,入库文件零明文密钥。

最小可用配置

分三种情况,按你需要选一种:

单机 / 开发 / 测试:进程内总线

yaml
soda:
  event:
    bus-type: simple

完事。不需要 Redis,事件在同一个 JVM 内同步派发。

生产 / 多实例:Redis 事件总线

yaml
soda:
  event:
    bus-type: redis
    redis:
      host: ${SODA_REDIS_HOST}
      port: 6379
      password: ${SODA_REDIS_PASSWORD}
      database: 0
      topic: soda-events

前提:

  • soda-event-redis 依赖已引入(见 安装)
  • 工程里有可用的 RedisTemplate / StringRedisTemplate(Spring Data Redis 自动配置会提供)

什么都不配

能跑,但不建议。

不配 bus-type 时,SimpleEventBusAutoConfiguration 的 matchIfMissing = true 会让进程内总线生效。功能是对的,但意图不明确 —— 半年后没人知道当初是想用 Simple 还是漏配了。

注意

不要写 bus-type: spring。 17.0.1 已经删掉了 soda-event-spring 模块,但 EventProperties 的默认值仍是 "spring"。显式配成 spring 的后果是:一个 EventBus Bean 都不会创建,RepositoryEventAutoConfiguration(依赖 @ConditionalOnBean(EventBus.class))跟着不注册,而 SpringBusAutoConfiguration 里的 CqrsAroundHandler 强依赖 EventBus —— 应用在启动阶段就抛 NoSuchBeanDefinitionException,不是运行期静默丢事件。

老工程从 v16 及更早版本迁过来时,这一条最容易被带进生产。

Redis 总线要调的几项

默认值能跑通开发环境,生产要看这几项:

键默认值什么时候要改
soda.event.redis.topicsoda-events多个业务共用一套 Redis 时,用不同 topic 隔离
...stream.group-namesoda-events-group同一 topic 有多个独立消费方时,各用各的消费组
...stream.consumer-namesoda-events-consumer-<端口>多实例部署时必须是每个实例唯一(默认值已按端口区分)
...stream.concurrency1消费吞吐不够时调大,但要保证 EventHandler 幂等
...stream.batch-size10每次从 Stream 拉取的条数
...stream.maxlen10000Stream 长度上限,防止无限增长吃内存
...stream.max-retries3失败重试次数,超过进死信流
...stream.dead-letter-streamsoda-events-dead-letter死信流的 key 名
警告

consumer-name 在多实例部署时必须唯一。 两个实例用同一个消费名,Redis 会把它们当成同一个消费者 —— 消息会被其中一个抢走,另一个永远收不到,而且消费位点会互相踩。默认值带 server.port,如果多实例同端口(容器里常见),必须显式覆盖成 Pod 名或主机名。

幂等

Redis Stream 是「至少一次」投递,同一个事件可能被消费多次。跨服务场景建议打开:

yaml
soda:
  event:
    redis:
      stream:
        idempotency:
          enabled: true
          redis-key-prefix: soda-events-idempotency
          expire-time: 86400          # 秒,默认 24 小时

打开后,消费端会先查 eventId 的处理状态:SUCCESS / PROCESSING 直接跳过,FAILED 允许重试。

提示

幂等依赖事件的 eventId 稳定。soda 的事件 ID 是雪花算法生成的,同一个事件对象重投时 ID 不变 —— 这是幂等能工作的前提。如果你自己 new 事件对象重发,ID 会变,幂等就失效了。

异步线程池

CQRS 的异步出口(sendAsyncCommand 这类)走一个独立线程池:

yaml
scoda:
  ddd:
    cqrs:
      async:
        core-pool-size: 8
        max-pool-size: 16
        queue-capacity: 100
        thread-name-prefix: cqrs-async-
注意

前缀是 scoda,不是 soda。这是历史遗留的拼写错误,README 里写的 soda.ddd.cqrs.async 不生效 —— 写错了不会报错,线程池静默地用默认值。

队列满时的拒绝策略是在调用者线程执行(不丢任务,但会阻塞调用线程)。所以 queue-capacity 打满时,Web 请求线程会跟着卡住。生产环境建议:

  • queue-capacity 不要设太大 —— 队列只是缓冲,不是仓库
  • 监控线程池的活跃数和队列深度
  • 想完全接管,自己定义一个名为 cqrsAsyncExecutor 的 Executor Bean

三个最常见的配错

现象根因修法
领域事件一条都不触发,无报错配了 bus-type: spring,或者写成了 soda.ddd... 这类不存在的键显式写 simple 或 redis,用 @ConfigurationProperties 校验确认键名
线程池参数不生效前缀写成 soda.ddd.cqrs.async改成 scoda.ddd.cqrs.async
多实例下事件只有一台机器收到consumer-name 重复每个实例注入唯一值(Pod 名 / 主机名)

配置校验

EventProperties 带 @Validated,配错会在启动时失败,而不是运行时才暴露:

键约束
soda.event.bus-type@NotBlank
soda.event.redis.port@Positive
soda.event.redis.database@PositiveOrZero
soda.event.redis.stream.concurrency 等@Positive
soda.event.redis.stream.max-retries@PositiveOrZero

启动日志里出现 Binding to target ... failed 就是这一层拦下来的 —— 按提示改键值即可。

下一步