数据库的事务和spring中的事务类似,因为spring中的事务依赖数据库中事务机制,本文尽量不介绍重复部分。
大家可以先去了解一下我之前写的spring的事务:https://blog.csdn.net/w8827130/article/details/84499213
我所使用的是mysql数据库,所以本文围绕mysql来说。
数据库事务介绍:
数据库中ACID对应着事务的四个特性:原子性、一致性、隔离性和持久性。
事务中存在着一些问题:脏读、不可重复读和幻读,所以存在着不同的事务的隔离等级来处理这些问题。
事务的隔离等级有read uncommitted(读未提交)、read committed(读已提交)、repeatable read(可重复读)、serializable(串行化)。
隔离级别 | 脏读 | 幻读 | 不可重复读 |
读未提交 | √ | √ | √ |
读已提交 | × | √ | √ |
可重复读 | × | × | √ |
串行化 | × | × | × |
MySql5.5.5版本以上默认Engine是InnoDB,其他版本默认是MyISAM。
show variables like '%version%';查看本机mysql版本,我本机的mysql版本是5.6.26的。
只有InnoDB提供事务的支持,在mysql控制中输入show engines;可以查看当前mysql版本支持哪些存储引擎。不同的存储引擎可以按具体需求在不同的业务场景中使用,并不是规定死了只能用InnoDB。
show variables like '%isolation%';查看到当前版本mysql的事务隔离等级,默认为可重复读(repeatable read)
事务的隔离等级:
read uncommitted(读未提交)
read uncommitted有三个问题,脏读、不可重复读和幻读。
让我们来看看事务的脏读是如何进行的。
首先创建新表和插入数据
/* 创建账号表 */
create table account
(
id int primary key auto_increment,
name varchar(40),
money float
) character set utf8 collate utf8_general_ci;
/* 插入测试数据 */
insert into account(name,money) values('aaa',1000);
insert into account(name,money) values('bbb',1000);
insert into account(name,money) values('ccc',1000);
查看刚刚生成的account表和其中的数据,select * from test.account;(test.account是数据库名.表名)
set session transaction isolation level read uncommitted;(设置当前窗口会话的本机mysql事务隔离机制为read uncommitted)
打开窗口A和窗口B两个mysql控制台并且设置两个窗口的isolation level为read uncommitted(左边窗口为A窗口,右边窗口为B窗口)
开启窗口A和B的事务start transaction;
事务A开始更新数据,更新account表中name为aaa用户的money字段,减100。这个时候事务A可以读取到自己事务还未提交的数据,name为aaa的账号的money变成了900,事务B也可以查询到事务A中未提交的数据。
这时候事务A还未提交,navicatForMysql中account表里name为aaa账号的money字段依然是事务A更新之前的1000,并没有变成900。
这时候将事务A进行回滚(rollback;),数据回滚了,又变回了1000,而在这之前事务B读到的事务A未提交成功的数据就变成了脏数据,这就是脏读。
read committed(读已提交)
先查看窗口A和窗口B的account表的初始数据
把窗口A和B的isolation level改为read committed(set session transaction isolation level read committed;)
然后再来试试能不能进行脏读
在isolation level为read committed级别下,事务B未提交的数据,事务A读取不到,所以解决了脏读的问题。
但是read committed级别下还有不可重复读和幻读的问题,现在来重现一下不可重复读的问题。
在事务B提交事务之后,name为aaa的money变成了900,而这时候事务A还未完成,它读取到了事务B已提交的数据,aaa的money从1000变成了900,前后读取的数据不一致,这就是不可重复读。
repeatedable read(可重复读)
可重复读的隔离等级下,解决了脏读和不可重复读的问题,但是还会出现幻读的问题。
set session transaction isolation level repeatable read;(设置数据库会话中的事务隔离等级为可重复读)
在事务A未提交前,name为aaa的money一直是900,事务B已经将money改为800并且提交了事务,这时候数据库中money已经是800了,这时候事务A中的aaa的money仍然是900,没有出现不可重复读的问题,在可重复读的隔离等级下可以解决不可重复读的问题。
让我们来看看幻读是怎样的?
事务B插入一条数据后提交事务,而在repeatable read隔离级别下,事务A依然是可重复读的,所以事务A前后读到的数据行并没有变化,但是数据库中已经多了一条数据了。
这时候在事务A中同样也插入id为4的数据行,就会报错,ERROR 1062 (23000): Duplicate entry '4' for key 'PRIMARY',我明明在事务A中没有查到id为4的数据行,居然报错说主键冲突,然后再更新所有的数据行的money为1000,更新完了再进行查询发现多了一条数据,这就是幻读。
serializable(串行化)
串行化的隔离等级下,解决了脏读、不可重复读和幻读的问题。
set session transaction isolation level serializable;(设置数据库会话中的事务隔离等级为串行化)
同时开启事务A和事务B,然后事务A和事务B准备向数据库中插入一条数据,这时候会发现控制台并没有迅速做出反应,而是一直在尝试获取锁,最后报错提示获取锁的时候超时,尝试重启事务,好像进入了死锁。
在事务B发生错误后,事务A也插入同样的数据,也是一样的超时报错,然后我回滚了事务B,这时候事务A再进行插入操作就成功了,这个就直接限制了同一时间只能有一个事务。
但是在事务B插入操作还未超时之前事务A也插入相同的数据的时候,mysql就会检测到死锁,将事务A报错,而事务B插入成功了,删除和修改也是相同的结果,也就是说在serializable隔离级别下,增删改行为不能出现两个事务同时操作同一张表的情况,而读不受限制,这就避免了脏读、不可重复读和幻读,serializable是最安全的数据库事务隔离级别。
总结
可重复读:行锁(事务过程中读取某行的数据不变,因为该行被锁了)
串行化:锁表(一个事务开始执行,就将表锁住,事务执行完再释放锁)
数据库事务并发时会出现一些问题,脏读、不可重复读和幻读,而数据库也有相应的数据库事务隔离级别来相应的解决这些问题,但是世间安得双全法,不负如来不负卿,想要更高的数据安全级别,就需要拿数据库性能来换,更高的事务隔离级别意味着更低的事务并发数,而且很容易发生死锁情况,所以我们一般默认的事务隔离级别是repeatable read。当然也可以根据业务需求去选择更适合自己系统的事务隔离级别,一直都是没有什么最好,只有更合适。
至于数据库的事务底层是如何实现的,放在下一篇博客学习,避免过长,看的各位大佬眼睛疼。