Elasticsearch 支持事务吗?为什么?
结论先行:不支持 ACID 意义上的跨文档事务。ES 只保证“单文档级别的原子性”——一次索引、更新或删除要么整体生效要么整体不生效;
bulk批量操作部分失败时不回滚;没有隔离级别、没有多文档回滚、没有读已提交。原因是它的定位是分布式检索,段不可变 + 分片自治 + 近实时可见,与事务所需的全局锁和统一提交点天然冲突。
一、能力对照
| 能力 | MySQL | Elasticsearch |
|---|---|---|
| 单条记录原子性 | 是 | 是(单文档) |
| 多文档原子性 | 是(同一事务) | 否 |
| 回滚 | 支持 | 不支持,只能补偿 |
| 隔离级别 | RC/RR 等 | 无(读已提交都不保证) |
| 并发控制 | 行锁 + MVCC | 乐观锁(_seq_no + _primary_term) |
| 批量操作 | 事务包裹 | bulk 逐条执行,逐条返回结果 |
| 可见性 | 提交即可见 | refresh 后可见,默认 1 秒 |
二、单文档的原子性靠什么
一次 index 请求
→ 路由到主分片
→ 主分片写入并生成 _seq_no
→ 同步到副本分片
→ 全部成功才返回 200
文档级操作的“全或无”由主分片串行执行保证,
但跨文档、跨分片没有统一的提交协议。
三、乐观锁:唯一的并发控制手段
GET /order/_doc/1
{
"_index": "order", "_id": "1", "_version": 3,
"_seq_no": 12, "_primary_term": 2,
"_source": { "status": "CREATED", "amount": 12.34 }
}
更新时带上版本号,版本不匹配返回 409:
POST /order/_update/1?if_seq_no=12&if_primary_term=2
{
"doc": { "status": "PAID" }
}
_seq_no 在分片内单调递增,_primary_term 在主分片切换时递增,两者组合能防止“主分片切换期间用旧版本覆盖新数据”。
四、bulk 的失败语义
POST /_bulk
{ "index": { "_index": "order", "_id": "1" } }
{ "amount": 12.34 }
{ "index": { "_index": "order", "_id": "2", "routing": "bad" } }
{ "amount": 99.99 }
响应里 errors: true,但第一条已成功写入,第二条失败——bulk 不是事务,需要业务按 items[].status 逐条判断并补偿。
五、真实的“事务”怎么落
| 需求 | 做法 |
|---|---|
| 多文档一致 | 应用层用状态机 + 补偿,或把强一致数据放 MySQL |
| 幂等写入 | 业务主键作为 _id,重复写覆盖而非新增 |
| 顺序保证 | 同一 _id 的更新天然串行(同分片同文档) |
| 跨索引一致 | outbox 表 + 版本号 + 对账任务 |
| 需要读己之写 | 写入后 refresh=wait_for 或 ?refresh=true |
POST /order/_doc/1?refresh=wait_for
{ "status": "PAID" }
refresh=wait_for 会等下一次 refresh 完成再返回,把“近实时”变成“写后立即可查”,代价是写入延迟上升。
常见追问
- 追问:那 ES 的
_version和_seq_no有什么区别?_version是文档级版本,每次更新加一,可按version或version_type=external控制;_seq_no是分片级序号,用于复制与乐观锁,粒度更细。 - 追问:为什么不能像数据库那样加锁?ES 的段不可变且分片分布在不同节点,实现全局锁需要分布式协调,抵消了水平扩展的优势。
- 追问:更新频繁会怎样?每次更新都是“标记删除 + 新文档”,段合并压力大,吞吐下降,这是无事务设计的直接代价。
小结
- 只有单文档原子性,没有跨文档事务、回滚与隔离级别。
- 并发控制靠
if_seq_no+if_primary_term的乐观锁。 bulk部分失败不回滚,必须逐条检查items。- 强一致需求留给数据库,ES 用补偿与对账达到最终一致。