这周是我学习 Java 后端的一个分水岭。前六周学完了 Spring Boot、MySQL、Redis、微服务、消息队列,感觉自己已经能写业务代码了。但这一周学完 ES 和分布式基础之后,我才意识到:真正的后端开发,远不止 CRUD 那么简单。
一、Elasticsearch:为什么 MySQL LIKE 不够用?
1.1 从"模糊查询"说起
之前做项目,有个搜索框要查书名。我想当然地写了SELECT * FROM book WHERE title LIKE '%Java%'。数据量小的时候跑得挺欢,等数据上了十万,接口直接超时——全表扫描,索引用不上。
后来才知道,MySQL 的 B+ 树索引是为精确查找和范围扫描设计的。你想查"内容里包含哪些词",B+ 树根本帮不上忙。这时候就该 Elasticsearch 出场了。
1.2 倒排索引:ES 的核心武器
ES 用的是倒排索引。打个比方:你有一本书,传统做法是"第 1 页写了什么、第 2 页写了什么"(正排);倒排索引反过来,是"Java 这个词出现在第 1、5、10 页,虚拟机出现在第 3、7 页"(词 → 页码列表)。
查询的时候,把搜索词分词(“Java 虚拟机"分成"Java"和"虚拟机”),直接查倒排索引表,拿到包含这些词的文档 ID,根本不用扫全表。这就是为什么 ES 能在毫秒级返回结果。
1.3 text 和 keyword:两种字段类型
ES 里字段类型分两种:
- text:写入时分词,存的是一个个词条,用于全文搜索(用 match 查它)
- keyword:不分词,整个值作为一个整体,用于精确匹配、排序、聚合(用 term 查它)
新手第一大坑:用 term 去查 text 字段。text 字段写入时词条会被转成小写(“Java"存成了"java”),而 term 不分词,拿大写"Java"原样去查,当然查不到。正确做法是精确匹配走 keyword 子字段:{"term": {"title.keyword": "完整书名"}}。
二、DSL 查询:ES 里的 SQL
ES 的查询语言叫 DSL(Domain Specific Language),本质就是一段 JSON。查询的统一入口是GET /索引名/_search,body 里放查询条件。
2.1 match 和 term:全文搜索 vs 精确匹配
- match:查询词会被分词,任意词条命中即匹配,并按命中程度计算 _score(相关性得分),用于全文搜索
- term:查询词不分词,整体精确匹配,用于状态、ID、枚举等精确过滤
口诀:搜内容用 match,筛条件用 term。
2.2 bool 查询:四个子句各管一摊
真实业务从来不只有一个条件。“价格 30-80、书名含 Java、排除某分类、最好还讲并发”——这种组合条件用 bool 查询:
| 子句 | 含义 | 参与算分吗 |
|---|---|---|
| must | 必须满足,相当于 AND | ✅ 参与 |
| filter | 必须满足,相当于 AND | ❌ 不参与 |
| should | 满足了加分,不满足也行,相当于 OR | ✅ 参与 |
| must_not | 必须不满足,相当于 NOT | ❌ 不参与 |
must 和 filter 的区别是面试高频中的高频:两者都要求条件必须满足,但 must 会计算 _score,filter 不算分。不算分为什么反而是优点?因为过滤结果可以被 ES 缓存,而且省掉了算分开销——所以所有"不需要按相关度排序"的条件(状态、时间范围、价格区间、分类),一律放 filter,性能更好。
2.3 深分页问题
用 from + size 分页时,ES 是分布式的,数据散在多个分片上。你要第 100 页(from=990, size=10),ES 得让每个分片都把前 1000 条发给协调节点合并排序,再从中挑出你要的 10 条——越往后翻,白搬运的数据越多。所以生产上一般限制翻页深度,更深的需求用search_after。
三、Spring Boot 集成 ES:三步走
3.1 依赖和配置
引入spring-boot-starter-data-elasticsearch,yml 里配置 ES 地址:
spring:elasticsearch:uris:http://localhost:92003.2 实体类映射
用@Document注解把 Java 类映射到 ES 索引,@Field定义字段类型和分词器:
@Document(indexName="books")publicclassBookDocument{@IdprivateStringid;@Field(type=FieldType.Text,analyzer="ik_max_word")privateStringtitle;@Field(type=FieldType.Keyword)privateStringcategory;// ...}3.3 NativeQuery 三步流程
复杂查询(bool 组合、排序、高亮)用ElasticsearchOperations+NativeQuery:
// 第一步:构建 QueryNativeQueryquery=NativeQuery.builder().withQuery(q->q.bool(b->b.must(m->m.match(mq->mq.field("title").query(keyword))).filter(f->f.range(r->r.field("price").gte(JsonData.of(30)).lte(JsonData.of(80)))))).withPageable(PageRequest.of(page-1,size)).withSort(s->s.field(f->f.field("price").order(SortOrder.Asc))).withHighlightQuery(newHighlightQuery(newHighlight(List.of(newHighlightField("title"))),BookDocument.class)).build();// 第二步:执行查询SearchHits<BookDocument>hits=esOperations.search(query,BookDocument.class);// 第三步:处理结果(含高亮)for(SearchHit<BookDocument>hit:hits.getSearchHits()){BookDocumentbook=hit.getContent();Map<String,List<String>>highlightFields=hit.getHighlightFields();if(highlightFields.containsKey("title")){book.setTitle(String.join("",highlightFields.get("title")));}}记住这个流程:NativeQuery.builder() 链式拼条件 → search 执行 → 遍历 SearchHit 处理高亮。
3.4 数据同步:MySQL 和 ES 怎么保持一致?
MySQL 是主存储(增删改、事务都靠它),ES 是搜索副本。两份数据怎么同步?
- 同步双写:业务代码写完 MySQL 顺手写 ES,简单但有失败不一致的风险
- 监听 binlog:用 Canal 等工具监听 MySQL 变更日志,异步同步到 ES,业务解耦、一致性更好,大厂常用
四、分布式事务:CAP、BASE 和四种方案
4.1 问题从哪来
单体应用里,一个@Transactional就能保证多个操作要么全成功、要么全回滚。但微服务里,订单在 order-service(订单库),库存在 inventory-service(库存库),@Transactional只能管住自己这一个数据库——订单插进去了,远程调用扣库存失败了,订单怎么办?
跨服务记账怎么保证不出错,就是分布式事务要解决的问题。
4.2 CAP 定理:三者最多满足两个
CAP 定理说:一个分布式系统,一致性(C)、可用性(A)、分区容错性(P)三者最多只能同时满足两个。
关键推论:P 是没得选的。分布式系统里机器之间靠网络通信,而网络故障是必然会发生的,你不可能"不允许网络分区"。所以实际的选择是在 C 和 A 之间二选一:
- CP:保一致,宁可拒绝服务也不能给错数据。ZooKeeper(选主期间短暂不可用)、银行转账
- AP:保可用,先给你响应,数据之后再对齐。Eureka、Nacos、电商商品详情页
选型原则:资金、库存这类"给错数据会出大事"的场景保 C;搜索、推荐、详情页这类"给旧数据也无所谓"的场景保 A。
4.3 BASE 理论:AP 的具体落地
选了 AP,等于承认"数据可以暂时不一致"。BASE 理论给出了答案:
- Basically Available(基本可用):出问题时系统不整体挂掉,但允许降级
- Soft State(软状态):允许数据存在"中间状态",不要求实时同步
- Eventually Consistent(最终一致性):不要求实时一致,但最终所有数据会对齐
一句话总结:BASE = 牺牲强一致性,换取系统的高可用。
4.4 四种分布式事务方案
| 方案 | 一致性 | 性能 | 业务侵入 | 适用场景 |
|---|---|---|---|---|
| 2PC(两阶段提交) | 强一致 | 差(同步阻塞) | 低 | 传统数据库 XA,现在少用 |
| TCC | 强一致 | 较高 | 高(要写三个接口) | 资金、核心短链路 |
| Seata AT | 最终一致(接近强) | 中 | 低(一个注解) | 一般业务,最常用 |
| MQ 最终一致 | 最终一致 | 高 | 中 | 允许延迟的异步场景 |
Seata AT 模式是我最想说的。它的卖点是对业务几乎零侵入——加一个注解搞定。
工作原理分两阶段:
- 一阶段:每个分支事务直接执行本地提交,同时 Seata 自动拦截 SQL,把修改前后的数据快照记到undo_log表里(beforeImage 和 afterImage)
- 二阶段:如果所有分支都成功 → 全局提交,undo_log 异步删掉;如果任何分支失败 → 全局回滚,按 undo_log 里的 beforeImage 生成反向 SQL 补偿回去
为什么"一阶段直接提交"还能保证一致?因为 Seata 有全局锁:被全局事务占用的行记录,其他全局事务不能改,防止了脏写。
代码里就一个注解:
@GlobalTransactional(rollbackFor=Exception.class)publicvoidcreateOrder(LonguserId,LongproductId,Integercount){orderMapper.insert(newOrder(userId,productId,count));inventoryFeign.deduct(productId,count);// 远程扣库存accountFeign.debit(userId,count*100);// 远程扣余额}任何一步抛异常,TC 会驱动所有分支按 undo_log 回滚。
五、分布式锁:Redis 的三个坑和 Redisson
5.1 为什么需要分布式锁
单机场景,synchronized锁住一个 JVM 内的共享资源就够了。但微服务部署了多个实例,3 个 Pod 各自扣库存,synchronized只能锁住自己这个 JVM,3 个实例之间互相看不到——库存只剩 1 件,3 个 Pod 同时判断"还有货",各自扣一次,结果超卖了。
你需要一把"跨仓库的公共钥匙"——谁拿到这把公共钥匙谁才能进仓库操作,这就是分布式锁。
5.2 Redis 实现分布式锁的三个坑
基础实现:SET key value NX EX seconds,NX 表示"不存在才设置"(保证互斥),EX 表示过期时间(防止死锁)。必须原子操作,不能用 SETNX + EXPIRE 两步。
坑 1:不可重入。同一个线程再次获取锁会失败——自己把自己锁死了。解决:用 Hash 结构记录持有者和重入次数。
坑 2:过期时间不好设。设短了业务没执行完锁就过期,别人拿到锁;设长了持有者宕机,别人要等很久。解决:看门狗自动续期——默认给锁 30 秒过期,每 10 秒检查一次,持有者还活着就续期到 30 秒。
坑 3:主从切换锁丢失。Redis 主节点刚加锁、还没同步到从节点就挂了,从节点升级为主——别人可以拿到锁,互斥性被破坏。解决:RedLock 算法(向 N 个独立 Redis 节点都加锁,过半成功才算成功),但争议大,生产环境强一致场景用 ZooKeeper 更可靠。
5.3 Redisson:把三个坑全填了
Redisson 是 Redis 官方推荐的 Java 客户端,封装了分布式锁的所有坑:
RLocklock=redisson.getLock("lock:stock:1");try{lock.lock();// 默认30s过期,看门狗自动续期// 业务逻辑}finally{lock.unlock();}生产环境直接用 Redisson,不用自己手写。
六、分布式 ID:雪花算法
6.1 方案对比
- UUID:128 位,无序,太长,不适合做主键(B+ 树插入性能差)
- 数据库自增:简单但有瓶颈(单点、性能差)
- Snowflake 雪花算法:趋势递增、不依赖中心、高性能
6.2 雪花算法 64 位分配
1 位符号位(永远 0) + 41 位时间戳(毫秒级,可用 69 年) + 10 位机器 ID(最多 1024 台) + 12 位序列号(每毫秒 4096 个)- 趋势递增:时间戳在高位,整体递增,B+ 树插入友好
- 不依赖中心:每台机器根据 workerId 独立生成,不需要协调
- 高性能:本地计算,不依赖网络
时钟回拨问题:服务器时间被 NTP 同步回退,导致生成的 ID 重复。解决方案:等待时钟追上、或报错拒绝、或用美团 Leaf 等开源方案处理。
七、项目实战:设备管理平台
这周学的东西在"设备管理平台"里都用到了:
- ES 搜索:设备搜索接口用 ES,bool 查询组合关键词全文搜 + 状态过滤 + 时间范围过滤 + 高亮飘红
- 数据同步:设备主数据存 MySQL,通过 Canal 监听 binlog 异步同步到 ES
- 分布式锁:设备绑定/解绑时用 Redisson 锁住 deviceId,防止并发重复绑定
- 分布式事务:设备绑定要同时更新用户服务和设备服务,用 Seata AT 保证强一致
- 分布式 ID:设备数据上报量巨大,用雪花算法生成趋势递增的 report_id
面试讲项目时可以说:“强一致的场景我们用 Seata AT,允许延迟的场景用 MQ 最终一致性,选型依据是业务对一致性的容忍度”——这一句话就能体现你不是背八股,而是真的做过取舍。
八、本周感悟
这周学完,我最大的感触是:后端开发远不止 CRUD。
之前觉得会写 Spring Boot、会连数据库、会调接口就够了。但这周学完 ES 和分布式基础之后,我才意识到:真正的后端开发,要考虑性能(ES 倒排索引)、要考虑一致性(分布式事务)、要考虑并发(分布式锁)、要考虑扩展性(分布式 ID)。
每一个技术选型背后,都是对业务场景的权衡。CAP 定理告诉你"为什么做不到完美",BASE 理论告诉你"实践中怎么妥协",四种分布式事务方案告诉你"不同场景怎么选"。
这周的内容面试必问,尤其是 CAP、Seata AT、Redis 分布式锁的三个坑、雪花算法。我把它们整理成博客,也算给自己做个总结。
下一周要学 Kafka 和 Flink,搜广推方向的核心技术。继续加油。
参考资料:
- Elasticsearch 官方文档
- Spring Data Elasticsearch 参考文档
- Seata 官方文档
- Redisson 官方文档
作者:王俊翰
原文链接:一周从零吃透 Elasticsearch + 分布式基础
转载请注明出处