初学的时候,感受事务的四大特性就那么回事,不就是一堆事要么完成,要么所有失败吗。还有常常说的脏读,幻读,不可重复读根本没法理解,就是那个存款取款的例子,我修改了数据,对方看到我修改的数据,这不很正常吗。如今看来,当时根本就不知道并发是什么鬼,更何谈并发事物了。java
而后给你来一堆名词,共享锁,排它锁,悲观锁,乐观锁...... 想一想就以为那时候能记下来已是奇迹了。mysql
Spring 还给事务弄了一个传播机制的家伙,Spring 事务传播机制能够看这篇文章 。 本文应该来讲是对初学者的福音,有必定经验的人看的话应该也会有收获。git
这个是刚入门面试的时候必问一个面试题,刚入行的时候我是硬生生背下来的。程序员
隔离性(Isolation) 两个事务之间是隔离程度,具体的隔离程度由隔离级别决定,隔离级别有面试
事务指的是从开始事务->执行操做->提交/回滚 整个过程,在程序中使用一个链接对应一个事务spring
-- sql 中的事务 START TRANSACTION; select * from question; commit ;
// 最原始的 jdbc 事务 Connection connection = 获取数据库链接; try{ connection.setAutoCommit(false); // todo something connection.commit(); }catch(Exception e){log(e); connection.rollback(); }finally{ try{connection.close()}catch(Exception e){log(e);}; }
并发事务是指两个事务一同开始执行,若是两个事务操做的数据之间有交集,则颇有可能产生冲突。这时怎么办呢,其实这也是 临界资源 的一种,在应用程序中,咱们解决这类问题的关键是加锁,在数据库的实现也是同样,但在数据库中须要考虑更多。常见的须要考虑的问题有(下面说的我和人都是指一个会话)sql
看过网上的大部分文章,基本都是一个表格来演示两个事务的并发,有的根本就是直接抄的,不知道那做者真的懂了没,其实咱们是能够用客户端来模拟两个事务并发的状况的,打开两个 session ,让两个事务互相穿插。数据库
下面的演示都是基于 mysql5.7
版本,查询事务隔离级别和修改隔离级别语句session
-- 查看事务隔离级别 select @@tx_isolation; -- 修改当前 session 事务隔离级别 set session transaction isolation level read uncommitted; set session transaction isolation level read committed ; set session transaction isolation level repeatable read ; set session transaction isolation level serializable; -- 开启事务提交和回滚 START TRANSACTION; select * from question; commit ;rollback;
准备数据表,暂时先使用 InnoDB 引擎并发
CREATE TABLE `account` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(64) DEFAULT NULL, `balance` decimal(10,2) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `test`.`account` (`id`, `name`, `balance`) VALUES ('1', 'sanri', '100.00'); INSERT INTO `test`.`account` (`id`, `name`, `balance`) VALUES ('2', '9420', '100.00');
打开两个 session ,设置隔离级别为 read uncommitted
时间(相对时间) | 事务A | 事务B |
---|---|---|
1 | start TRANSACTION | |
2 | start TRANSACTION | |
3 | update account set balance = balance - 20 where id = 1; | |
4 | select * from account where id = 1 -- 80 | |
5 | rollback | |
6 | commit |
这个会有什么问题呢,网上说可能事务 B 可能会去存款,但我试过了,事务B 在这时候存款会被阻塞,由于事务A 在更新的时候已经加了排它锁,只有等事务A 提交或回滚事务B 才能执行。
它真正的问题出在,若是程序来读到了这个 80 块钱返回到了第三方的系统,而事务A 回滚了,这时候问题就大了,它主要体如今读不一致。或者用户看到我本身取款失败了钱没取到但为何我账户余额少了的不一致问题。
解决脏读是设置隔离级别为读提交的数据 read committed
打开两个 session 设置隔离级别为 read committed
时间(相对时间) | 事务A | 事务B |
---|---|---|
1 | start TRANSACTION | |
2 | start TRANSACTION | |
3 | select * from account where id = 1 -- 100 | |
4 | update account set balance = balance - 20 where id = 1; | |
5 | commit; | |
6 | select * from account where id = 1 -- 80 | |
7 | commit; |
两次一样条件的查询,结果确不一致。刚开始的时候必定会以为,这没问题啊,事务B 作了更新操做,我这少 20 块钱变 80 有问题吗?
其实仍是有问题的,主要出如今复杂的业务逻辑查了两次相同的数据集(在程序员看来是相同数据集),又好比 mapper 中有两个方法名不同,但作了一样功能的 sql 语句 (这个在代码屡次接手后会出现),再或者在一个 sql 块中有两个更新语句使用了同一个查询,恰好数据被改了
begin update xxx inner join (select balance from account where id = 1) set xxx = xxoo; update xoxo inner join (select balance from account where id = 1) set xxbb = mmcc; end
解决办法是设置隔离级别为可重复读 repeatable read
或者显示的加上共享锁 (select * from account where id = 1 lock in share mode;
),但这会阻塞事务B,由于共享锁是一种悲观锁
mysql 的多事务并发版本控制
使用可重复读以后会发现,发现查询和更新并无互相阻塞,推测 mysql 应该不是简单的使用共享锁来实现可重复读, 使用共享锁会使性能特别低下,由于一个查询也要加锁。
Mysql 的可重复读使用的是 MVCC 机制,当一个事务开始后,select 查询屡次都会和第一次查询的结果一致,这种查询称为快照读,与之相对的是当前读,对于加锁语句,或更新语句都是使用当前读 ,好比
-- 这里的更新会使用最新的 balance 来更新,同时会加上排它锁,不用担忧最终结果是错的 update account set balance = balance - 20 where id = 1
幻读相比较于不可重复读来讲有点相似,都是同一个查询条件查到了不一致的结果,但幻读更注重于添加或删除数据,而不可重复读注重于修改数据,产生的影响也是和不可重复读相似的。
More Actions时间(相对时间) | 事务A | 事务B |
---|---|---|
1 | start TRANSACTION | |
2 | start TRANSACTION | |
3 | select * from account | |
4 | delete from account where id = 1 | |
5 | commit; | |
6 | select * from account -- 少了一行 |
幻读的解决办法一种就是修改隔离级别为 serializable
,或者锁定整张表,但不论是串行化执行事务或锁定整张表,都是同一时刻只有一个事务在执行的意思,也即没有并发事务了,性能会特别低下。
mysql 有一个 gap 锁的机制,它在 repeatable read
隔离级别下防止了幻读,也没有锁整张表,它取了一个平衡值,锁定索引间的间隙。具体查看这篇文章或查看官网说明
http://www.javashuo.com/article/p-mxfgnwhq-z.html
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
read uncommitted | 容许 | 容许 | 容许 |
read committed | 不容许 | 容许 | 容许 |
repeatable read | 不容许 | 不容许 | 容许 |
serializable | 不容许 | 不容许 | 不容许 |
这个问题是我在工做中遇到的,先来看一段代码
@Transactional public synchronized void insertXX(xx){ long maxNo = xxMapper.selectMaxNo(); return maxNo + 1; XXEntity xx = new XXEntity(maxNo,'x','xx'); xxMapper.insert(xx); }
初一看这个方法,没啥问题,获取最大编号并添加进数据库,为防止并发致使编号重复加了同步锁。
但在实际生产环境中这个方法出问题了,出现了相同的编号致使程序出错。
其实这里的缘由是由于锁并无完整的包含事务,事务是 spring 用 aop 实现的,在代理方法中去调用了目标方法,可是锁是加在了目标方法上,事务在锁释放后才提交,又由于隔离级别使用的是可重复读,读不到未提交的数据,因此若是在事务提交的过程当中,有线程执行此方法,是没有上锁的,进来查到的编号仍是原来的编号,解决办法有两种 ,一种是把锁上移,使用 aop 来实现锁,一种是再加一个方法不加事务,并包裹本方法。
方法一:
@Autowized private XXService xxService; @Transactional(propagation = Propagation.NOT_SUPPORTED) public synchronized void proxyXX(){ xxService.insertXX(); } @Transactional public void insertXX(xx){ long maxNo = xxMapper.selectMaxNo(); return maxNo + 1; XXEntity xx = new XXEntity(maxNo,'x','xx'); xxMapper.insert(xx); }
这里必须另启一个类,由于 spring aop 是对类生效的
方法二:
定义一个切面,好比用注解来实现切点,而后加锁
@Lock @Transactional public void insertXX(xx){ long maxNo = xxMapper.selectMaxNo(); return maxNo + 1; XXEntity xx = new XXEntity(maxNo,'x','xx'); xxMapper.insert(xx); }
都知道 MyISAM 只支持表锁,MyISAM 能支持行锁和表锁,但 Innodb 使用行锁也是有条件的,就是查询列必须是索引的,不然将使用表锁
还有一个特色就是 Innodb 是支持事务的,但 Myisam 不支持事务
对于 MyISAM来讲更加适合那种不常常作更新操做只提供查询和 统计操做的数据,好比
统计表,配置表,冷数据表...
对于 Innodb 来讲适合的主要对象就是常常作更新操做的表,好比
业务表,热数据表
创做不易,但愿能够支持下个人开源软件,及个人小工具,欢迎来 gitee 点星,fork ,提 bug 。
Excel 通用导入导出,支持 Excel 公式
博客地址:https://blog.csdn.net/sanri1993/article/details/100601578
gitee:https://gitee.com/sanri/sanri-excel-poi
使用模板代码 ,从数据库生成代码 ,及一些项目中常常能够用到的小工具
博客地址:https://blog.csdn.net/sanri1993/article/details/98664034
gitee:https://gitee.com/sanri/sanri-tools-maven