配置项
soda 的配置面很小:一个总线类型开关 + 一组 Redis 参数 + 一个线程池。这一页讲怎么配、配在哪、以及三个最容易踩的坑。逐键的完整清单见 配置参考。
配置写在哪儿
soda 用的是 Spring Boot 的标准机制:application.yml(或 .properties),加 profile 做环境隔离。
# application.yml —— 所有环境共用
soda:
event:
bus-type: simple # 显式写,别依赖默认值
scoda:
ddd:
cqrs:
async:
core-pool-size: 8# 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 是要入库的,入库文件零明文密钥。
最小可用配置
分三种情况,按你需要选一种:
单机 / 开发 / 测试:进程内总线
soda:
event:
bus-type: simple完事。不需要 Redis,事件在同一个 JVM 内同步派发。
生产 / 多实例:Redis 事件总线
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.topic | soda-events | 多个业务共用一套 Redis 时,用不同 topic 隔离 |
...stream.group-name | soda-events-group | 同一 topic 有多个独立消费方时,各用各的消费组 |
...stream.consumer-name | soda-events-consumer-<端口> | 多实例部署时必须是每个实例唯一(默认值已按端口区分) |
...stream.concurrency | 1 | 消费吞吐不够时调大,但要保证 EventHandler 幂等 |
...stream.batch-size | 10 | 每次从 Stream 拉取的条数 |
...stream.maxlen | 10000 | Stream 长度上限,防止无限增长吃内存 |
...stream.max-retries | 3 | 失败重试次数,超过进死信流 |
...stream.dead-letter-stream | soda-events-dead-letter | 死信流的 key 名 |
consumer-name 在多实例部署时必须唯一。 两个实例用同一个消费名,Redis 会把它们当成同一个消费者 —— 消息会被其中一个抢走,另一个永远收不到,而且消费位点会互相踩。默认值带 server.port,如果多实例同端口(容器里常见),必须显式覆盖成 Pod 名或主机名。
幂等
Redis Stream 是「至少一次」投递,同一个事件可能被消费多次。跨服务场景建议打开:
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 这类)走一个独立线程池:
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的ExecutorBean
三个最常见的配错
| 现象 | 根因 | 修法 |
|---|---|---|
| 领域事件一条都不触发,无报错 | 配了 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 就是这一层拦下来的 —— 按提示改键值即可。