数据库中的事务和锁总结

数据库事务

简单来说,事务就是满足ACID特性的一系列操作。我们可以用commit提交一个事务,也可以用rollback回滚事务。

- 事务开启
BEGIN
- 这里可以是一系列增删改查
COMMIT;
- ROLLBACK;

ACID

Atomicity原子性

即事务要么全部成功要么全部失败,中间有步骤失败就应该回滚。

例如A账户给B账户转账

begin transaction
update account set money = money - 100 where name='A';
update account set money = money + 100 where name='B';
if Error then
  rollback
else
  commit

如果没有原子性的话,A扣了100块钱后,突然断电了,那么B就没有加100,A却少了100。很明显这是不对的。

Consistency一致性

数据库在事务执行前后都保持一致性状态。

在一致性状态下,所有事务对一个数据的读取结果都是相同的。

对银行转帐事务,假设在事务开始之前,A和B的账户总额为2000,那么不管事务成功还是失败,应该保证事务结束后ACCOUNT表中A和B的存款总额依旧为2000。

Isolation隔离性

一个事务所做的修改在最终提交以前,对其它事务是不可见的。

例如两个事务都在对同个数据进行操作,如果没有相互隔离,那么将引起数据的不一致,进而可能导致错误。

举个栗子,假设有如下一张表:

mysql> CREATE TABLE t(val int) engine=InnoDB;
INSERT INTO t(val) values(1);

目前该表有一个字段,而且有一条数据,该字段值为1。接下来有两个事务,都对该表做如下操作

事务A 事务B
启动事务,查询得到值1 启动事务
- 查询得到值1
- 将值1改为2
查询得到值V1 -
- 提交事务
查询得到值V2 -
提交事务 -
查询得到值V3 -

我们通过不同的隔离级别来分析V1、V2和V3的值

未提交读(read uncommitted)

在这种级别下V1、V2、V3都是2,可以看到的是事务B在为提交时,V1查询到的结果已经是被事务B修改后的值。

也就是说在这种级别下,事务中的修改,即使没有提交,对其它事务也是可见的

这种隔离级别基本不会被用到。

提交读(read committed)

在这种级别下,V1是1,V2和V3是2,可见其他事务在没有提交前,所做的修改对其他事务都是不可见的,一个事务只能读取已经提交的事务所做的修改。

这种级别,我们每次读取的时候就能够读取已经提交的数据。

可重复读(repeatable read)

在这种级别下V1、V2都是1,V3是2,可见事务开始后整个过程的数据都是一致的,前后读到的数据都不会变。

很多时候,我们希望在事务里面,只要我们没有修改数据,那么数据就不应该有变化,无论我们读取多少次,这就是可重复读,保证在同一个事务中多次读取同样数据的结果是一样的,这也就是可重复读的作用。

可串行化(serializable)

在这种级别下,事务B在执行到修改val值为2时会被锁住,必须等到事务A提交后才可以继续执行。此时V1和V2都是1,V3是2。

如果在执行事务的时候,有其他事务正在执行,那么它将等待其他事务执行结束之后才能被执行。相当于给数据加了锁。

隔离性如何实现的

数据库会根据不同隔离级别判断如何创建和使用视图,通过视图来获取数据

隔离级别 视图创建时机
可重复读 视图在事务启动时创建,整个事务期间都使用该视图
提交读 视图在sql语句执行的时候创建
未提交读 不使用视图
串行化 不使用视图,采用加锁的方式

在mysql中,每条记录更新的时候都会同时记录一条回滚操作,通过这个回滚操作,都可以得到前一个状态的值

例如在一个事务里,一个字段从1改成2,再从2改成3,最终从3改成4,那么便会产生3个回滚操作:

-----------------------------------------
| (回滚段)                               |
|将2回滚为1 <--- 将3回滚成2 <---- 将4回滚成3 |  <----- 当前值4
-----------------------------------------

由此产生了数据的不同版本,这称之为MVCC(多版本并发控制)

具体的可以看这几篇文章:
数据库事务特征、数据库隔离级别,以及各级别数据库加锁情况(含实操)–read uncommitted篇
MySQL数据库事务各隔离级别加锁情况–read committed && MVCC
MySQL数据库事务各隔离级别加锁情况–Repeatable Read && MVCC

排他锁(X锁,悲观锁)

很多时候我们并不需要那么高的隔离级别,我们通过一种叫排他锁的也能够实现可串行化。

例如:select * from goods where id = 1 for update;

排他锁的申请前提:没有线程对该结果集中的任何行数据使用排他锁或共享锁,否则申请会阻塞。

for update仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效。在进行事务操作时,通过“for update”语句,MySQL会对查询结果集中每行数据都添加排他锁,其他线程对该记录的更新与删除操作都会阻塞。排他锁包含行锁、表锁。

假设有A、B两个用户同时各购买一件 id=1 的商品,用户A获取到的库存量为 1000,用户B获取到的库存量也为 1000,用户A完成购买后修改该商品的库存量为 999,用户B完成购买后修改该商品的库存量为 999,此时库存量数据产生了不一致。

此时我们可以采用排他锁

begin;
select * from product where id=1 for update;
update product set count=count-1 where id = 1;
commit;

每次获取商品时,对该商品加排他锁。也就是在用户A获取获取 id=1 的商品信息时对该行记录加锁,期间其他用户阻塞等待访问该记录。悲观锁适合写入频繁的场景。

乐观锁

加锁会对数据库有一定的性能损耗,因此我们还可以通过一种叫乐观锁的机制来实现上述过程

# 先读取产品数量
select * from product where id=1;

# 接下来开启事务
begin;
update product set count=count-1 where id = 1 and account = currCount;
commit;

注意这里的and account = currCount,通过程序中获取到的产品数量与数据库中的产品量相等才执行更新。

每次获取商品时,不对该商品加锁。在更新数据的时候需要比较程序中的库存量与数据库中的库存量是否相等,如果相等则进行更新,反之程序重新获取库存量,再次进行比较,直到两个库存量的数值相等才进行数据更新。乐观锁适合读取频繁的场景。

当然我们也可以给表加一个版本字段,每次更新行的数据时,版本就加1。然后更新的时候判断版本也是可以的。

Durability持久性

一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。

可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。