MySql 事务和锁

可以把 MySQL 的事务和锁 理解成两个相关但不同的东西:

事务:保证"一组操作要么全部成功,要么全部失败"。

锁:保证"多个操作同时修改/读取数据时,不要互相搞乱"。

你现在如果在学 MySQL / Spring Boot,这两个概念非常重要。


一、先理解什么是事务

比如银行转账:

复制代码
张三账户 -100
李四账户 +100

这其实是两个 SQL:

ini 复制代码
UPDATE account SET money = money - 100 WHERE name = '张三';

UPDATE account SET money = money + 100 WHERE name = '李四';

如果第一条成功,第二条失败:

复制代码
张三:少了 100
李四:没收到 100

这显然不行。

所以我们希望:

sql 复制代码
开始事务
   ↓
张三 -100
   ↓
李四 +100
   ↓
全部成功 → COMMIT
   ↓
只要一个失败 → ROLLBACK

这就是事务


二、MySQL 怎么开启事务?

ini 复制代码
START TRANSACTION;

UPDATE account
SET money = money - 100
WHERE name = '张三';

UPDATE account
SET money = money + 100
WHERE name = '李四';

COMMIT;

如果中间出错:

ini 复制代码
ROLLBACK;

例如:

ini 复制代码
START TRANSACTION;

UPDATE account
SET money = money - 100
WHERE name = '张三';

-- 假设这里发生错误

ROLLBACK;

那么前面的修改也会撤销。


三、事务最重要的 4 个特性:ACID

这个面试非常常见。

1. Atomicity 原子性

一组操作,要么全部成功,要么全部失败。

比如:

复制代码
转账

张三 -100
李四 +100

不能出现:

复制代码
张三 -100
李四 没 +100

所以:

事务是一个不可再分的整体。


2. Consistency 一致性

事务执行前后,数据库都应该处于合法状态

比如:

yaml 复制代码
转账前:

张三 1000
李四 1000

总金额 = 2000

转账 100 后:

yaml 复制代码
张三 900
李四 1100

总金额 = 2000

总金额不能莫名其妙变成:

yaml 复制代码
1900

3. Isolation 隔离性

多个事务同时执行的时候,互相不能随便干扰

例如:

css 复制代码
事务 A:修改张三余额

事务 B:同时查询张三余额

B 到底应该看到修改前还是修改后的数据?

这就是事务隔离要解决的问题。


4. Durability 持久性

事务一旦:

ini 复制代码
COMMIT;

成功提交:

数据就应该永久保存。

即使数据库之后重启,也不能因为重启导致刚提交的数据消失。


四、那锁又是什么?

假设现在有一个商品:

复制代码
商品库存 = 10

突然有两个用户同时购买。

css 复制代码
用户 A:买 1 个
用户 B:买 1 个

如果没有任何控制,可能发生:

css 复制代码
A 读取库存:10
B 读取库存:10

A:10 - 1 = 9
B:10 - 1 = 9

最终库存:9

但实际上应该是:

ini 复制代码
10 - 1 - 1 = 8

这就是并发问题


五、锁就是解决并发问题的

比如 A 要修改库存:

ini 复制代码
UPDATE product
SET stock = stock - 1
WHERE id = 1;

MySQL 会对相关数据加锁。

可以简单理解成:

css 复制代码
A:
我要修改商品 1

        🔒 商品1

B:
我要修改商品 1

        ⏳ 等待

A 完成:

sql 复制代码
COMMIT

之后:

复制代码
🔓 商品1

B 才能继续。

所以:

锁就是为了控制并发访问。


六、事务和锁是什么关系?

这是最容易混淆的地方。

你可以这么理解:

复制代码
事务
 ↓
管理一组数据库操作
 ↓
保证这组操作的完整性

而:

复制代码
锁
 ↓
控制多个事务之间的并发访问
 ↓
防止数据冲突

两者经常一起出现。

例如:

ini 复制代码
START TRANSACTION;

SELECT stock
FROM product
WHERE id = 1
FOR UPDATE;

UPDATE product
SET stock = stock - 1
WHERE id = 1;

COMMIT;

这里:

sql 复制代码
START TRANSACTION

事务

而:

sql 复制代码
FOR UPDATE

是在请求排他锁


七、FOR UPDATE 是什么?

这个在 Spring Boot 开发中很常见。

假设:

ini 复制代码
SELECT stock
FROM product
WHERE id = 1;

普通查询。

如果改成:

sql 复制代码
SELECT stock
FROM product
WHERE id = 1
FOR UPDATE;

意思可以理解成:

我要读取这条数据,而且接下来我要修改它,请先把它锁住。

例如库存扣减:

ini 复制代码
START TRANSACTION;

SELECT stock
FROM product
WHERE id = 1
FOR UPDATE;

UPDATE product
SET stock = stock - 1
WHERE id = 1;

COMMIT;

并发情况下:

sql 复制代码
事务 A
   ↓
SELECT ... FOR UPDATE
   ↓
🔒 锁住商品
   ↓
修改库存
   ↓
COMMIT
   ↓
🔓 解锁


事务 B
   ↓
SELECT ... FOR UPDATE
   ↓
等待 A 释放锁
   ↓
拿到最新库存
   ↓
继续操作

这就避免了很多并发修改问题。


八、MySQL 常见的锁

先不用记得特别细,建立一个整体概念:

markdown 复制代码
MySQL 锁
│
├── 按锁的粒度
│   ├── 表锁
│   └── 行锁
│
└── 按锁的性质
    ├── 共享锁
    └── 排他锁

1. 表锁

锁住整张表。

比如:

bash 复制代码
user 表

┌──────────────────┐
│ id │ name │ age  │
├──────────────────┤
│ 1  │ 张三 │ 18   │
│ 2  │ 李四 │ 20   │
│ 3  │ 王五 │ 22   │
└──────────────────┘

        🔒
      整张表

其他事务可能无法正常修改这张表。

优点:

简单。

缺点:

并发能力比较差。


九、行锁

行锁就是:

只锁某一行。

比如:

bash 复制代码
user 表

id   name
1    张三   🔒
2    李四
3    王五

事务 A 修改:

ini 复制代码
UPDATE user
SET name = '张三三'
WHERE id = 1;

主要锁住:

ini 复制代码
id = 1

这时候另一个事务修改:

ini 复制代码
UPDATE user
SET name = '李四四'
WHERE id = 2;

通常可以正常进行。

所以:

行锁并发能力比表锁好很多。

InnoDB 就大量使用行级锁。


十、共享锁和排他锁

这两个也非常重要。

共享锁 S Lock

Shared Lock。

可以简单理解:

大家都可以读,但不能随便修改。

例如:

css 复制代码
事务 A:读 🔒
事务 B:读 🔒

可以

事务 C:修改 ❌

排他锁 X Lock

Exclusive Lock。

可以理解成:

我现在要修改,你们先别动。

例如:

css 复制代码
事务 A:

🔒 X锁

其他事务:

读取/修改
    ↓
可能需要等待

SELECT ... FOR UPDATE 通常就是为了获取排他性质的锁。


十一、为什么又出现"事务隔离级别"?

因为多个事务同时运行时,会出现各种问题。

例如:

css 复制代码
事务 A
事务 B

A 修改数据的时候,B 到底能不能看到?

所以 MySQL 提供了不同的事务隔离级别

主要有 4 个:

markdown 复制代码
READ UNCOMMITTED
        ↓
READ COMMITTED
        ↓
REPEATABLE READ
        ↓
SERIALIZABLE

隔离性越来越强,并发能力通常越来越弱。


十二、READ UNCOMMITTED

最低级别。

事务 A 修改:

yaml 复制代码
余额 = 1000
↓
修改成 500

还没:

ini 复制代码
COMMIT;

事务 B 就可能读到:

复制代码
500

但是 A 最后:

ini 复制代码
ROLLBACK;

实际上数据库又变回:

yaml 复制代码
1000

于是 B 刚才读到了一个根本不存在的最终数据

这叫:

脏读


十三、READ COMMITTED

要求:

只能读取已经提交的数据。

所以:

css 复制代码
A 修改
 ↓
500
 ↓
未提交

B 查询
 ↓
看到 1000

A:

ini 复制代码
COMMIT;

B 再查询:

复制代码
看到 500

这样可以解决:

脏读

但是会出现另一个问题:

css 复制代码
事务 B:

第一次查询:
1000

事务 A:
修改成 500
COMMIT

事务 B:

第二次查询:
500

同一个事务里:

复制代码
第一次:1000
第二次:500

这叫:

不可重复读


十四、REPEATABLE READ

意思就是:

同一个事务里,多次读取同一条数据,结果尽量保持一致。

MySQL InnoDB 默认就是:

复制代码
REPEATABLE READ

例如:

css 复制代码
事务 B:

第一次查询:
1000

事务 A:
修改成 500
COMMIT

事务 B:

第二次查询:
1000

这样 B 在自己的事务里看到的数据是一致的。


十五、SERIALIZABLE

最严格。

可以理解成:

大家排队执行。

并发能力最低,但是隔离性最高。

简单理解:

css 复制代码
事务 A
 ↓
执行完
 ↓
事务 B
 ↓
执行完
 ↓
事务 C

当然 MySQL 实际实现比这个复杂,这里先建立概念就够了。


十六、你可以把整个东西串起来

你现在学习 MySQL,可以形成这张脑图:

markdown 复制代码
                 MySQL
                   │
          ┌────────┴────────┐
          │                 │
        事务                锁
          │                 │
    一组操作的整体性       控制并发
          │                 │
      ACID              防止数据冲突
          │                 │
          └───────┬─────────┘
                  │
             事务隔离
                  │
       ┌──────────┼──────────┐
       │          │          │
      RU         RC         RR        Serializable
       │          │          │             │
      脏读       不可重复读   幻读等       最严格

十七、结合你以后写 Spring Boot 来看

你以后可能会看到:

typescript 复制代码
@Transactional
public void transfer() {

    accountMapper.decreaseMoney(1, 100);

    accountMapper.increaseMoney(2, 100);
}

这里:

css 复制代码
@Transactional

就是告诉 Spring:

这个方法里的数据库操作放到一个事务里面。

于是:

scss 复制代码
方法开始
   ↓
开启事务
   ↓
decreaseMoney()
   ↓
increaseMoney()
   ↓
全部成功
   ↓
COMMIT

如果中间异常:

scss 复制代码
decreaseMoney()
   ↓
increaseMoney()
   ↓
发生异常
   ↓
ROLLBACK

而如果你又写:

python 复制代码
@Select("""
    SELECT *
    FROM product
    WHERE id = #{id}
    FOR UPDATE
""")
Product selectForUpdate(Long id);

那么:

java 复制代码
@Transactional
public void buy() {

    Product product = mapper.selectForUpdate(id);

    // 扣库存
}

就是:

sql 复制代码
@Transactional
        ↓
      事务
        ↓
SELECT ... FOR UPDATE
        ↓
       加锁
        ↓
     修改数据
        ↓
      COMMIT

事务负责"这一整套操作是不是一个完整的整体",锁负责"并发情况下别人能不能同时动这份数据"。

你如果正在从 Java 基础往 Spring Boot 过渡,建议下一步重点搞懂 "脏读、不可重复读、幻读 + MySQL 的 MVCC" 。这几个理解了,事务和锁基本就串起来了。

相关推荐
步行cgn12 分钟前
Lombok 详解:通过注解消除样板代码
后端
一只小闪闪20 分钟前
easy-trans java数据通用翻译框架v2.3.0
java·后端·架构
2601_9620558122 分钟前
Spring全部注解
java·后端·spring
步行cgn1 小时前
Spring Boot 启动报错:MissingServletWebServerFactoryBean 的排查与解决
java·spring boot·后端
步行cgn2 小时前
Spring Boot 项目打包部署详解
java·spring boot·后端
我的xiaodoujiao2 小时前
Django 基础知识详细图文教程 4-Django 视图定义与使用
开发语言·数据库·后端·测试工具·django·sqlite
Apifox2 小时前
Apifox 8 月更新|调试、权限与协作体验持续优化
前端·后端·测试
kyson_2 小时前
后端登录凭证设计:Session-Cookie、Redis-Token 与 JWT
后端
对象存储与RustFS3 小时前
RustFS 后台扫描器与自愈调参:五档速度、位腐检测周期与并发上限
后端·rust·开源