数据库系统
数据库的 4 个基本概念
数据、数据库、数据管理系统、数据库系统 是与数据技术密切相关的4个基本概念
事务
事务(Transaction) 是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务的目的是确保数据库的完整性。
事务具有四个标准属性,通常简称为ACID属性:
- 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改要么全都执行,要么全都不执行。
- 一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态。
- 隔离性(Isolation):在事务开始和完成时,中间过程对系统其他事务是不可见的(或称为并发控制)。
- 持久性(Durability):一旦事务提交,则其结果就是永久性的,即使系统发生崩溃。
事务隔离级别
包括 读未提交、读已提交、可重复读、串行化四个隔离级别,它们的隔离性依次增加。
隔离级别 | 值 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交 | READ-UNCOMMITTED | ✅ | ✅ | ✅ |
读已提交 | READ-COMMITTED | ❎ | ✅ | ✅ |
可重复读 | REPEATABLE-READ | ❎ | ❎ | ✅ |
串行化(默认) | SERIALIZABLE | ❎ | ❎ | ❎ |
- 脏读:一个事务读取到另一个事务修改了但未提交的数据。
- 不可重复读:一个事物在读取数据后重新读取,无法获取相同的数据(数据被修改,或数据被删除)。
- 幻读:一个数据在读取数据后重新读取,读取到了其它事务插入的新的数据。
⚠️
使用数据库是 MySQL 8
创建测试数据
create database test_transaction;
use test_transaction;
create table tb_balance
(
id int primary key auto_increment,
name varchar(16),
balance int
);
insert into tb_balance(name, balance) value("张三", 100);
insert into tb_balance(name, balance) value("李四", 200);
insert into tb_balance(name, balance) value("王五", 0);
查询事务级别
查询会话隔离级别:
select @@Session.transaction_isolation;
sql+---------------------------------+ | @@Session.transaction_isolation | +---------------------------------+ | REPEATABLE-READ | +---------------------------------+
查询全局隔离级别:
select @@Global.transaction_isolation;
sql+--------------------------------+ | @@Global.transaction_isolation | +--------------------------------+ | REPEATABLE-READ | +--------------------------------+
读未提交
# 关闭自动提交
set autocommit = false;
# 设置事务隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
事务 A > select * from tb_balance where id = 1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 100 |
+----+--------+---------+
1 row in set (0.00 sec)
事务 B > update tb_balance set balance = 0 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
事务 A > select * from tb_balance where id = 1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 0 |
+----+--------+---------+
1 row in set (0.00 sec)
这里事务 A 读取到了事务 B 未提交的数据,若事务 A 使用读取到的数据进行了业务处理,之后事务 B 发生了回滚,就相当于事务 A 使用了错误的数据处理了业务,即出现了脏读。
读已提交
# 关闭自动提交
set autocommit = false;
# 设置事务隔离级别为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
事务 A > select * from tb_balance where id = 1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 100 |
+----+--------+---------+
1 row in set (0.00 sec)
事务 B > update tb_balance set balance = 0 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
事务 A > select * from tb_balance where id = 1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 100 |
+----+--------+---------+
1 row in set (0.00 sec)
事务 B > commit;
Query OK, 0 rows affected (0.01 sec)
事务 A > select * from tb_balance where id = 1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 0 |
+----+--------+---------+
1 row in set (0.01 sec)
读已提交解决了脏读的问题,但是通过上面可以发现使用相同的查询语句会出现不同的结果,那么在业务中应该使用哪一条数据进行处理就需要考虑了,也就是出现了不可重复读问题。
可重复读
# 关闭自动提交
set autocommit = false;
# 设置事务隔离级别为可重复读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
事务 A > select * from tb_balance where id = 1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 0 |
+----+--------+---------+
1 row in set (0.00 sec)
事务 B > update tb_balance set balance = 100 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
事务 A > select * from tb_balance where id = 1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 0 |
+----+--------+---------+
1 row in set (0.00 sec)
事务 B > commit;
Query OK, 0 rows affected (0.00 sec)
事务 A > select * from tb_balance where id = 1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 0 |
+----+--------+---------+
1 row in set (0.00 sec)
事务 A > select * from tb_balance where balance >= 100;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 2 | 李四 | 200 |
+----+--------+---------+
1 row in set (0.00 sec)
事务 C > insert into tb_balance(name, balance) value('老六', 200);
Query OK, 1 row affected (0.00 sec)
事务 C > commit;
Query OK, 0 rows affected (0.00 sec)
事务 A > select * from tb_balance where balance >= 100;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 2 | 李四 | 200 |
+----+--------+---------+
1 row in set (0.00 sec)
事务 A > update tb_balance set balance = balance + 100 where id = 4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
事务 A > select * from tb_balance where balance >= 100;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 2 | 李四 | 200 |
| 4 | 老六 | 300 |
+----+--------+---------+
2 rows in set (0.00 sec)
这里出现了事务 A 开始时没有的数据,原因是在可重复读的隔离级别,其他事务修改开始时是存在的数据,重复读取不会返回不同的数据,但是在事务 A 中可以修改其它事务插入的新数据,修改后将查询到新的数据,这就是幻读。
串行化
# 关闭自动提交
set autocommit = false;
# 设置事务隔离级别为串行化
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
在这个事务基本,MySQL 会对每一个语句加锁,所以性能极低,但是不是谁先开始事务,谁就先执行,一般情况下是行锁也就是更新或查询行数据时才加锁,比如如下执行流程:
事务 A >
select * from tb_balance;
# 会正常查询输出所有数据。
事务 B > update tb_balance set balance = 0 where id = 1;
# 会卡住,也是就是在获取锁。事务 A 提交后会正常执行。
锁
使用 show processlist;
可以查看当前执行的 SQL 信息。
在information.innodb_trx 中可以查看一些锁信息。
在 performance_schema.data_locks`可查看到更多锁信息
InnoDB 引擎
支持锁类型为:
- RECORD(行锁)
- TABLE(表锁)
支持的获取锁的模式有:
- 共享锁
- 排他锁
- 意向锁
- 记录锁
- 间隙锁
- 插入意向锁
- 自增锁