mysql事务详解

1 篇文章 0 订阅
1 篇文章 0 订阅

以下的分析和总结都是针对InnoDb引擎.

事务的四大特性

这四个特性并不是平级的关系. 准确的说, 一致性是最基本的属性, 其他的属性都是为了保证一致性而存在的

Atomicity 原子性

原子性是指整个事务是一个整体,不可分割的最小工作单位。一个事务中的所有操作要么全部执行成功,要么全部都不执行。其中任何一条语句执行失败,都会导致事务回滚。

Consistency 一致性

指数据库的记录总是从一个一致性状态转变成另一个一致性状态这里的一致性是语义上的一致性, 并不是语法上的一致性.

比如经典的银行转账例子,若A转账200给B,则需要执行如下三步:

  1. 检查A账户余额是否大于200
  2. 从A账户减去200
  3. 将B账户增加200

在事务的概念中,以上三个步骤必须处于同一个事务中,若不是处于同一事务中,当程序执行到3的时候系统异常了,就会导致用户的钱被扣.在真正的银行事务中,不仅需要强事务来保证一致性,而且会有一个日终对账系统,来计算每天所有的资金流向,保证整个资金流动值为0。

Isolation 隔离性

通常来说,一个事务所做的修改在事务未提交之前,对其他事务是不可见的。

还有一点需要注意,如果在同一个事务中如果执行相同的sql但是得到的结果不一样,此时也是违反隔离性的,比如把mysql的隔离级别设为read commited时就有可能会出现这种情况。

Durability 持久性

持久性 指的是数据一旦提交,结果就是永久性的。并不应为宕机等情况丢失。一般理解就是写入硬盘保存成功。

事务的实现方式

  • 原子性和持久性利用redo log(重做日志) 实现.
  • 一致性利用undo log(回滚日志)实现
  • 隔离性利用锁来实现

具体日志的实现方式见另一篇关于日志的文章中提到的.

事务传播级别

以下例子中用到的表结构:

create table user1 (
    id int unsigned not null auto increment,
    name varchar(32) not null,
    primary key(id)
) engine=InnoDB

create table user2 (
    id int unsigned not null auto increment,
    name varchar(32) not null,
    primary key(id)
) engine=InnoDB

在Spring中定义的事务的传播级主要有如下几种,现就其中几个主要的说明如下

PROPAGATION_REQUIRED

Spring默认的传播级别。按需加载。当上下文中已经存在事务,则加入到该事务执行。若上下文不存在事务,则新建一个事务执行。

此处上下文可以理解为@Transactional注解作用的地方,当该注解作用于方法时,上下文从方法最先开始的地方开启,也就是说在刚进入方法时,首先就会开启一个事务。

1.当外部方法未开启事务时, 若外部方法抛出异常, 如何运行

User1Service代码

@Service
public class User1Service extends ServiceImpl<User1Mapper, User1Entity> {
    @Resource
    private User1Mapper user1Mapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public void addUser1(User1Entity user1Entity) {
        user1Mapper.insert(user1Entity);
    }
}

User2Service代码

@Service
public class User2Service extends ServiceImpl<User2Mapper, User2Entity> {

    @Resource
    private User2Mapper user2Mapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public void addUser2(User2Entity user2Entity) {
        user2Mapper.insert(user2Entity);
    }
}

测试代码:

public void t1() {
        User1Entity user1Entity = new User1Entity();
        user1Entity.setId(100);
        user1Entity.setName("sju");
        user1Service.addUser1(user1Entity);

        User2Entity user2Entity = new User2Entity();
        user2Entity.setId(200);
        user2Entity.setName("fdf");
        user2Service.addUser2(user2Entity);
        throw new RuntimeException();
    }

结果: user1, user2均插入成功.

分析: 因为外围方法未开启事务,所以程序执行到两个addUser方法时都会开启一个新的事务,这两个事务是相互独立的,分别执行自己的逻辑,插入成功,外部方法异常不影响内部.

2.当外部方法未开启事务时, 若内部方法抛出异常, 如何运行

User1Service代码:

@Service
public class User1Service extends ServiceImpl<User1Mapper, User1Entity> {
    @Resource
    private User1Mapper user1Mapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public void addUser1(User1Entity user1Entity) {
        user1Mapper.insert(user1Entity);
    }
}

User2Service代码:

@Service
public class User2Service extends ServiceImpl<User2Mapper, User2Entity> {

    @Resource
    private User2Mapper user2Mapper;

    @Transactional(propagation = Propagation.REQUIRED)
    public void addUser2(User2Entity user2Entity) {
        user2Mapper.insert(user2Entity);
        throw new RuntimeException();
    }
}

测试代码:

 public void t1() {
        User1Entity user1Entity = new User1Entity();
        user1Entity.setId(100);
        user1Entity.setName("sju");
        user1Service.addUser1(user1Entity);

        User2Entity user2Entity = new User2Entity();
        user2Entity.setId(200);
        user2Entity.setName("fdf");
        user2Service.addUser2(user2Entity);
    }

结果: user1插入成功, user2插入失败

分析: 还是因为外部方法未开启事务, 所以user1, user2会开启两个独立的事务, 互不影响. 所以user1插入成功, user2插入失败.

3.当外部方法开启事务时, 若内部方法或者外部方法抛出异常, 如何运行

User1Service和User2Service代码同之前, 只是在测试方法上也加上@Transactional注解. 测试方法代码如下:

@Transactional
public void t1() {
        User1Entity user1Entity = new User1Entity();
        user1Entity.setId(100);
        user1Entity.setName("sju");
        user1Service.addUser1(user1Entity);

        User2Entity user2Entity = new User2Entity();
        user2Entity.setId(200);
        user2Entity.setName("fdf");
        user2Service.addUser2(user2Entity);
        throw new RuntimeException();
    }

结果: user1, user2都未插入

分析: 外部方法开启事务,内部两个方法都会加入到该事务中成为一个事务,外部方法回滚,则所有回滚

PROPAGATION_NESTED

事务之间是嵌套的。若上下文存在事务,则嵌套执行,若不存在事务,则新建事务执行。

原理:子事务是父事务的一部分,当进入子事务之前,会先在父事务建立一个回滚点save point,然后执行子事务。待子事务执行结束,再执行父事务。

总结起来就是: 子事务的执行不会影响父事务,但父事务的执行会影响子事务.

举个例子, 若methodA以PROPAGATION_REQUIRED修饰, methodB以PROPAGATION_NESTED修饰, 并且在methodA中调用methodB, 此时A为父事务, B为子事务。 当出现以下几种情况时, 其执行情况如下:

异常情况执行结果
A抛异常,B正常A, B 都回滚
A正常, B异常B先回滚, A再正常提交
A,B都抛异常A , B 都回滚

几个问题:

问题1: 若子事务回滚,会发生什么?

若子事务回滚,则父事务会回滚到之前建立的save point,然后执行其他业务逻辑,父事务之前的操作不受影响,更不会自动回滚。所以父事务不受子事务的影响。

问题2:若父事务回滚,子事务会回滚吗?

答:子事务会回滚

问题3:子事务和父事务谁先提交?

答:子事务先提交

PROPAGATION_SUPPORTS

支持当前事务,若没有当前事务,则以非事务执行

PROPAGATION_MANDATORY

使用当前事务,注意这是强制性的(mandatory),因为当发现当前没有事务时,直接抛出异常

**PROPAGATION_**REQUIRES_NEW

不管上下文中是否存在事务,每次都新建一个独立事务.

若上下文中存在事务,则先会将上下文事务挂起,然后执行本方法事务,待本方法事务执行完毕之后,继续执行上下文事务。

若上下文未开启事务,其行为逻辑与requires级别一致,即新建一个自己独立的事务.

当外部方法开启事务时,requires_new修饰的内部方法依然会单独开启独立事务,且与外部方法的事务相互独立,内部方法之间,内部方法和外部方法的事务均相互独立,互不影响。

PROPAGATION_NOT_SUPPORTED

以非事务方式执行代码,若当前存在事务,则将当前事务挂起

PROPAGATION_NEVER

以非事务执行,该处语气比上一个更强,因为当发现当前存在事务时,则直接抛出异常

事务隔离级别

定义事务在数据库当中读写的控制范围。总共有四大级别

隔离级别脏读不可重复读幻读
未提交读可能可能可能
提交读不可能可能可能
可重复读不可能不可能可能
串行读不可能不可能不可能
未提交读 Read Uncommited

一个事务所做的修改在该事务未提交前对其他事务可见。

此种隔离级别一般不采用,因为可能会产生脏读.

如下表所示,事务A在时刻5读取到了事务B在时刻4还未提交的操作,导致出现了脏读。按照实际情况,应该最后余额1500,现在只有500

时间事务A事务B
1开始事务-
2-开始事务
3-查询余额有1000
4-取出1000,余额0
5查询余额0-
6-撤销掉事务
7存入500,余额500-
8提交事务-
提交读 Read Commited

只能读取到其他事务已经提交的数据。

一个事务所做的修改在该事务未提交之前对其他事务不可见,即在未提交之前,实际上并没有对数据库进行更改,只有在真正提交的时候才会去更改数据。此级别能够解决脏读的问题,但是有可能会产生不可重复读的问题。Orcale默认数据隔离级别是该级别。

下图展示的是在该级别下产生的不可重复读的问题,表A原先数据只有1条, 事务A在同一个事务中前后两次读取到的余额是不一致的,这是因为事务B在这过程当中修改了该条记录并提交

时间事务A事务B
1begin-
2begin
3select * from t; 返回结果: 1行
4insert into t select 2;
5commit;
6select * from t; 返回结果: 2行
可重复读 Repetable Read

保证在同一个事务中,两次读取到的数据是一致的。该级别能解决不可重复读的问题,但是又可能会出现幻读(注意此处指的是一般意义上的数据库,MYSQL InnoDB在该级别下是不存在幻读的,具体原因见后面)

幻读针对的是数据的新增,而不可重复读针对的是数据的修改.

下图是出现幻读的情景,前后两次读取的数据不一致。

时间点事务A事务B
1set autocommit = falseset autocommit=false
2select count(*) from user
结果为 1
-
3-insert into user(name) values(‘jki’)
4select count(*) from user
结果为 2
-
5-commit
6commit-
7--
8select count(*) from user
结果为2
-

user表在刚开始操作之前只有一条数据.可以看到在同一个事务A中, 前后两个时间点2和4得到的结果是不一样的, 这就是幻读, 原因是事务B在这期间新插入了一条数据.

Mysql InnoDb默认数据库隔离级别是该级别,即可重复读

一般意义上的不可重复读是无法解决幻读的,但是在测试MySQL的时候发现,是不存在幻读的,这是怎么回事?

如下图是Mysql的不可重复读对应的操作,我们发现是不存在幻读的

时间点事务A事务B
1set autocommit = falseset autocommit=false
2select count(*) from user
结果为 1
-
3-insert into user(name) values(‘jki’)
4select count(*) from user
结果仍然为1
-
5-commit
6select count(*) from user
结果仍然为1
-
7commit-
8select count(*) from user
结果为2
-

我们可以看到在同一个事务A中, 即使事务B同时插入了一条记录, 事务A在事务B未提交的时间点4和事务B已经提交的时间点6统计的结果仍然和之前的保持一致.

那么InnoDb的可重复读隔离级别是如何解决幻读的?

答: InnoDB的行锁总共有三种算法,分别如下:

  • Record lock 单个行记录上的锁

  • Gap lock 间隙锁, 锁定一个范围,但不包括记录本身

  • Next-Key lock: 上述两个锁的组合,锁定一个范围,同时包括锁定记录本身.

Record Lock总是会去锁住索引记录, 如果InnoDb在建表的时候没有设置任何一个索引,则存储引擎会使用隐式的主键来进行锁定.

InnoDB对于行的查询默认使用Next-key lock算法(此处的查询指的是显示指定加锁的查询,如for update,否则的话普通查询是通过一致性非锁定读获取数据),例如一个索引有如下三条记录: 1, 3, 7, 则Next-key lock表示的区间为:

(-∞, 1] (1, 3] (3, 7] (3, +∞)

注意,当索引是唯一索引且是等值查询时(例如常见的主键等值查找id =1), Next-Key lock会降级为record lock,只会锁住某行索引记录,而不是一个区间.

通过使用next-key lock, InnoDb在可重复读的级别下,就可以防止出现幻读.因为在其他事务对于该区间范围内插入或删除数据时,该区域已经有一个next-key lock, 必须等待该锁释放掉,才能执行插入.

串行读 Serialize

保证事务串行执行。其原理是通过对读取的每一行数据都加锁来保证的。它不会出现上述错误,但是会导致大量的锁竞争,影响性能。

几个概念

1.脏读

一个事务读取到了另外一个事务未提交的数据。此处会违反事务的隔离性原则。

2.不可重复读

在同一个事务中两次读取到的数据不一致。主要原因是当事务隔离级别是Read Commited(提交读)的时候,一个事务所做的修改在事务未提交之前对其他事务是不可见的。该隔离级别能满足事务隔离性的一般定义。但是会导致不可重复读产生。

3.幻读

当某个事务在读取某个范围内的数据时,另外一个事务也对该范围内的数据进行新增或删除,此时两次读取到的结果不一致,导致出现幻读。

幻读和不可重复读的区别是不可重复读是对于数据的修改操作, 如果用锁来实现,在可重复读中,当sql第一次读取到数据时,就将数据加锁,这样其他事务就无法修改这些数据,就可以实现可重复读。但是该方式却无法锁住insert的数据,这就是幻读。

分布式事务

未完待定

@Transactional注解

该注解是Spring中用来控制事务的注解.在使用时需要注意以下几点:

  1. @Transactional注解默认回滚RuntimeExcepption异常,即当方法被Transactional注解时,若在期间发生RuntimeException异常,则Spring会自动回滚该事务,注意此处即使异常被处理(即catch捕获了该类型的RuntimeException异常),Spring仍然会回滚事务

  2. @Transactional注解有一个参数rollbackFor可用来指定当自定义的非RuntimeException发生时,也会进行事务回滚(Spring默认只回滚RuntimeException异常,对于其他非Runtime异常是不会自动回滚的)

  3. @Transactional注解只能作用于public方法上,虽然也能写在非public上而不报错,但实际上是不起作用的

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MySQL事务隔离级别决定了在并发环境下多个事务之间的隔离程度。MySQL提供了四个事务隔离级别,分别是读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。以下是对这四个隔离级别的详细解释: 1. 读未提交(Read Uncommitted):这是最低级别的隔离级别。在该级别下,一个事务可以看到其他事务未提交的修改。这可能导致脏读(Dirty Read)和不可重复读(Non Repeatable Read)的问题。 2. 读已提交(Read Committed):在该级别下,一个事务只能看到其他事务已经提交的修改。这可以避免脏读的问题,但仍可能导致不可重复读的问题。 3. 可重复读(Repeatable Read):在该级别下,一个事务在执行期间能够看到同一结果集的一致性快照。这可以避免脏读和不可重复读的问题,但仍可能导致幻读(Phantom Read)的问题。 4. 串行化(Serializable):在该级别下,事务之间是完全隔离的,每个事务必须按照顺序执行。这可以避免脏读、不可重复读和幻读的问题,但也导致并发性能的严重下降。 要查看MySQL的默认隔离级别和当前话的隔离级别,可以使用以下命令: ```sql SELECT @@GLOBAL.tx_isolation, @@tx_isolation; ``` 请注意,MySQL 8之前可以使用上述命令,而MySQL 8及更高版本可以使用以下命令: ```sql SELECT @@global.transaction_isolation, @@transaction_isolation; ``` 这样可以查看默认的全局隔离级别和当前话的隔离级别。这些隔离级别可以通过设置`transaction_isolation`参数来进行更改。<span class="em">1</span><span class="em">2</span><span class="em">3</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值