简介:这份资源面向具备一定 Spring Boot 基础、希望掌握实时搜索与数据同步方案的后端开发者,围绕 Spring Boot 整合 Elasticsearch 8.3 并通过 RabbitMQ 同步 MySQL 数据展开,覆盖依赖配置、Repository 操作、消息监听与发送、事务事件监听、批量写入与错误重试等关键环节,可用于电商检索、日志分析等需要关系型数据库与搜索引擎协同的场景。压缩包共 78 个文件,约 90KB,以 21 个 java 源码与 21 个 class 编译文件为主体,辅以 17 个 xml 配置、2 个 yml 配置文件及证书、构建脚本等,完整呈现可运行的工程骨架。已有 214 人学习下载,读者可据此理清 MySQL 到 RabbitMQ 再到 Elasticsearch 的数据链路,参考消息驱动同步与批量处理的实现思路,并借助单元测试与监控配置排查常见问题。
1. Spring Boot 整合 Elasticsearch 8.3 与 RabbitMQ:先把版本对齐再动手
上周帮一位同事排查环境,现象很典型:Spring Boot 3.2 项目里引了spring-boot-starter-data-elasticsearch,本地跑得好好的,换到另一台机器启动就报NoNodeAvailableException,日志里连一个可用的节点都找不到。翻到最后发现,Elasticsearch 服务端装的是 7.17,客户端走的是 8.x 的 REST 协议,安全认证和连接握手全对不上。这类问题在 spring boot rabbitmq elasticsearch8 的组合里特别常见——三个组件各自有版本节奏,任何一个错半拍,启动就翻车。
这套环境适合两类人:一是要在业务系统里做 MySQL 到 Elasticsearch 的准实时同步,二是想用 RabbitMQ 把写库和建索引解耦,避免双写不一致。装不好会怎样?轻则索引建不出来,重则消息堆积、数据对不上,排查起来像开黑匣子。下面按我实际搭过一遍的顺序,从版本选型一路写到验证跑通。
2. 环境准备与版本选型:三个组件的版本必须对齐
2.1 版本对照表与选型理由
这套组合的坑,八成出在版本上。Elasticsearch 8.x 默认开启安全认证,客户端必须带账号密码或证书;Spring Boot 3.x 才正式支持 Elasticsearch 8 的 Java Client;RabbitMQ 的 Spring AMQP 版本又跟 Spring Boot 绑定。我一般先定 Spring Boot,再反推另外两个。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 17 | Spring Boot 3.x 最低要求 17 |
| Spring Boot | 3.2.x | 内置 Spring Data Elasticsearch 5.2.x,对应 ES 8.3 |
| Elasticsearch | 8.3.3 | 与 Spring Data ES 5.2 官方兼容 |
| RabbitMQ | 3.12.x | 管理插件与 Spring AMQP 3.1 匹配 |
| MySQL | 8.0.x | 开启 binlog 便于后续扩展 |
| Maven | 3.9.x | 编译插件版本别太老 |
选 8.3.3 而不是最新的 8.x,是因为 Spring Data Elasticsearch 5.2 的兼容矩阵明确写到 8.3,再往上客户端 API 有变动,ElasticsearchOperations的部分方法签名会变。熟手可以追新,但第一次搭环境,稳比新重要。
2.2 依赖清单与 pom 配置
依赖不要贪多,spring-boot-starter-data-elasticsearch已经带了 Java Client,再手动引elasticsearch-java容易版本打架。RabbitMQ 用 starter 即可。
<dependencies> <!-- Web 层,提供同步触发接口 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Elasticsearch 客户端,版本由 Spring Boot 父工程管理 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <!-- RabbitMQ,用于解耦写库与建索引 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!-- MySQL 驱动与 JPA --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> </dependencies>这里的关键点是不要给spring-boot-starter-data-elasticsearch写死版本号,交给父工程spring-boot-starter-parent统一管理,否则很容易出现客户端 8.5、服务端 8.3 的错配。MySQL 驱动从 8.0 起包名是com.mysql:mysql-connector-j,老教程里的mysql:mysql-connector-java已经废弃,照抄会拉不到包。
2.3 本地起 Elasticsearch 8.3 与 RabbitMQ
我用 Docker 起中间件,省得污染本机。Elasticsearch 8.3 首次启动会自动生成elastic用户密码,记得从日志里捞出来。
# 启动 Elasticsearch 8.3.3,单节点模式,关闭不必要的内存占用 docker run -d --name es833 \ -p 9200:9200 -p 9300:9300 \ -e "discovery.type=single-node" \ -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ -e "xpack.security.enabled=true" \ docker.elastic.co/elasticsearch/elasticsearch:8.3.3 # 从日志里取初始密码,形如 "Password for the elastic user is: xxxx" docker logs es833 2>&1 | grep -A2 "elastic user" # 启动 RabbitMQ,带管理插件方便看队列 docker run -d --name rabbit312 \ -p 5672:5672 -p 15672:15672 \ rabbitmq:3.12-managementdiscovery.type=single-node是单机开发必须的,不加会尝试组集群然后卡在选举。xpack.security.enabled=true保持默认开启,别图省事关掉——关掉之后客户端配置里不写认证能跑,但换到测试环境开了安全认证就全挂,这种“本地能跑线上不能跑”的玄学问题多半是这里埋的。RabbitMQ 默认账号密码都是guest,只允许本机访问,容器映射端口后从宿主机连没问题。
3. 配置连接:application.yml 里三个容易写错的参数
3.1 Elasticsearch 连接配置
Spring Boot 3.x 的 ES 配置前缀是spring.elasticsearch,不是老版本的spring.data.elasticsearch。这一点写错,配置根本不生效,客户端会用默认localhost:9200且不带认证,然后报 401。
spring: elasticsearch: uris: http://localhost:9200 username: elastic password: 你的初始密码 connection-timeout: 5s socket-timeout: 30s data: elasticsearch: repositories: enabled: trueuris写http://前缀,8.x 客户端支持 HTTPS,但本地开发用 HTTP 更省事。connection-timeout设 5 秒,避免服务没起来时启动线程卡太久;socket-timeout给 30 秒,批量建索引时别设太短。repositories.enabled打开后可以用ElasticsearchRepository接口,简单 CRUD 不用手写。
3.2 RabbitMQ 与 MySQL 配置
RabbitMQ 的配置重点是确认机制和并发消费者数量,MySQL 这边注意时区和批量抓取。
spring: rabbitmq: host: localhost port: 5672 username: guest password: guest publisher-confirm-type: correlated # 开启发送确认 publisher-returns: true # 开启路由失败回调 listener: simple: acknowledge-mode: manual # 手动 ack,防止消息丢失 prefetch: 10 # 每个消费者预取 10 条 concurrency: 2 max-concurrency: 4 datasource: url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driverpublisher-confirm-type: correlated配合publisher-returns: true,消息发到交换机但路由不到队列时能拿到回调,不然消息静默丢失,排查时毫无线索。acknowledge-mode: manual是同步场景的后悔药——消费者处理完建索引再手动 ack,中途异常消息还能重回队列。prefetch: 10控制单个消费者未确认消息上限,太大容易内存涨,太小吞吐上不去,10 到 20 是常见取值。
3.3 索引映射与实体定义
MySQL 表结构映射到 ES 时,字段类型要显式声明,别指望动态映射。日期字段尤其容易翻车,MySQL 的datetime到 ES 里如果被识别成text,范围查询直接失效。
@Document(indexName = "product") public class ProductDoc { @Id private Long id; @Field(type = FieldType.Text, analyzer = "standard") private String name; @Field(type = FieldType.Keyword) private String category; @Field(type = FieldType.Double) private Double price; @Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second) private LocalDateTime updateTime; // getter/setter 省略 }@Document(indexName = "product")对应索引名,启动时如果配置了自动建索引会按注解生成 mapping。category用Keyword而不是Text,因为它是精确过滤字段,用Text会被分词,聚合和 term 查询都对不上。updateTime显式指定DateFormat,避免默认格式与写入值不匹配导致mapper_parsing_exception。
4. 同步链路搭建:MySQL 变更经 RabbitMQ 落到 Elasticsearch
4.1 交换机、队列与绑定声明
同步链路的核心是一个 topic 交换机加一个持久化队列。交换机按业务类型路由,队列持久化保证 RabbitMQ 重启后消息不丢。
@Configuration public class RabbitConfig { public static final String EXCHANGE = "sync.exchange"; public static final String QUEUE = "sync.product.queue"; public static final String ROUTING_KEY = "product.#"; @Bean public TopicExchange syncExchange() { // durable=true,交换机持久化 return new TopicExchange(EXCHANGE, true, false); } @Bean public Queue syncQueue() { // 持久化队列,避免 broker 重启后队列消失 return QueueBuilder.durable(QUEUE).build(); } @Bean public Binding syncBinding() { return BindingBuilder.bind(syncQueue()).to(syncExchange()).with(ROUTING_KEY); } }TopicExchange的第三个参数autoDelete设 false,别让队列在无消费者时被自动删除。ROUTING_KEY用product.#通配,后续加product.update、product.delete都能命中同一个队列,扩展时不用改绑定。队列用QueueBuilder.durable而不是new Queue,语义更清楚,也方便后续加死信参数。
4.2 写库后发消息:事务边界要卡准
最容易出错的地方是“库写成功了消息没发出去”或者“消息发了库回滚了”。常见做法是把发消息放在事务提交之后,用TransactionSynchronization回调。
@Service public class ProductService { private final ProductRepository repository; private final RabbitTemplate rabbitTemplate; public ProductService(ProductRepository repository, RabbitTemplate rabbitTemplate) { this.repository = repository; this.rabbitTemplate = rabbitTemplate; } @Transactional public void saveAndSync(Product product) { repository.save(product); // 事务提交后再发消息,避免回滚后消息已投递 TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void afterCommit() { rabbitTemplate.convertAndSend( RabbitConfig.EXCHANGE, "product.update", product.getId() ); } } ); } }afterCommit保证只有数据库事务真正提交才发消息,这是双写一致性的第一道闸。消息体只传主键id,不传整个对象——对象可能很大,而且消费端应该以数据库为准重新查,避免消息里的旧数据覆盖新数据。convertAndSend默认用 JDK 序列化,生产环境建议配Jackson2JsonMessageConverter,否则跨语言消费方解不了。
4.3 消费端建索引:手动 ack 与幂等处理
消费端拿到 id 后回查 MySQL,再写入 ES。手动 ack 配合幂等写入,重复消息不会造成脏数据。
@Component public class ProductSyncConsumer { private final ProductRepository repository; private final ElasticsearchOperations esOperations; @RabbitListener(queues = RabbitConfig.QUEUE) public void onMessage(Long productId, Message message, Channel channel) throws IOException { long tag = message.getMessageProperties().getDeliveryTag(); try { Product product = repository.findById(productId).orElse(null); if (product != null) { // save 是幂等的,同一 id 重复写会覆盖 esOperations.save(toDoc(product)); } channel.basicAck(tag, false); } catch (Exception e) { // 处理失败,拒绝并重回队列,配合重试上限防止死循环 channel.basicNack(tag, false, true); } } }basicAck的第二个参数multiple设 false,逐条确认,批量确认在异常时容易误确认。basicNack第三个参数requeue=true会把消息放回队列,但要注意:如果异常是数据本身的问题,无限重回会形成死循环,生产上一般配死信队列或重试次数上限。esOperations.save按@Id覆盖写入,天然幂等,重复消费不会产生重复文档。
5. 避坑与排查:五个真实踩过的坑
5.1 启动报 401 Unauthorized
现象:应用启动正常,第一次调 ES 接口报ResponseException: 401 Unauthorized。原因:spring.elasticsearch.username/password没配,或者密码里含特殊字符没加引号。解决:从容器日志确认初始密码,YAML 里用单引号包住密码;如果改过密码,用curl -u elastic:密码 http://localhost:9200先验证凭据本身可用。
5.2 索引字段类型与预期不符
现象:price字段做范围查询查不到数据。原因:索引是动态映射生成的,price被识别成text。解决:删掉索引DELETE /product,确认实体上@Field(type = FieldType.Double)生效后重启重建;或者用PUT /product/_mapping手动补 mapping。动态映射只适合原型,正式环境一律显式声明。
5.3 消息发出但队列没收到
现象:convertAndSend不报错,管理后台队列消息数为 0。原因:routing key 与绑定不匹配,或者交换机名写错。解决:打开publisher-returns后注册ReturnsCallback打印退回消息;在 RabbitMQ 管理页的 Exchanges 里看绑定关系,确认 routing key 能被product.#命中。
5.4 消费端反复重试同一条消息
现象:日志里同一条消息被消费多次,ES 数据没更新。原因:basicNack的requeue=true遇到不可恢复异常(比如 id 在库里不存在)形成死循环。解决:区分可重试与不可重试异常,数据不存在直接 ack 丢弃并记日志;或者给队列配x-dead-letter-exchange,超过重试次数进死信队列人工处理。
5.5 时间字段写入报 mapper_parsing_exception
现象:写入 ES 时报日期格式解析失败。原因:LocalDateTime序列化格式与 mapping 里声明的DateFormat不一致。解决:统一用yyyy-MM-dd'T'HH:mm:ss格式,实体上@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second),并在 Jackson 配置里注册JavaTimeModule,别让默认序列化输出带时区偏移的格式。
6. 装完怎么验证:三步确认链路真的通了
环境装完不等于链路通,我一般按“中间件 → 单组件 → 全链路”三步验证,每步都有明确的观察点。
第一步,确认两个中间件本身可用。ES 用curl查集群健康,RabbitMQ 看管理页队列是否声明成功:
# ES 返回 status 为 yellow 或 green 即正常,单节点 yellow 是副本未分配,可接受 curl -u elastic:'你的密码' http://localhost:9200/_cluster/health?pretty # 确认索引 mapping 里 price 是 double、updateTime 是 date curl -u elastic:'你的密码' http://localhost:9200/product/_mapping?pretty第二步,单独验证 ES 写入。调一次业务接口触发saveAndSync,然后直接查 ES 文档:
# 按 id 查文档,能看到 _source 里字段齐全即写入成功 curl -u elastic:'你的密码' http://localhost:9200/product/_doc/1?pretty第三步,验证消息链路。在 RabbitMQ 管理页(http://localhost:15672)看sync.product.queue的 Ready 和 Unacked 计数:调用接口后 Ready 短暂上升再归零,说明消费正常;如果 Ready 持续上涨,说明消费者没起来或 ack 卡住。再故意把 ES 停掉调一次接口,观察消息是否留在队列里、恢复后是否被重新消费——这一步能验证手动 ack 和重回队列是否真的生效。
| 验证点 | 正常表现 | 异常时先看哪 |
|---|---|---|
| ES 集群健康 | yellow/green | 容器日志、端口占用 |
| 索引 mapping | 字段类型与实体一致 | 是否动态映射抢先建了索引 |
| 队列 Ready 计数 | 调用后短暂上升后归零 | 消费者是否注册、ack 模式 |
| 文档内容 | _source字段完整 | 消费端回查 SQL 与转换逻辑 |
这套验证我吃过亏:有一次只看了接口返回 200 就以为通了,结果 ES 里一条数据没有,原因是消费者所在的包没被扫描到,@RabbitListener根本没生效。从那以后我每次搭完这套环境,都强制走一遍“停 ES 看消息堆积、恢复看消费归零”的动作,比看日志靠谱得多。希望帮到你。
本文还有配套的精品资源,点击获取