Java及数据库事务

数据库并发问题

1、脏读(读取未提交数据)

A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。就好像原本的数据比较干净、纯粹,此时由于B事务更改了它,这个数据变得不再纯粹。这个时候A事务立即读取了这个脏数据,但事务B良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务A却什么都不知道,最终结果就是事务A读取了此次的脏数据,称为脏读。
Java及数据库事务_第1张图片

2、不可重复读(前后多次读取同一数据,数据内容不一致)

事务A在执行读取操作,由整个事务A比较大,前后读取同一条数据需要经历很长的时间 。而在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读。

Java及数据库事务_第2张图片
3、幻读(前后多次读取,数据总量不一致)

事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。

Java及数据库事务_第3张图片

事务隔离级别

任何支持事务的数据库,都必须具备四个特性,分别是:
原子性(Atomicity)、
一致性(Consistency)、
隔离性(Isolation)、
持久性(Durability),也就是我们常说的事务ACID,这样才能保证事务((Transaction)中数据的正确性。

而事务的隔离性就是指,多个并发的事务同时访问一个数据库时,一个事务不应该被另一个事务所干扰,每个并发的事务间要相互进行隔离。

Java及数据库事务_第4张图片
读未提交(Read Uncommitted) (所有并发问题都会发生)

读未提交,顾名思义,就是可以读到未提交的内容。

因此,在这种隔离级别下,读不会加任何锁。而写会加排他锁,所以这种隔离级别的一致性是最差的,可能会产生“脏读”、“不可重复读”、“幻读”。

如无特殊情况,基本是不会使用这种隔离级别的。

https://segmentfault.com/a/1190000012654564

读提交(Read Committed) (避免了脏读问题)

读提交,顾名思义,就是只能读到已经提交了的内容。

这是各种系统中最常用的一种隔离级别,也是SQL Server和Oracle的默认隔离级别。这种隔离级别能够有效的避免脏读.

写数据是使用排他锁, 读取数据不加锁而是使用了MVCC机制, 这样就可以大大提高并发读写效率, 写不影响读, 因为读并未加锁, 读的是记录的镜像版本

事务启动后(事务真正启动时会生成整个库的快照start trasaction+begin+第一条sql), 读使用的MVCC“快照读”的方式, 在一个事务中多次查询都是查到事务启动前的数据快照, 不会读到数据库未提交的更新数据. 因为一旦该数据修改被提交了, 事务查询到的数据就是这次提交成功后的快照(一个事务中2次查询数据不一样->不可重复读)

https://segmentfault.com/a/1190000012655091

可重复读(Repeated Read) (避免脏读,不可重复读, 幻读(mysql高版本))

可重复读,顾名思义,就是专门针对“不可重复读”这种情况而制定的隔离级别,自然,它就可以有效的避免“不可重复读”。而它也是MySql的默认隔离级别

在这个级别下,普通的查询同样是使用的“快照读”,但是,和“读提交”不同的是,当事务启动时,就不允许进行“修改操作(Update)”了,而“不可重复读”恰恰是因为两次读取之间进行了数据的修改,因此,“可重复读”能够有效的避免“不可重复读”,但却避免不了“幻读”,因为幻读是由于“插入或者删除操作(Insert or Delete)”而产生的(MySql中的不可重复读级别可以避免幻读)

串行化(Serializable) (避免所有并发问题)

这是数据库最高的隔离级别,这种级别下,事务“串行化顺序执行”,也就是一个一个排队执行。

这种级别下,“脏读”、“不可重复读”、“幻读”都可以被避免,但是执行效率奇差,性能开销也最大,所以基本没人会用。

总结一下

为什么会出现“脏读”?因为没有“select”操作没有规矩。

为什么会出现“不可重复读”?因为“update”操作没有规矩。

为什么会出现“幻读”?因为“insert”和“delete”操作没有规矩。

MVCC:Snapshot Read(快照读) vs Current Read(当前读)
MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。
MVCC最大的好处:读不加锁,读写不冲突。

在MVCC并发控制中,读操作可以分成两类:

快照读:读取的是记录的可见版本 (有可能是历史版本),不用加锁。通过MVVC(多版本控制)和undo log来实现的

当你执行select *之后,在A与B事务中都会一样的数据,这是不用想的,当执行select的时候,innodb默认会执行快照读,相当于就是给你目前的状态找了一张照片,以后执行select 的时候就会返回当前照片里面的数据,当其他事务提交了也对你不造成影响,和你没关系,这就实现了可重复读了.
那这个照片是什么时候生成的呢?不是开启事务的时候,是当你第一次执行select的时候,也就是说,当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据,之后无论再有其他事务commit都没有关系,因为照片已经生成了,而且不会再生成了,以后都会参考这张照片。
如果当前事务commit后, 再进行新的事务或者直接查询,就可以看到其他已提交的事务作出的修改(生产新快照)

使用场景: 简单的select操作,属于快照读,不加锁。
select * from table where ?;

当前读:读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。通过加record lock(记录锁)和gap lock(间隙锁)来实现的

2、update、insert、delete 当前读
  当你执行这几个操作的时候默认会执行当前读,也就是会读取最新的记录,也就是别的事务提交的数据你也可以看到,这样很好理解啊,假设你要update一个记录,update会立即去查最新的数据作为修改的基准数据, 并把这个基准数据锁住, 不然其他事务在update结束前修改这个数据. 默认加的是排他锁,也就是你读都不可以,这样就可以保证数据不会出错了。但注意一点,就算你这里加了写锁,别的事务也还是能访问的,是不是很奇怪?数据库采取了一致性非锁定读,别的事务会去读取一个快照数据。
  如果在事务中select, 结果看不到其他事务已经提交的修改(commit以后重新select就可以), 但update时,会以其他事务已经提交的数据做为基准进行update. update成功后,会自动commit, 再select就会看到最新的数据.(如果update不成功, 比如没有找到需要update的数据,就不会commit, select到的数据还是最开始的快照, 看不到其他事务提交的内容)

当前读:特殊的读操作和插入/更新/删除操作,属于当前读,需要加锁。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;

所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。

https://www.cnblogs.com/crazylqy/p/7611069.html

SpringBoot中使用事务

Maven中配置(使用JDBC事务管理器)

		<dependency>
   			<groupId>org.springframework.boot</groupId>
   			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>

常用其他事务管理器
Java及数据库事务_第5张图片

Main方法注解事务管理器@EnableTransactionManagement

@SpringBootApplication
@EnableTransactionManagement
@EnableEurekaClient
@MapperScan("com.example.demo.dao")
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}

事务三大接口

PlatformTransactionManager 事务管理器: 定义使用哪一个事务管理器
TransactionDefinition 事务的一些基础信息,如超时时间、隔离级别、传播属性等: 定义默认的基础信息
TransactionStatus 事务的一些状态信息,如是否一个新的事务、是否已被标记为回滚

Spring事务隔离级别

操作名称 说明 级别
@Transactional(isolation = Isolation.DEFAULT) 默认隔离级别,和数据库的中的4种对应,数据库中用啥,spring事务隔离就用啥 -1
@Transactional(isolation = Isolation.READ_UNCOMMITTED) 读取未提交数据(会出现脏读, 不可重复读,幻读),基本不使用 1
@Transactional(isolation = Isolation.READ_COMMITTED)(SQLSERVER默认) 读取已提交数据(会出现不可重复读和幻读) 2
@Transactional(isolation = Isolation.REPEATABLE_READ) 可重复读(会出现幻读) 4
@Transactional(isolation = Isolation.SERIALIZABLE) 串行化 8

Spring事务传播属性

传播属性 说明
@Transactional(propagation = Propagation.REQUIRED) 支持当前事务,如果当前有事务, 那么加入事务, 如果当前没有事务则新建一个(默认情况)
@Transactional(propagation = Propagation.NOT_SUPPORTED) 以非事务方式执行操作,如果当前存在事务就把当前事务挂起,执行完后恢复事务(忽略当前事务)
@Transactional(propagation = Propagation.SUPPORTS ) 如果当前有事务则加入,如果没有则不用事务
@Transactional(propagation = Propagation.MANDATORY ) 支持当前事务,如果当前没有事务,则抛出异常。(当前必须有事务)
@Transactional(propagation = Propagation.NEVER) 以非事务方式执行,如果当前存在事务,则抛出异常。(当前必须不能有事务)
@Transactional(propagation = Propagation.REQUIRES_NEW) 支持当前事务,如果当前有事务,则挂起当前事务,然后新创建一个事务,如果当前没有事务,则自己创建一个事务。
@Transactional(propagation = Propagation.NESTED) 如果当前存在事务,则嵌套在当前事务中。如果当前没有事务,则新建一个事务自己执行(和required一样)。嵌套的事务使用保存点作为回滚点,当内部事务回滚时不会影响外部事物的提交;但是外部回滚会把内部事务一起回滚回去。(这个和新建一个事务的区别)

Propagation.REQUIRED

//service1中

public class UserInfoExtendServiceImpl{


    @Transactional(propagation = Propagation.REQUIRED)
    public void serviceA() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(100);
        infoVo.setUserName("ceshi1");
        userInfoDao.save(infoVo);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void serviceB() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(200);
        infoVo.setUserName("ceshi2");
        userInfoDao.save2(infoVo);
    }
}

//sercie2中:
public class UserInfoServiceImpl{
    private UserInfoExtendService userInfoExtendService;
    public void setUserInfoExtendService(UserInfoExtendService userInfoExtendService) {
        this.userInfoExtendService = userInfoExtendService;
    }


@Transactional
    public void service() {
        userInfoExtendService.serviceA();
        userInfoExtendService.serviceB();
    }    
}    

说明:默认情况下,propagation=PROPAGATION_REQUIRED,整个service调用过程中,只存在一个共享的事务,当有任何异常发生的时候,所有操作回滚。
sercieA,serviceB,service,他们三个将为同一个事务。

如果当前有事务则加入事务中,如果当前没有事务则自己创建一个事务,上面的例子中service中有事务了,serviceA,serviceB中自己就不会创建事务了,而是service,serviceA,serviceB为一个事务。
如果service中没有事务,则sercieA,serviceB会各自创建一个事务,互不影响哦!
“当前事务”:对于serviceA来说,当前事务就是service里面的事务,相当于调用sercieA的那个方法.

错误方法:

@Transactional(propagation = Propagation.REQUIRED)
    public void serviceA() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(100);
        infoVo.setUserName("ceshi1");
        userInfoDao.save(infoVo);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void serviceB() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(200);
        infoVo.setUserName("ceshi2");
        userInfoDao.save2(infoVo);
    }

    @Transactional
    public void service() {
        serviceA();
        serviceB();
    }

Spring aop内部方法调用会丢失代理的哦。service,serviceA,serviceB在一个类里面,service调用serviceA,serviceB,会产生serviceA,serviceB上面的事务无效,只有service有效。

Propagation.SUPPORTS

//Service1里面:

/**
 * 如果当前有事务则加入事务中,如果没有则什么都不做,相当于没事务。
 */
@Transactional(propagation = Propagation.SUPPORTS)
    public void serviceC() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(1000);
        infoVo.setUserName("ceshi2");
        userInfoDao.save(infoVo); //没有被回滚哦
    }

//Service2里面:    
    //@Transactional,开启事务后,serviceC会和C1公用一个事务,如果这里没有开启,则serviceC不会自己创建事务。
    public void serviceC1() {
        Sercie1.serviceC();
    }

说明:
C1在调用C的过程中

  1. C1没有事务,则C也没有事务。
  2. C1有事务,则C加入到C1的事务中。

Propagation.NOT_SUPPORTED

这个的意思是一直处于无事务状态中执行,如果当前有事务则忽略事务,自己处在一个无事务中执行。
和上面正好反正,和never事务不一样哦,不会跑异常,自己只是安静的做事。

Propagation.MANDATORY

//Service1中:

    //Propagation.MANDATORY当前必须存在一个事务,否则抛出异常。
    @Transactional(propagation = Propagation.MANDATORY)
    public void serviceD() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(200);
        infoVo.setUserName("D");
        userInfoDao.save(infoVo);
    }

    public void serviceE() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(100);
        infoVo.setUserName("E");
        userInfoDao.save2(infoVo);
    }

//Service2中
    //这里调用
    public void serviceDE() {
        Sercie1.serviceE();//E正常入库了,
        Sercie1.serviceD();//D必须要有事务,不然则抛异常了。servcieDE上面没有事务,所有抛异常了。
    }

当前必须存在一个事务,否则抛出异常。

Propagation.NEVER

//Service1中:

    //Propagation.NEVER当前必须没有事务,否则抛出异常。
    @Transactional(propagation = Propagation.NEVER)
    public void serviceD() {
        System.out.println("执行到了这里。。。。");
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(200);
        infoVo.setUserName("D");
        userInfoDao.save(infoVo);
    }

    public void serviceE() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(100);
        infoVo.setUserName("E");
        userInfoDao.save2(infoVo);
    }

//Service2中
    //这里调用
    public void serviceDE() {
        Sercie1.serviceE();//E正常入库了,
        Sercie1.serviceD();//D必须要没有事务,不然则抛异常了。
    }

servcieDE上面没有事务,D正常以无事务的方式执行,
servcieDE上面有事务,D抛出异常,servcieD里面直接不回执行就已经往外抛出异常了。D有异常后,如果不处理则会连带servcieE也会滚,别忘记了serviceDE中事务。

Propagation.REQUIRES_NEW

//service1类中
/**
     * REQUIRES_NEW新建一个事务,不管当前有没有事务,都新建一个独立的事务。
     * 这里面serviceFG创建了一个事务,然后serviceF也创建了一个事务,他们互相独立;
     * 当前方法必须在自己的事务里运行,如果当前存在一个事务,则挂起该事务,这个事务执行完毕后,再唤醒挂起的事务。
     * 挂起事务使用suspend方法将原事务挂起。
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void serviceF() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(1000);
        infoVo.setUserName("F2");
        userInfoDao.save(infoVo); //这个里面有一个异常哦!
    }

    public void serviceG() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(1000);
        infoVo.setUserName("G2");
        userInfoDao.save2(infoVo);
    }

//servcie2中

    @Transactional
    public void serviceFG() {
        Sercie1.serviceG();

        try {
            Sercie1.serviceF();
        } catch (Exception e) {
            //e.printStackTrace();
        }
        Sercie1.serviceH();
    }

新建一个事务,不管当前有没有事务,都新建一个独立的事务。
如果当前存在事务,则把当前事务挂起,自己新创建一个事务,新事务执行完毕后再唤醒当前事务;两个事务没有依赖关系,可以实现自己新事务回滚了,但外部事务继续执行;外部事物回滚也不会影响到新事物的的提交。就是双方互不影响。

注意虽然sercieF新建了一个事务,但是如果serviceF抛出异常,还是需要捕获 不然serviceFG里面里面发现有异常抛出,就会把serviceG也给回滚了。

sercieFG有事务:

  1. sercieG()提交成,serviceF新建一个事务,serviceF内部有错,则自己回滚自己的;serviceFG捕获了sercieF的异常则不会影响到serviceFG的事务提交。
    如果不处理serviceF中的异常,serviceFG发现异常会回滚自己的事务,这个时候serviceG也会被回滚。
  2. serviceG()正常执行,sercieF()正常执行后,自己就提交事务了;serviceH抛异常了,会导致serviceG和serviceH中的数据库操作回滚,但是不会影响到serviceF中的提交。

sercieFG中无事务
sercieG(),sercieH该咋的就咋的,不存在回滚的情况,也不会影响到serviceF;serviceF会自己新建一个事务,自己处理自己内部的事。

Propagation.NESTED

//Sercie1
    /**
     * 嵌套事务,如果当前有事务则设置保存点,没有则新启一个事务
     */
    @Transactional(propagation = Propagation.NESTED)
    public void serviceH() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(1000);
        infoVo.setUserName("H");
        userInfoDao.save(infoVo); //这个里面有一个异常哦!
    }

    public void serviceI() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(1000);
        infoVo.setUserName("I");
        userInfoDao.save2(infoVo);
    }

//Sercie2    
    @Transactional
    public void serviceHI() {
        Sercie1.serviceI();
        try {
            Sercie1.serviceH();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

如果当前存在事务,则使用 SavePoint 技术把当前事务状态进行保存,然后底层共用一个连接,当NESTED内部出错的时候,自行回滚到 SavePoint这个状态,只要外部捕获到了异常,就可以继续进行外部的事务提交,而不会受到内嵌业务的干扰,但是,如果外部事务抛出了异常,整个大事务都会回滚。如果没有,则自己新建一个事务自己处理。

如果当前调用方有事务
1)serviceH方法内部报错,则只会回滚serviceH里面的。
2)serviceH方法内部不报错,但是外面的调用方报错了,则serviceH会跟着一起回滚。
3)serviceH方法内部不报错,外面也不报错,则serviceH和外面事务一起提交。

如果当前没有事务
serviceH就相当于一个自己新建了一个事务,和外面没有关系了,它内部就一个独立事务了。

在一个事务里面再嵌套一个事务,嵌套的事务就是在一个当前事务中设置一个保存点,保存点内部事务报错,则回滚保存点内部事务,不影响外面的。
与事务中新建事务的区别:在当前事务中新建事务,如果新事务中报错内部会回滚不影响外面的事务,外面的事务报错了,外面事务的回滚不会把新事务中的事务给回滚掉,而嵌套事务则会跟着外部事务一起回滚。

异常被捕获不回滚

userInfoDao.save(UserInfoVo userinfovo){
    int id = insert.executeAndReturnKey(params).intValue();//插入数据库
    throw new RuntimeException("测试异常");
}

@Transactional(rollbackFor = Exception.class)
    public void serviceI() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(102222);
        infoVo.setUserName("I");
        try {
            userInfoDao.save(infoVo); //这个里面有一个异常哦!
        } catch (Exception e) { //异常被catch住了,事务不会回滚。
            e.printStackTrace();
        }
    }

userInfoDao.save(infoVo);这个里面有个异常,抛异常后,被cry catch住了,所以这个事物不会回滚,数据还是插入数据库了。
异常被catch住了,就相当于没有异常了。

@Transactional注解的可用参数

timeout() 事务超时设置.超过这个时间,发生回滚,默认值为-1表示永不超时

readOnly() 只读事务,从这一点设置的时间点开始(时间点a)到这个事务结束的过程中,其他事务所提交的数据,该事务将看不见!(查询中不会出现别人在时间点a之后提交的数据)。注意是一次执行多次查询来统计某些信息,这时为了保证数据整体的一致性,要用只读事务

rollbackFor()导致事务回滚的异常类数组.

rollbackForClassName() 导致事务回滚的异常类名字数组

noRollbackFor 不会导致事务回滚的异常类数组

noRollbackForClassName 不会导致事务回滚的异常类名字数组

默认情况下,如果在事务中抛出了未检查异常(继承自 RuntimeException 的异常)或者 Error,则 Spring 将回滚事务;除此之外,Spring 不会回滚事务。你如果想要在特定的异常回滚可以考虑rollbackFor()等属性

多线程事务传播性失效

@Transactional(propagation = Propagation.NESTED)
    public void serviceH() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(10020);
        infoVo.setUserName("H");
        userInfoDao.save2(infoVo); //这个里面没有异常
}

public void serviceI() {
        UserInfoVo infoVo = new UserInfoVo();
        infoVo.setAge(1012);
        infoVo.setUserName("I");
        userInfoDao.save(infoVo);  //这个里面有异常
}    

@Transactional
    public void serviceHI() {
        System.out.println("HI......");

        new Thread(new Runnable() {
            public void run() {
                userInfoExtendService.serviceH();  //嵌套事务
            }
        }).start();
        //serviceI抛异常了,按理说serviceH也应该跟着一起回滚的,但是由于serviceH开启了一个独立的线程,所以serviceH已经和serviceI不是同一个事务了
        //serviceI抛异常了,serviceH不会跟着回滚
        userInfoExtendService.serviceI();
    }

前面介绍过嵌套事务,如果serviceH嵌套事务没有异常,serviceI有异常,他们是需要一起回滚的;serviceH有异常,serviceI没有异常,则serviceH自己回滚自己,serviceI继续提交。这里serviceI抛异常了,按理serviceH也应该一起回滚的,但是由于serviceH开启了一个独立的线程,所以serviceH已经和serviceI不是同一个事务了。事务的传播性也就断了。

注意:事务必须在同一个线程中才有效,serviceI与serviceH不在同一个线程中,他们之间就没有事务关系了。各自为政,各自提交各自的,自个为一个独立的事务。
原因很简单,spring事务一个线程绑定一个数据库session,在该线程的数据库session中修改数据库的事务属性,改为手动提交。如果不同线程则为不同的数据库session了,不同session是互相隔离的,所以serviceI与serviceH他们两个是两个线程,也就导致了最后操作数据库是两个session了。
当然,serviceI本身还是有事务特性的,serviceH本身也还有事务特性的。只是serviceI与serviceH不在是一个事务而已了。

Mysql命令行

Java及数据库事务_第6张图片

你可能感兴趣的:(IT)