数据库事务

提到事务大家肯定都不陌生,最典型的例子就是银行转账。如果从账户A转100元到账户B,主要执行:

  1. 检查账户A的余额
  2. 如果余额足够,从账户A的余额扣除100元
  3. 账户B余额添加100元

我们需要保证上面3步动作是不可分割的。否则,我们完全可以在步骤1和步骤2中间,再次发起另外一笔转账,这时候余额还没有扣除,检查仍然通过,这样银行不就乱套了吗?

事务的概念:

数据库事务通常包含了一个序列的对数据库的读/写操作。包含有以下两个目的:

  • 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
  • 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。

并非任意的对数据库的操作序列都是数据库事务。数据库事务拥有以下四个特性,习惯上被称之为ACID特性:

  • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行
  • 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行
  • 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中

原子性、一致性和持久性都很好理解,我们今天就来说说是隔离性。

事务的隔离

当数据库上有多个事务并发执行的时候,就有可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题:

  • 脏读:读取到其他事务未提交的数据,因为这条数据之后可能会被回滚,导致我们当前读取到的是脏数据
  • 不可重复读:一个事务中前后读取同一条数据两次,并且在两次读取中间,有其他事务修改了这条记录并提交,导致前后两次读到的数据不一致
  • 幻读:一个事务前后对同一个条件进行两次搜索,并且两次搜索中间有其他事务提交了更新,导致前后两次查询得到了不同的结果集。一个简单的例子,事务A需要查询两次id between 1 and 100,第一次查询得到了10条数据,这时候另一个事务插入了一条id在100以内的数据,事务A再次查询就会得到11条数据,比前一次查询多了一条。

数据不一致的问题实际上是并发编程很常见的问题,如果每个每个事务都串行执行,那么上面的问题就都不存在了。

在解决并发时的数据不一致问题时,我们常常通过加锁来解决。但是,如果加锁粒度太大,并发就上不去。

SQL标准根据并发事务可能出现的问题,定义了不同的事务隔离级别,隔离级别越高,串行化程度就越高,程序的并发也就越低,因此很多时候,我们需要在二者之间寻找一个平衡点。

这些隔离级别从低到高分别是:

  • 读未提交(Read Uncommitted):允许脏读,一个事务还没提交,它的变更就能被别的事务看见
  • 读已提交(Read Committed):解决了脏读问题,一个事务只有提交之后,它的变更才能被别的事务看到
  • 可重复读(Repeatable Read):解决了不可重复读问题,一个事务执行过程中看到的数据总是跟这个事务在启动时看到的数据是一致的。mysql通过mvcc提供快照读,两个事务的读写不会冲突。标准中只要求该级别解决不可重复读问题,但是 mysql的可重复读还通过gap锁解决了幻读现象。该级别是mysql的默认隔离级别。
  • 序列化(Serializable):这是最高的隔离级别,也是SQL标准中的默认隔离级别。A serializable execution is defined to be an execution of the operations of concurrently executing SQL-transactions that produces the same effect as some serial execution of those same SQL-transactions. A serial execution is one in which each SQL-transaction executes to completion before the next SQL-transaction begins.

上面的四种隔离级别,从上往下,隔离性越来越高,但是性能也越来越差。我们一般不会使用序列化隔离级别,该级别会导致大量的事务超时。mysql默认的隔离级别是可重复读级别,并且解决了幻读问题。有时候,当我们对数据的隔离性要求不高的时候,可以将其修改为读已提交级别。

mysql中查看当前隔离级别:

1
2
3
4
5
6
7
8
$ mysql -p$MYSQL_ROOT_PASSWORD
mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.01 sec)

设置隔离级别:

1
mysql> SET [global|session] transaction isolation LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE];

1
2
mysql> SET session transaction isolation LEVEL READ COMMITTED; # 当前session级别
mysql> SET global transaction isolation LEVEL READ COMMITTED; # 全局生效