Elasticsearch 支持事务吗?为什么?

#中间件#面试八股

Elasticsearch 支持事务吗?为什么?

结论先行:不支持 ACID 意义上的跨文档事务。ES 只保证“单文档级别的原子性”——一次索引、更新或删除要么整体生效要么整体不生效;bulk 批量操作部分失败时不回滚;没有隔离级别、没有多文档回滚、没有读已提交。原因是它的定位是分布式检索,段不可变 + 分片自治 + 近实时可见,与事务所需的全局锁和统一提交点天然冲突。

一、能力对照

能力MySQLElasticsearch
单条记录原子性是是(单文档)
多文档原子性是(同一事务)否
回滚支持不支持,只能补偿
隔离级别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 用补偿与对账达到最终一致。
笔记加载中…