作为一个 Python 开发工程师,和数据库相关的问题是日常开发中绕不开的话题,尤其是 MySQL 的并发问题,常常成为面试和实际项目中的重点。
今天就和大家聊聊 MySQL 在高并发场景中可能出现的三大经典问题:脏读、不可重复读和幻读。为了更易理解,我还会通过代码举例,带你深入了解这些问题的本质和应对方案。
在多事务并发环境中,数据的一致性和隔离性是核心考量点。这直接关联到 MySQL 的事务隔离级别。
MySQL 支持四种事务隔离级别,从低到高分别是:读未提交(READ UNCOMMITTED)、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)和串行化(SERIALIZABLE)。不同级别决定了数据库对脏读、不可重复读和幻读问题的容忍程度。
脏读:未提交数据被读取
脏读意味着一个事务读取到了另一个未提交事务修改的数据,导致数据可能回滚,变得不可靠。假设一个场景:
# 事务A:更新账户余额
cursor.execute("START TRANSACTION;")
cursor.execute("UPDATE accounts SET balance=5000 WHERE id=1;")
# 注意,此时事务A未提交
# 事务B:读取账户余额
cursor.execute("START TRANSACTION;")
cursor.execute("SELECT balance FROM accounts WHERE id=1;")
balance = cursor.fetchone()
print(f"读取到的余额: {balance}")
如果数据库隔离级别是 READ UNCOMMITTED
,事务 B 会读取到事务 A 修改但未提交的数据。此时,如果事务 A 回滚,事务 B 得到的数据就是无效的,造成了脏读问题。要避免这种情况,推荐将隔离级别提升至 READ COMMITTED
或更高。
不可重复读:同一事务两次读取的数据不一致
不可重复读意味着同一事务在多次读取同一数据时,得到的结果却不同。来看代码示例:
# 事务A:读取账户余额
cursor.execute("START TRANSACTION;")
cursor.execute("SELECT balance FROM accounts WHERE id=1;")
initial_balance = cursor.fetchone()
print(f"第一次读取余额: {initial_balance}")
# 事务B:修改账户余额并提交
cursor.execute("START TRANSACTION;")
cursor.execute("UPDATE accounts SET balance=8000 WHERE id=1;")
cursor.execute("COMMIT;")
# 事务A:再次读取账户余额
cursor.execute("SELECT balance FROM accounts WHERE id=1;")
new_balance = cursor.fetchone()
print(f"第二次读取余额: {new_balance}")
在 READ COMMITTED
隔离级别中,事务 A 会在第二次读取时发现数据已被事务 B 修改,造成不可重复读。如果要求同一事务中多次读取结果一致,可以将隔离级别提升到 REPEATABLE READ
。
幻读:新增或删除的数据影响查询结果
幻读意味着一个事务在多次查询时,发现符合条件的记录数量发生了变化。举个例子:
# 事务A:查询余额大于100万的记录数量
cursor.execute("START TRANSACTION;")
cursor.execute("SELECT COUNT(*) FROM accounts WHERE balance > 1000000;")
initial_count = cursor.fetchone()[0]
print(f"第一次查询记录数量: {initial_count}")
# 事务B:插入新记录并提交
cursor.execute("START TRANSACTION;")
cursor.execute("INSERT INTO accounts (id, balance) VALUES (3, 1200000);")
cursor.execute("COMMIT;")
# 事务A:再次查询余额大于100万的记录数量
cursor.execute("SELECT COUNT(*) FROM accounts WHERE balance > 1000000;")
new_count = cursor.fetchone()[0]
print(f"第二次查询记录数量: {new_count}")
在 REPEATABLE READ
下,MySQL 使用多版本并发控制(MVCC),可以避免不可重复读,但无法完全防止幻读。要杜绝幻读,需要将隔离级别提升到 SERIALIZABLE
,这会导致数据库执行范围锁定,影响性能。
面试官最喜欢的提问:如何选择合适的事务隔离级别?
回答:
选择事务隔离级别需要在数据一致性和系统性能之间找到平衡:
读未提交(READ UNCOMMITTED):性能最好,但可能出现脏读、不可重复读和幻读,适用于对数据一致性要求极低的场景。
读已提交(READ COMMITTED):防止脏读,适合大多数在线事务处理系统(OLTP)。
可重复读(REPEATABLE READ):防止脏读和不可重复读,是 MySQL 默认隔离级别,适用于需要高一致性的大型应用。
串行化(SERIALIZABLE):最高隔离级别,完全避免所有问题,但性能最低,适用于金融、银行等强一致性需求场景。
掌握这些隔离级别的适用场景和潜在问题,能在面试中脱颖而出。记住,数据库优化永远是系统性能和数据可靠性之间的权衡。
对编程、职场感兴趣的同学,大家可以联系我微信:golang404,拉你进入“程序员交流群”。
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》。