Skip to content

数据库系统

数据库的 4 个基本概念

数据、数据库、数据管理系统、数据库系统 是与数据技术密切相关的4个基本概念

事务

事务(Transaction) 是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务的目的是确保数据库的完整性。

事务具有四个标准属性,通常简称为ACID属性:

  1. 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改要么全都执行,要么全都不执行。
  2. 一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态。
  3. 隔离性(Isolation):在事务开始和完成时,中间过程对系统其他事务是不可见的(或称为并发控制)。
  4. 持久性(Durability):一旦事务提交,则其结果就是永久性的,即使系统发生崩溃。

事务隔离级别

包括 读未提交读已提交可重复读串行化四个隔离级别,它们的隔离性依次增加。

隔离级别脏读不可重复读幻读
读未提交READ-UNCOMMITTED
读已提交READ-COMMITTED
可重复读REPEATABLE-READ
串行化(默认)SERIALIZABLE
  • 脏读:一个事务读取到另一个事务修改了但未提交的数据。
  • 不可重复读:一个事物在读取数据后重新读取,无法获取相同的数据(数据被修改,或数据被删除)。
  • 幻读:一个数据在读取数据后重新读取,读取到了其它事务插入的新的数据。

⚠️

使用数据库是 MySQL 8

创建测试数据
sql
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                |
    +--------------------------------+

读未提交

sql
# 关闭自动提交
set autocommit = false;
# 设置事务隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
sql
事务 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 使用了错误的数据处理了业务,即出现了脏读

读已提交

sql
# 关闭自动提交
set autocommit = false;
# 设置事务隔离级别为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
sql
事务 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)

读已提交解决了脏读的问题,但是通过上面可以发现使用相同的查询语句会出现不同的结果,那么在业务中应该使用哪一条数据进行处理就需要考虑了,也就是出现了不可重复读问题。

可重复读

sql
# 关闭自动提交
set autocommit = false;
# 设置事务隔离级别为可重复读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
sql
事务 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 中可以修改其它事务插入的新数据,修改后将查询到新的数据,这就是幻读

串行化

sql
# 关闭自动提交
set autocommit = false;
# 设置事务隔离级别为串行化
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

在这个事务基本,MySQL 会对每一个语句加锁,所以性能极低,但是不是谁先开始事务,谁就先执行,一般情况下是行锁也就是更新或查询行数据时才加锁,比如如下执行流程:

sql
事务 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(表锁)

支持的获取锁的模式有:

  • 共享锁
  • 排他锁
  • 意向锁
  • 记录锁
  • 间隙锁
  • 插入意向锁
  • 自增锁