数据库Transaction应用与实现

为什么会有Transactions?

在数据系统中很多事情可能会发生,比如:

  • 数据库软件、硬件可能在任意时刻发⽣生故障(包括写操作进⾏到一半时);
  • 应⽤用程序可能在任意时刻崩溃;
  • ⽹网络中断可能会意外切断数据库与应⽤用的连接,或数据库之间的连接;
  • 多个客户端可能会同时写入数据库,覆盖彼此的更更改;
  • 客户之间的竞争条件可能导致令⼈人惊讶的错误;

在这些场景中,很多是基于数据库的应用方无法解决的,所以有了Transaction的概念。但是Transaction的发展也并不是一帆风顺的,理论上数据库应用方希望的体验是,满足高并发,所有的竞态问题都由数据库所解决,提供高数据一致性的体验,但是现实世界并不完美,因为并发就会引入资源竞争的问题。

事务的产生,其实是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题.可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的.而不是伴随着数据库系统天生就有的.

ACID里的AID都是数据库的特征,也就是依赖数据库的具体实现.而唯独这个C,实际上它依赖于应用层,也就是依赖于开发者.这里的一致性是指系统从一个正确的状态,迁移到另一个正确的状态.什么叫正确的状态呢?就是当前的状态满足预定的约束就叫做正确的状态.而事务具备ACID里C的特性是说通过事务的AID来保证我们的一致性.

做个比喻事务就好比一个保镖,我们提到事务就会说ACID,而我们提到保镖会说强壮,保护安全,好功夫,踏实.这里强壮,好功夫和踏实都是保镖自己的特征,而安全是属于你的,而你通过保镖的特征来保护你的安全.

但是这里必须注意,我们能够通过AID保证我们的一致性,但事务本身没办法确保.用上面保镖的例子来说,你正常被保护一定是安全的,但是你故意骗保镖离开你身边,然后你自己溜出去玩.同样,如果你在事务里故意写出违反约束的代码,比如银行系统定时每天给自己的账户打入100w,这个事务是没办法的.

这里我们举个大家都在说的财务系统的例子.

A要向B支付100元,而A的账户中只有90元,并且我们给定账户余额这一列的约束是,不能小于0.那么很明显这条事务执行会失败,因为90-100=-10,小于我们给定的约束了.

这个例子里,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库数据就破坏约束了,因此事务不能成功,这里我们说事务提供了一致性的保证.然后我们再看个例子

A要向B支付100元,而A的账户中只有90元,我们的账户余额列没有任何约束.但是我们业务上不允许账户余额小于0.因此支付完成后我们会检查A的账户余额,发现余额小于0了,于是我们进行了事务的回滚.

这个例子里,如果事务执行成功,虽然没有破坏数据库的约束,但是破坏了我们应用层的约束.而事务的回滚保证了我们的约束,因此也可以说事务提供了一致性保证(ps:事实上,是我们应用层利用事务回滚保证了我们的约束不被破坏).最后我们再看个例子

A要向B支付100元,而A的账户中只有90元,我们的账户余额列没有任何约束.然后支付成功了.

这里,如果按照很多人的理解,事务不是保证一致性么?直观上账户余额为什么能为负呢.但这里事务执行前和执行后,我们的系统没有任何的约束被破坏.一直都是保持正确的状态.

所以,综上.你可以理解一致性就是:应用系统从一个正确的状态到另一个正确的状态.而ACID就是说事务能够通过AID来保证这个C的过程.C是目的,AID都是手段.

Consistency ensures that a transaction can only bring the database from one valid state to another, maintaining database invariants: any data written to the database must be valid according to all defined rules, including constraints, cascades,triggers, and any combination thereof. This prevents database corruption by an illegal transaction, but does not guarantee that a transaction is correct.

  • 数据库事务的一致性是指:保证事务只能把数据库从一个有效(正确)的状态“转移”到另一个有效(正确)的状态。那么,什么是数据库的有效(正确)的状态?满足给这个数据库pred-defined的一些规则的状态都是 valid 的。这些规则有哪些呢,比如说constraints, cascades,triggers 及它们的组合等。具体到某个表的某个字段,比如你在定义表的时候,给这个字段的类型是number类型,并且它的值不能小于0,那么你在某个 transaction 中给这个字段插入(更改)为一个 String 值或者是负值是不可以的,这不是一个“合法”的transaction,也就是说它不满足我们给数据库定义的一些规则(约束条件)。
  • “This prevents database corruption by an illegal transaction, but does not guarantee that a transaction is correct. ” 这又怎么理解呢?在数据库的角度来看,它只关心 transaction 符不符合定义好的规则,符合的就是legal的,不符合的就是illegal的。transaction 是否正确是从应用层的角度来看的,数据库并不知道你应用层的逻辑意义,它不保证应用层的transaction的正确性,这个逻辑正确性是由应用层的programmer来保证的。 这么说估计还是抽象,那么看下面我们熟知的转账的例子。
Table: Account
Columns:   Name(string), Balance(int)
约束条件:无

执行下面一个事务(A,B的初始余额均为1000,A给B转账1200)

1.  往表Account插入数据(A,1000)
2. 往表Account插入数据 (B,1000)
3. A给B转账1200,更新A的余额为-200,(A,-200)
4. B的余额增加1200,更新B的余额为2200(B,2200)

那么,数据库会认为这个 transaction 合不合法呢?也就是它满不满足我们给数据库的定义的规则呢?答案就是这个 transaction 是合法的,因为你定义表的时候没有约定 Balance 不能小于0。虽然我们从应用层的角度来看,这个transaction是不正确的,因为它不符合逻辑- balance不能小于0. 但我们数据库只关心你的 transaction 满不满足你的数据库定义的rule,不关心它具有什么业务的逻辑,这个业务逻辑是应该由应用层来理解并处理的。

修改一下上面这个例子

Table: Account
Columns:   Name(string), Balance(int)
约束条件:Balance >= 0

执行下面一个事务(A,B的初始余额均为1000,A给B转账1200)

1.  往表Account插入数据(A,1000)
2. 往表Account插入数据 (B,1000)
3. A给B转账1200,更新A的余额为-200,(A,-200)
4. B的余额增加1200,更新B的余额为2200(B,2200)

注意,这里增加了约束条件Balance > 0, 上面的这个transaction违反了规则Balance>=0,那么这个事务数据库认为它是非法的,不满足一致性的要求,所以数据库执行这个事务会失败。

最后请再认真研读一下链接 Consistency (database systems) 中的这段话。

This(Consistency)does not guarantee correctness of the transaction in all ways the application programmer might have wanted (that is the responsibility of application-level code) but merely that any programming errors cannot result in the violation of any defined database constraints.[1]

简单说:事务的一致性只是在数据库预先定义的约束有关,满足了约束即满足了一致性

什么是Transaction呢?我们可以把重点放在Transaction的特性上,理解了它的特性,相信大家也就知道了什么是Transaction。

事务(Transaction)是并发控制的基本单位。
所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。例如,银行转帐工作:从一个帐号扣款并使另一个帐号增款,这两个操作要么都执行,要么都不执行。

Transaction ACID

数据库事务必须具备ACID特性,ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔离性)和Durability(持久性)的英文缩写。

原子性:指整个数据库事务是不可分割的工作单位。只有使据库中所有的操作执行成功,才算整个事务成功;事务中任何一个SQL语句执行失败,那么已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。
  一致性:指数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。例如对银行转帐事务,不管事务成功还是失败,应该保证事务结束后ACCOUNTS表中Tom和Jack的存款总额为2000元。

隔离性:指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
  持久性:指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

事务的(ACID)特性是由关系数据库管理系统(RDBMS,数据库系统)来实现的。数据库管理系统采用日志来保证事务的原子性、一致性和持久性。日志记录了事务对数据库所做的更新,如果某个事务在执行过程中发生错误,就可以根据日志,撤销事务对数据库已做的更新,使数据库退回到执行事务前的初始状态。
  数据库管理系统采用锁机制来实现事务的隔离性。当多个事务同时更新数据库中相同的数据时,只允许持有锁的事务能更新该数据,其他事务必须等待,直到前一个事务释放了锁,其他事务才有机会更新该数据。

事务的原子性是指一个事务要么全部执行,要么不执行.也就是说一个事务不可能只执行了一半就停止了.比如你从取款机取钱,这个事务可以分成两个步骤:1划卡,2出钱.不可能划了卡,而钱却没出来.这两步必须同时完成.要么就不完成.

**事务的一致性是指事务的运行并不改变数据库中数据的一致性.例如,完整性约束了a+b=10,一个事务改变了a,那么b也应该随之改变. **

数据库隔离级别

在并发很大的这种情况下,我们期望多个事物可以并行执行,当多个事务竞争一个资源时,可以序列化的执行。但是这种序列化和并发性能是有一定矛盾的(理论上,你可以单线程一直跑,就不会有并发的问题;或者你可以不解决任何竞争,全部并行跑,但是问题出现时异常难移分析和定位),所以在数据库的发展中出现了数据库隔离级别的概念(你可以选择不通的隔离级别来得到你想要的并行能力)。

数据库事务的隔离级别有4个,由低到高依次为Read uncommitted 、Read committed 、Repeatable read 、Serializable ,这四个级别可以逐个解决脏读 、不可重复读 、幻读 这几类问题。

image.png
Read uncommitted 读未提交

公司发工资了,领导把5000元打到singo的账号上,但是该事务并未提交,而singo正好去查看账户,发现工资已经到账,是5000元整,非常高 兴。可是不幸的是,领导发现发给singo的工资金额不对,是2000元,于是迅速回滚了事务,修改金额后,将事务提交,最后singo实际的工资只有 2000元,singo空欢喜一场。

image.png

出现上述情况,即我们所说的脏读 ,两个并发的事务,“事务A:领导给singo发工资”、“事务B:singo查询工资账户”,事务B读取了事务A尚未提交的数据。

当隔离级别设置为Read uncommitted 时,就可能出现脏读,如何避免脏读,请看下一个隔离级别。

Read committed 读提交

singo拿着工资卡去消费,系统读取到卡里确实有2000元,而此时她的老婆也正好在网上转账,把singo工资卡的2000元转到另一账户,并在 singo之前提交了事务,当singo扣款时,系统检查到singo的工资卡已经没有钱,扣款失败,singo十分纳闷,明明卡里有钱,为 何......

出现上述情况,即我们所说的不可重复读 ,两个并发的事务,“事务A:singo消费”、“事务B:singo的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

Repeatable read 重复读

当隔离级别设置为Repeatable read 时,可以避免不可重复读。当singo拿着工资卡去消费时,一旦系统开始读取工资卡信息(即事务开始),singo的老婆就不可能对该记录进行修改,也就是singo的老婆不能在此时转账

虽然Repeatable read避免了不可重复读,但还有可能出现幻读 。

singo的老婆工作在银行部门,她时常通过银行内部系统查看singo的信用卡消费记录。有一天,她正在查询到singo当月信用卡的总消费金额 (select sum(amount) from transaction where month = 本月)为80元,而singo此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录(insert transaction ... ),并提交了事务,随后singo的老婆将singo当月信用卡消费的明细打印到A4纸上,却发现消费总额为1080元,singo的老婆很诧异,以为出 现了幻觉,幻读就这样产生了。

注:Mysql的默认隔离级别就是Repeatable read。

Serializable 序列化

Serializable 是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。

幻读的意思应该是:禁止修改同一数据项,但不阻止新的数据写入。因此,在事务执行过程中,前面的查询和后面的查询,在这期间有另外一个事务插入了数据的话,就会出现幻读。但幻读和和不可重复读之间并不矛盾。

由于很多人(当然也包括本人), 容易搞混 不可重复读 和 幻读, 这两者确实非常相似。

但 不可重复读 主要是说多次读取一条记录, 发现该记录中某些列值被修改过。
而 幻读 主要是说多次读取一个范围内的记录(包括直接查询所有记录结果或者做聚合统计), 发现结果不一致(标准档案一般指记录增多, 记录的减少应该也算是幻读)。(可以参考MySQL官方文档对 Phantom Rows 的介绍)

其实对于 幻读, MySQL的InnoDB引擎默认的RR级别已经通过MVCC自动帮我们解决了, 所以该级别下, 你也模拟不出幻读的场景; 退回到 RC 隔离级别的话, 你又容易把幻读和不可重复读搞混淆, 所以这可能就是比较头痛的点吧!
具体可以参考《高性能MySQL》对 RR 隔离级别的描述, 理论上RR级别是无法解决幻读的问题, 但是由于InnoDB引擎的RR级别还使用了MVCC, 所以也就避免了幻读的出现!

MVCC虽然解决了幻读问题, 但严格来说只是解决了部分幻读问题, 接下来进行演示:

1.打开客户端1查看隔离级别及初始数据

mysql> SELECT @@SESSION.tx_isolation;
+------------------------+
| @@SESSION.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set (0.00 sec)
 
mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption        |
+----+-----------+-----+--------+--------------------+
|  1 | 金刚狼 | 127 |      1 | 我有一双铁爪 |
|  2 | 钢铁侠 | 120 |      1 | 我有一身铁甲 |
|  3 | 绿巨人 |   0 |      2 | 我有一身肉    |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)
 
mysql> 

2.打开客户端2查看隔离级别及初始数据

mysql> SELECT @@SESSION.tx_isolation;
+------------------------+
| @@SESSION.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set (0.00 sec)
 
mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption        |
+----+-----------+-----+--------+--------------------+
|  1 | 金刚狼 | 127 |      1 | 我有一双铁爪 |
|  2 | 钢铁侠 | 120 |      1 | 我有一身铁甲 |
|  3 | 绿巨人 |   0 |      2 | 我有一身肉    |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)
 
mysql> 

3.在客户端2中开启事务, 然后查询数据

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
 
mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption        |
+----+-----------+-----+--------+--------------------+
|  1 | 金刚狼 | 127 |      1 | 我有一双铁爪 |
|  2 | 钢铁侠 | 120 |      1 | 我有一身铁甲 |
|  3 | 绿巨人 |   0 |      2 | 我有一身肉    |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)
 
mysql> 

4.在客户端1中插入一条id为4的新数据 (直接自动提交)

mysql> insert into test_transaction (`id`,`user_name`,`age`,`gender`,`desctiption`) values (4, '死侍', 18, 0, 'A bad boy');
Query OK, 1 row affected (0.00 sec)
mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption        |
+----+-----------+-----+--------+--------------------+
|  1 | 金刚狼 | 127 |      1 | 我有一双铁爪 |
|  2 | 钢铁侠 | 120 |      1 | 我有一身铁甲 |
|  3 | 绿巨人 |   0 |      2 | 我有一身肉    |
|  4 | 死侍    |  18 |      0 | A bad boy          |
+----+-----------+-----+--------+--------------------+
4 rows in set (0.00 sec)
 
mysql> 

5.在客户端2事务中再次查询数据, 发现数据没有变化(表示可以重复读, 并且克服了幻读)!! 但是在客户端2事务中插入一条id为4的新数据, 发现提示数据已经存在!!!

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
 
mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption        |
+----+-----------+-----+--------+--------------------+
|  1 | 金刚狼 | 127 |      1 | 我有一双铁爪 |
|  2 | 钢铁侠 | 120 |      1 | 我有一身铁甲 |
|  3 | 绿巨人 |   0 |      2 | 我有一身肉    |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)

mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption        |
+----+-----------+-----+--------+--------------------+
|  1 | 金刚狼 | 127 |      1 | 我有一双铁爪 |
|  2 | 钢铁侠 | 120 |      1 | 我有一身铁甲 |
|  3 | 绿巨人 |   0 |      2 | 我有一身肉    |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)

mysql> insert into test_transaction (`id`,`user_name`,`age`,`gender`,`desctiption`) values (4, '死侍', 18, 0, 'A bad boy');
1062 - Duplicate entry '4' for key 'PRIMARY'
mysql> 

//并且, 此时`update/delete`也是可以操作这条在事务中看不到的记录的!

6.那么这是什么问题呢?

The snapshot of the database state applies to SELECT statements within a transaction, not necessarily to DML statements. If you insert or modify some rows and then commit that transaction, a DELETE or UPDATE statement issued from another concurrent REPEATABLE READ transaction could affect those just-committed rows, even though the session could not query them. If a transaction does update or delete rows committed by a different transaction, those changes do become visible to the current transaction.
个人认为应该翻译为: 数据库状态的快照适用于事务中的SELECT语句, 而不一定适用于所有DML语句。 如果您插入或修改某些行, 然后提交该事务, 则从另一个并发REPEATABLE READ事务发出的DELETE或UPDATE语句就可能会影响那些刚刚提交的行, 即使该事务无法查询它们。 如果事务更新或删除由不同事务提交的行, 则这些更改对当前事务变得可见。

7.当然, 使用隔离性的最高隔离级别SERIALIZABLE也可以解决幻读, 但该隔离级别在实际中很少使用!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343