不能直接用 stringredistemplate.convertandsend() 发布消息,因其导致业务模块硬依赖redis细节、缺乏业务语义、易引发拼写错误与反序列化异常;应封装为契约接口(如usereventpublisher)并使用带版本的dto事件。

为什么不能直接用 StringRedisTemplate.convertAndSend() 发布消息
因为这会让业务模块(比如 hyf-user)硬依赖 Redis 底层通信细节,一旦后续要切到 Kafka 或加幂等校验、重试逻辑,就得改所有调用点。更麻烦的是,convertAndSend() 不带业务语义,参数是原始字符串或 JSON,下游消费者无法静态感知消息结构,IDE 无提示、编译不校验、重构易出错。
常见错误现象包括:UserConstants.USER_REGISTER 字符串散落在各处、JSON 字段名拼错、消费者反序列化失败抛 JsonMappingException、新增字段后老消费者直接 crash。
- 必须把频道名、消息体结构、序列化方式全部收敛到一个契约接口里
- 发布动作应封装为带明确业务意图的方法,例如
userEventPublisher.publishUserRegistered(user) - 频道名不应暴露为字符串常量,而应由接口内部决定(比如拼接前缀
event:user:register) - 消息体必须用 DTO 类型,禁止裸
String或Map
如何定义可被 Spring 管理的统一事件发布器接口
核心是让接口既解耦又可测试:它不持有 StringRedisTemplate 实例,而是通过构造函数注入,并只暴露业务方法。Spring Boot 启动时自动装配实现类,上层服务只依赖接口。
示例接口定义:
public interface UserEventPublisher {
void publishUserRegistered(UserRegisteredEvent event);
void publishUserUpdated(UserUpdatedEvent event);
}
对应实现类中才引入 StringRedisTemplate,并负责:JSON.toJSONString() 序列化、设置 ContentType header、写入固定频道(如 event:user:register)、记录日志。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 接口方法名必须体现业务动作,而非技术动作(拒绝
publish(String channel, Object msg)) - 每个事件类型必须是独立不可变 DTO,含
@Data+@Builder+ 显式serialVersionUID - 避免在接口里暴露 Redis 相关异常(如
RedisConnectionFailureException),统一转为EventPublishException - 若需异步发布,应在实现类内用
@Async,而非要求调用方自己开线程
消费者端如何避免重复消费与消息丢失
Redis Pub/Sub 本身不保证消息可靠性:客户端断连期间发布的消息会直接丢弃,且没有 offset 概念。所以“解耦”不等于“免责”,必须在应用层补足。
标准做法是:订阅者不直接处理业务逻辑,而是把收到的原始消息落地到本地 DB(如 MySQL 的 event_inbox 表),再由定时任务或监听器异步拉取并去重处理。
- 每条入库消息必须有唯一
message_id(推荐用 UUID 或 RedisPUBLISH返回值 + 时间戳组合) - 入库前先
SELECT ... FOR UPDATE或用唯一索引拦截重复message_id - 业务处理成功后才更新该记录状态为
processed,失败则留待重试 - 不要依赖
RedisMessageListenerContainer的setSubscriptionExecutor做并发消费——它无法控制重试边界
跨服务事件命名与版本兼容怎么管
当 hyf-mail 和 hyf-message 都订阅 user.register,但 v2 版本的用户对象加了 tenantId 字段,老邮件服务就会解析失败。频道名和消息结构必须带版本号。
推荐格式:event:user:register:v2,并在 DTO 类上用注释标明兼容范围,例如:
/**
* 用户注册事件 v2
* 兼容:hyf-mail@1.3+, hyf-message@2.0+
* 不兼容:hyf-sms@1.0(需升级)
*/
public class UserRegisteredEvent implements Serializable { ... }
- 禁止用
event:user:register这种无版本频道——它等于埋雷 - 频道名中的版本号必须与 DTO 类名或包路径一致(如
v2.UserRegisteredEvent) - 旧版本消费者下线前,需保留对应频道的桥接转发逻辑(如 v1 订阅者收到 v2 消息时做字段降级)
- Spring Cloud Contract 不适用 Redis 场景,得靠人工对齐 + 单元测试覆盖字段变更
实际最难的不是写代码,是让所有团队遵守同一套事件命名和演化规则。频道名一旦写死在生产环境,改起来比数据库字段还痛——毕竟它不走 migration 脚本,全靠人肉 grep 和发版协调。










