Galera Cluster的写集冲突检测,本质上是一种基于"认证"(Certification)机制的乐观并发控制方案。当多个节点同时对同一行数据执行写操作时,系统不会在写入时立即加锁等待,而是允许所有节点都先执行事务,最后在提交阶段通过比较写集(Write Set)来判断是否存在冲突。如果两个事务的写集存在交集,且来自不同节点,则后提交的事务会被回滚并报错,应用层需要重新执行。这套机制是Galera实现多主同步写入的核心基石,理解它才能真正用好Galera Cluster。
要搞清楚写集冲突检测,首先得明白Galera Cluster到底是什么。它是一个基于MySQL/MariaDB的同步多主复制集群方案,由Codership开发。和传统的异步复制不同,Galera要求所有节点的数据在任何时刻都保持一致,任何一个节点都可以接受写请求,然后通过组通信(Group Communication)把变更同步到其他所有节点。这听起来很美好,但问题也很明显:多个节点同时写同一行数据怎么办?这就是写集冲突检测要解决的核心问题。
什么是写集(Write Set)
写集是Galera用来记录一个事务修改了哪些数据的集合。具体来说,它记录的是事务执行过程中所有被修改的行的"主键值+表名"的哈希组合。每个事务在执行过程中,Galera会在本地维护一个写集,把所有UPDATE、DELETE、INSERT操作涉及的行标识都收集起来。注意,这里记录的不是具体修改了什么内容,而是"哪些行被改了"。这个设计非常关键,因为冲突检测只需要知道"改了哪些行",而不需要知道"改成了什么"。
举个例子,假设有一个表users,主键是id。事务A在节点1上执行了UPDATE users SET name='Alice' WHERE id=100,事务B在节点2上执行了UPDATE users SET name='Bob' WHERE id=100。那么两个事务的写集都包含了"users表+id=100"这个标识。当两个事务都尝试提交时,Galera发现它们的写集有交集,就判定为冲突。
认证机制(Certification-Based Concurrency Control)的工作流程
Galera采用的是认证机制,而不是传统数据库常见的两阶段锁(2PL)。整个流程分为三个阶段:执行阶段、复制阶段、认证提交阶段。
第一阶段是执行阶段。事务在本地节点正常执行SQL语句,Galera在后台默默收集写集。这个阶段不做任何锁竞争检查,也不和其他节点通信,完全是本地操作。这就是"乐观"的含义——假设不会有冲突,先干了再说。
第二阶段是复制阶段。事务执行完毕后,Galera把整个事务的写集(包含所有修改的binlog事件)通过组通信协议发送给集群中的所有其他节点。每个节点收到后,会把这些事件应用到自己的本地数据库中,但此时还不算正式提交,只是"预应用"。
第三阶段是认证提交阶段。这是最关键的一步。每个节点在收到其他节点的事务写集后,会拿自己本地已提交的事务写集和新收到的写集做比较。具体规则是:如果新收到的事务写集与本地已经认证通过的事务写集存在交集,并且这两个事务来自不同的节点(即不同的全局事务ID),那么新事务就会被判定为冲突,直接回滚。如果没有交集,或者虽然有交集但来自同一个节点(说明是同一个事务的不同副本),则认证通过,事务正式提交。
写集冲突的具体判定规则
Galera的冲突判定有几个硬性规则,理解这些规则对排查问题非常重要。
规则一:写集交集判定。两个事务的写集必须存在至少一个共同的"表+主键"标识,才会被认为有冲突。如果事务A改了id=100,事务B改了id=200,即使它们操作的是同一张表,也不会冲突。
规则二:来源节点判定。即使写集有交集,如果两个事务来自同一个节点(即同一个全局序列号的事务在不同节点上的副本),也不算冲突。这是因为同一个事务在所有节点上的写集是完全相同的,不存在真正的并发修改。
规则三:只看写集不看读集。这一点非常重要。Galera的冲突检测完全基于写集,和事务读取了什么数据无关。也就是说,即使事务A读了某行然后事务B修改了同一行,只要事务A本身没有修改任何数据(写集为空),就不会产生冲突。这和传统的MVCC机制有本质区别。
-- 以下是一个典型的冲突场景
-- 节点1执行:
BEGIN;
UPDATE orders SET status='paid' WHERE order_id=5001;
COMMIT; -- 写集: {orders, order_id=5001}
-- 节点2同时执行:
BEGIN;
UPDATE orders SET status='shipped' WHERE order_id=5001;
COMMIT; -- 写集: {orders, order_id=5001}
-- 结果:节点2的事务被回滚,报错:
-- ERROR 1213 (40001): Deadlock found when trying to get lock;
-- try restarting transaction全局事务ID(GTID)在冲突检测中的角色
Galera使用全局事务ID(Global Transaction ID)来唯一标识每一个事务。这个ID由两部分组成:节点的UUID和一个本地递增的序列号。在认证阶段,Galera通过比较GTID来判断两个事务是否来自同一个节点。如果GTID的节点UUID部分相同,说明是同一个事务的副本,不构成冲突。如果UUID不同且写集有交集,才判定为真正的冲突。
这意味着Galera的冲突检测是"最后提交者输"(Last Committer Loses)的策略。先完成认证的事务获胜,后到的事务被回滚。这也是为什么在高并发写入场景下,某些节点会频繁遇到死锁错误的根本原因。
如何降低写集冲突的发生率
理解了机制之后,降低冲突就有了明确的方向。核心思路只有一个:减少不同节点同时修改同一行数据的概率。
方法一:合理设计数据分片策略。把热点数据分散到不同节点上处理,避免多个节点频繁操作同一行。比如按用户ID取模分片,让同一个用户的数据尽量落在同一个节点上。
方法二:应用层引入重试机制。既然Galera的冲突检测会导致事务回滚,应用程序就必须具备自动重试的能力。一般建议重试3-5次,每次间隔递增。很多成熟的数据库中间件和ORM框架都内置了这种重试逻辑。
方法三:控制事务粒度。事务越大,涉及的行越多,写集就越大,和其他事务产生交集的概率就越高。尽量把大事务拆成小事务,减少单次事务的写集范围。
方法四:使用定向路由。在应用层通过中间件将同一类数据的写操作固定路由到某一个节点,从根本上避免多节点同时写入同一行。这种方式牺牲了一部分多主写入的灵活性,但能大幅降低冲突率。
-- 应用层重试逻辑伪代码示例
max_retries = 5
for attempt in range(max_retries):
try:
begin_transaction()
execute_sql("UPDATE accounts SET balance=balance-100 WHERE id=1")
execute_sql("UPDATE accounts SET balance=balance+100 WHERE id=2")
commit()
break # 成功则退出
except DeadlockError:
rollback()
if attempt == max_retries - 1:
raise # 重试耗尽,抛出异常
sleep(2 attempt) # 指数退避写集冲突与死锁报错的关系
很多人在使用Galera时会频繁看到"Deadlock found"的报错,误以为Galera使用了传统的锁机制。实际上这个报错是Galera对写集冲突的一种包装。MySQL本身的InnoDB引擎也有自己的锁和死锁检测机制,但在Galera环境下,大部分"死锁"其实是Galera层面的写集冲突,而不是InnoDB层面的锁等待。
区分两者的方法是查看错误日志。如果是Galera的写集冲突,错误信息通常会包含"Deadlock found when trying to get lock; try restarting transaction",并且在Galera的状态日志中能看到对应的事务回滚记录。如果是InnoDB的锁死锁,错误信息会更具体地指出是哪两个事务在等待哪个锁。
写集冲突检测的性能影响
写集冲突检测本身的开销并不大,因为它只是在提交时做一次集合比较操作。但冲突发生后的回滚和重试会带来明显的性能损耗。每次冲突都意味着一个事务的全部工作被丢弃,需要从头再来。在高并发场景下,如果冲突率超过5%-10%,系统的有效吞吐量会急剧下降。
另外,写集的收集和传输也有一定的网络开销。每个事务的写集都需要通过组通信协议广播到所有节点,写集越大,网络传输量越大。对于大批量的INSERT操作,写集可能包含成千上万个行标识,这对网络带宽和节点间的通信效率都是考验。
与其他分布式数据库冲突检测方案的对比
和Galera的认证机制不同,Google Spanner使用的是基于TrueTime的两阶段提交加锁机制,通过全局时钟来确定事务顺序,从根本上避免冲突。CockroachDB则使用了类似Spanner的思路,但基于HLC(Hybrid Logical Clock)实现。TiDB使用的是Percolator模型,通过预写和两阶段提交来处理冲突。这些方案都比Galera的乐观认证更"悲观",在冲突发生前就做了更多的协调工作,但代价是更高的延迟。
Galera的优势在于低延迟和高吞吐,因为它默认假设不冲突。劣势就是冲突发生时的回滚代价。选择哪种方案取决于业务场景:如果写冲突概率低,Galera是非常高效的选择;如果业务天然存在大量并发写同一行的需求,可能需要考虑其他方案或者在应用层做更多的流量调度。
监控与诊断写集冲突的实用建议
要及时发现和定位写集冲突问题,建议重点关注以下几个指标。一是Galera的wsrep_local_cert_failures状态变量,它记录了本地节点认证失败的次数,这个值持续增长说明冲突在加剧。二是wsrep_local_bf_aborts,记录了因为写集太大而被中断的事务数量。三是应用层的死锁重试次数,如果重试频率异常升高,基本可以确认是写集冲突导致的。
通过慢查询日志结合Galera的事务ID,可以追踪到具体是哪些SQL语句在频繁冲突。然后针对性地优化SQL设计、调整数据分片策略或者增加应用层重试逻辑,就能有效缓解问题。
总结来说,Galera Cluster的写集冲突检测是一套设计精巧但需要深入理解才能用好的机制。它用乐观的方式实现了多主同步写入,代价是冲突时的回滚。掌握写集的构成、认证的三阶段流程、冲突判定规则以及降低冲突的实战方法,是驾驭Galera Cluster的基本功。在实际生产环境中,没有完美的方案,只有最适合业务的权衡。
