MySQL并发问题解析
MySQL作为最流行的关系型数据库之一,其并发性能直接影响着整个应用的稳定性和效率。然而,并发访问不可避免地会带来一系列问题,如脏读、不可重复读、幻读以及死锁等。
一、MySQL并发带来的问题
1. 脏读
定义:一个事务读取了另一个未提交事务的数据。
场景示例:
- 事务A修改了某条记录但尚未提交。
- 事务B读取了这条被修改的记录。
- 事务A回滚,数据恢复原状。
- 事务B读到的数据是“脏”的,从未真正存在过。
2. 不可重复读
定义:在同一事务中,多次读取同一数据返回的结果不一致。
场景示例:
- 事务A第一次读取某条记录。
- 事务B修改了该记录并提交。
- 事务A再次读取同一条记录,发现数据已变。
3. 幻读
定义:一个事务在执行过程中,前后两次查询同一范围的数据,但结果集不一致。
场景示例:
- 事务A查询年龄大于20的用户有10条。
- 事务B插入了一条年龄为25的新用户并提交。
- 事务A再次查询,发现结果变为11条。
4. 丢失更新
定义:两个事务同时读取同一数据并修改,后提交的事务覆盖了前一个事务的修改。
场景示例:
- 事务A和B同时读取账户余额为100。
- A执行+50,B执行-30。
- 若B后提交,最终余额为70,A的+50操作被覆盖。
5. 死锁
定义:两个或多个事务相互等待对方释放锁,导致所有事务都无法继续执行。
场景示例:
- 事务A持有表1的锁,请求表2的锁。
- 事务B持有表2的锁,请求表1的锁。
- 双方陷入无限等待。
二、MySQL的隔离级别与并发控制
MySQL通过事务隔离级别来控制并发行为,不同级别提供不同的数据一致性保证。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(READ UNCOMMITTED) | √ | √ | √ |
读已提交(READ COMMITTED) | × | √ | √ |
可重复读(REPEATABLE READ) | × | × | × |
串行化(SERIALIZABLE) | × | × | × |
三、并发问题的解决方案
1. 合理使用事务隔离级别
- 高一致性要求:使用
SERIALIZABLE
,但性能开销大。 - 平衡选择:
REPEATABLE READ
是InnoDB默认级别,能有效防止脏读、不可重复读和幻读。 - 高并发读场景:
READ COMMITTED
适用于读多写少、可容忍轻微不一致的场景。
2. 显式加锁控制
共享锁(S锁)与排他锁(X锁)
-- 共享锁:允许其他事务读,但不能写
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;-- 排他锁:不允许其他事务读写
SELECT * FROM users WHERE id = 1 FOR UPDATE;
乐观锁 vs 悲观锁
悲观锁:假设冲突频繁,提前加锁。
乐观锁:假设冲突少,通过版本号或时间戳控制。
3. 避免死锁的策略
按固定顺序访问资源:所有事务以相同顺序访问表和行。
减少事务持有锁的时间:尽快提交事务。
使用索引:避免全表扫描,减少锁的范围。
设置超时:
innodb_lock_wait_timeout
控制等待时间。死锁检测:MySQL自动检测并回滚代价较小的事务。
4. 使用连接池优化
高并发下频繁创建/销毁连接消耗资源。使用连接池(如HikariCP、Druid)复用连接,提升性能。
5. 分库分表与读写分离
读写分离:主库写,从库读,减轻主库压力。
分库分表:将大表拆分为多个小表,分散并发压力。
6. 使用缓存层
在应用与数据库之间加入缓存(如Redis),减少直接访问数据库的频率,尤其适用于热点数据。