今天我们来聊聊 MySQL 中与并发相关的一些问题。作为一名资深 Python 开发工程师,我觉得这些问题不仅关乎数据库的稳定性和数据的一致性,更与我们的代码实现和业务逻辑密切相关。
尤其是在高并发环境下,如何保证数据的一致性,如何防止脏读、不可重复读和幻读等问题,真的是每个程序员都必须知道的内容。今天就通过一些简单的代码示例和场景说明,让大家更加直观地理解这些问题。
首先,我们知道 MySQL 服务端支持多个客户端同时连接,这就意味着 MySQL 会在多个事务并发执行时进行资源的竞争和调度。
而并发执行的事务中,如果没有妥善的控制,就可能会遇到一些数据一致性问题。常见的这些问题包括脏读(Dirty Read)、不可重复读(Non-repeatable Read)和幻读(Phantom Read)。那么,具体它们是怎么发生的呢?
脏读(Dirty Read)
脏读是指一个事务读到了另一个事务未提交的脏数据。如果事务 A 修改了数据,但事务 A 并没有提交,而事务 B 读取到了事务 A 修改后的数据,那就产生了脏读。如果事务 A 随后回滚了,那么事务 B 得到的数据就是无效的。这种现象就叫做脏读。
举个例子,假设我们有两个事务 A 和 B,事务 A 从数据库中读取了小林的余额数据,然后进行了一次修改,但没有提交。与此同时,事务 B 也读取了小林的余额数据,这时事务 B 看到的余额已经是事务 A 修改后的数据了,即便事务 A 最终回滚了。
-- 事务 A
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE name = '小林';
-- 事务 A 没有提交
-- 事务 B
START TRANSACTION;
SELECT balance FROM account WHERE name = '小林'; -- 读到了事务 A 更新后的数据
-- 事务 B 没有更新数据,但可能会被脏数据影响
如果事务 A 最后执行回滚,那么事务 B 得到的数据就是过期数据,这就属于脏读。
如何避免脏读?
可以通过设置事务隔离级别来避免脏读,常用的隔离级别是 READ COMMITTED
。在这个隔离级别下,事务 B 不会读取未提交的数据,从而避免了脏读的发生。
不可重复读(Non-repeatable Read)
不可重复读是指在同一个事务中,多次读取同一数据时,如果中间有其他事务修改了数据,就会导致前后两次读取的结果不一致。
举个简单的例子,事务 A 从数据库中读取了小林的余额数据并进行了一些处理,接着事务 B 修改了余额并提交了。等事务 A 再次读取余额时,看到的数据就和第一次不一样了,这就是不可重复读。
-- 事务 A
START TRANSACTION;
SELECT balance FROM account WHERE name = '小林'; -- 读到 1000 元
-- 假设事务 A 在处理中
-- 事务 B
START TRANSACTION;
UPDATE account SET balance = balance + 500 WHERE name = '小林'; -- 修改余额
COMMIT;
-- 事务 A 再次读取
SELECT balance FROM account WHERE name = '小林'; -- 读到 1500 元
在上面的代码中,事务 A 在第一次读取时读到的是 1000 元,而在第二次读取时,却读到了 1500 元。造成这种现象的原因是事务 B 在事务 A 执行过程中对数据进行了修改,导致了数据不一致。
如何避免不可重复读?
可以通过使用更高的事务隔离级别来避免,比如 REPEATABLE READ
。在这个隔离级别下,事务 A 不会因为其他事务的提交而看到不一致的数据。
幻读(Phantom Read)
幻读是指在同一个事务中,重复执行相同的查询时,查询结果的数量发生了变化。这种问题通常发生在对数据行进行插入、删除、更新等操作时。
如果在事务 A 查询某个条件下的记录时,事务 B 在事务 A 执行查询的过程中插入了符合条件的新记录,那么事务 A 进行第二次查询时就会看到额外的记录,从而产生幻读现象。
举个例子,假设事务 A 查询余额大于 100 万的所有账户,得到了 5 条记录,然后事务 B 插入了一条新的余额大于 100 万的记录,提交事务后,事务 A 再次查询时发现记录数量变成了 6 条。这样就出现了幻读。
-- 事务 A
START TRANSACTION;
SELECT COUNT(*) FROM account WHERE balance > 1000000; -- 查询到 5 条记录
-- 假设事务 A 在处理中
-- 事务 B
START TRANSACTION;
INSERT INTO account (name, balance) VALUES ('小张', 2000000); -- 插入一条新记录
COMMIT;
-- 事务 A 再次查询
SELECT COUNT(*) FROM account WHERE balance > 1000000; -- 查询到 6 条记录
在这个例子中,事务 A 在第一次查询时得到了 5 条记录,而在第二次查询时却得到了 6 条记录,产生了幻读现象。
如何避免幻读?
为了避免幻读问题,可以使用 SERIALIZABLE
隔离级别,它会强制事务之间的互斥,确保在一个事务运行期间不会有其他事务修改或插入数据,从而避免了幻读。
如何在 MySQL 中设置隔离级别?
MySQL 提供了不同的事务隔离级别,每种隔离级别可以有效地解决某些并发问题。常用的隔离级别有以下几种:
READ UNCOMMITTED:最低的隔离级别,允许脏读。 READ COMMITTED:防止脏读,但允许不可重复读和幻读。 REPEATABLE READ:防止脏读和不可重复读,但允许幻读(MySQL 默认使用此级别)。 SERIALIZABLE:最高的隔离级别,防止所有并发问题,但性能可能较差。
我们可以通过以下语句来设置事务隔离级别:
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
如果面试官问你:如何理解 MySQL 的事务隔离级别?
你的回答:
MySQL 提供了四种常见的事务隔离级别,每个级别的作用和影响如下:
READ UNCOMMITTED:事务可以读取其他事务未提交的数据,可能出现脏读、不可重复读和幻读问题。 READ COMMITTED:事务只能读取已提交的数据,避免了脏读问题,但仍可能发生不可重复读和幻读。 REPEATABLE READ:事务在整个生命周期内读取的数据是固定的,避免了脏读和不可重复读问题,但在 MySQL 中,仍然可能出现幻读。 SERIALIZABLE:事务完全串行化,避免了脏读、不可重复读和幻读问题,但会大幅影响性能。
在实际开发中,根据业务的需求选择合适的事务隔离级别非常重要。例如,如果对数据一致性要求非常高,可以选择 SERIALIZABLE
,但如果对性能要求较高,可能会选择 REPEATABLE READ
或 READ COMMITTED
。
对编程、职场感兴趣的同学,大家可以联系我微信:golang404,拉你进入“程序员交流群”。
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》。