目录
④ 编写代码(实体类、Mapper、service层......)
事务(Transaction)是指将一系列数据操作捆绑成为一个整体进行统一管理。如果某一事物执行成功,则该事物中进行的所有数据更改均会提交,成为数据库中的永久组成部分。如果事物执行时遇到错误且必须取消回滚,则数据将全部恢复到操作前的状态,所有数据的更改均被清除。通俗来说可以理解为(多个操作看作一个整体、要么都执行要么都不执行)。
比如说转账,张三需要给李四转账500。那么张三账户上面的金额是不是要减500,而李四的账户上要加500。那万一李四的账户上没有收到这500,而张三的账户上已经减了500。这是不是就有问题了。该怎么解决呢,就要用到我们的事物了!
原子性 ( Atomicity ) | 事务是一个完整的操作,事务的各步操作是不可分的(原子的),要么都执行,要么都不执行 |
一致性 ( Consistency ) | 当事务完成时,数据必须处于一致状态 |
隔离性 ( Isolation ) | 并发事务之间彼此隔离、独立,不应以任何方式依赖于或影响其他事务 |
持久性 ( Durability ) | 事务完成后,它对数据库的修改被永久保持 |
DEFAULT | 默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这个值就是READ_COMMITTED。 |
READ_UNCOMMITTED | 表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。 |
READ_COMMITTED | 表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。 |
REPEATABLE_READ | 表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。 |
SERIALIZABLE | 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 |
所谓事务传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时指定的一个事务性方法的执行行为。下面举个例子进行说明:
- @Transactional
- public void test1() {
- test2(); // test1() 调用 test2()
- }
-
- @Transactional(propagation = xxx)
- public void test2() {
- // 在开始当前事务之前,也就是 test2 事务之前,调用者 test1 已经存在事务管理,
- // 此时使用 propagation 属性指定 test2 事务的执行行为
- }
一般可分为以下六种行为:
REQUIRED | 默认值。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。 |
REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则把当前事务挂起。 |
SUPPORTS | 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 |
NOT_SUPPORTED | 以非事务方式运行,如果当前存在事务,则把当前事务挂起。 |
NEVER | 以非事务方式运行,如果当前存在事务,则抛出异常。 |
MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 |
NESTED | 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED。 |
所谓声明式事务,就是通过配置的方式,比如通过配置文件(xml)或者注解的方式,告诉spring,哪些方法需要spring帮忙管理事务,然后开发者只用关注业务代码,而事务的事情spring自动帮我们控制。
① 配置文件的方式,即在spring的xml文件中进行统一配置,开发者基本上就不用关注事务的事情了,代码中无需关心任何和事务相关的代码,一切交给spring处理。
② 注解的方式,只需在需要spring来帮忙管理事务的方法上加上@Transactional注解就可以了,注解的方式相对来说更简洁一些。
- <dependency>
- <groupId>org.springframeworkgroupId>
- <artifactId>spring-txartifactId>
- <version>5.2.2.RELEASEversion>
- dependency>
- "1.0" encoding="UTF-8"?>
- <beans xmlns="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:tx="http://www.springframework.org/schema/tx"
- xmlns:context="http://www.springframework.org/schema/context"
- xmlns:aop="http://www.springframework.org/schema/aop"
- xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
- http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
- http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
- http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd ">
- <tx:advice id="advice" transaction-manager="transactionManager">
- <tx:attributes>
-
- <tx:method name="insert*" rollback-for="java.lang.Exception"/>
- <tx:method name="delete*" rollback-for="java.lang.Exception"/>
- <tx:method name="update*" rollback-for="java.lang.Exception"/>
- tx:attributes>
- tx:advice>
-
- <aop:config>
-
- <aop:pointcut id="txPointcut" expression="execution (* com.cskt.service.impl.*ServiceImpl.*(..))"/>
-
- <aop:advisor advice-ref="advice" pointcut-ref="txPointcut"/>
- aop:config>
- package com.cskt.domain;
-
- import lombok.Data;
-
- import java.math.BigDecimal;
-
- /**
- * @Description TODO
- * @Author Yii_oo
- * @Date 2023-08-18 16:17
- * @Version 1.0
- * @ClassDesc 银行卡实体类
- */
- @Data
- public class Card {
- private int id;
- private String name;
- private BigDecimal money;
- }
- package com.cskt.mapper;
-
- import com.cskt.domain.Card;
- import org.apache.ibatis.annotations.Options;
- import org.apache.ibatis.annotations.Select;
- import org.apache.ibatis.annotations.Update;
- import org.springframework.stereotype.Repository;
-
- @Repository
- public interface CardMapper {
-
- /**
- * 转账的方法
- * @param card
- * @return
- */
- @Update("UPDATE card SET money = #{money} WHERE id = #{id};")//使用注解的方式
- @Options(useGeneratedKeys = true , keyProperty = "id")//useGeneratedKeys 设置为 true,表示启用数据库自动生成主键。
- int transferAccount(Card card); //keyProperty 指定了将生成的主键值赋值给 User 对象的 id 属性。
-
- /**
- * 根据id查询
- * @return
- */
- @Select("select * from card where id=#{id}")
- @Options(useGeneratedKeys = true , keyProperty = "id")
- Card selectById(int id);
- }
- package com.cskt.service;
-
- import com.cskt.domain.Card;
-
- public interface CardService {
- /**
- * 根据id查询
- * @param id
- * @return
- */
- Card selectById(int id);
-
- int transferAccount(Card card);
-
- int transferAccount2(Card card);
- }
- package com.cskt.service.impl;
-
- import com.cskt.domain.Card;
- import com.cskt.mapper.CardMapper;
- import com.cskt.service.CardService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.beans.factory.annotation.Qualifier;
- import org.springframework.stereotype.Service;
-
- /**
- * @Description TODO
- * @Author Yii_oo
- * @Date 2023-08-18 17:08
- * @Version 1.0
- */
- @Service("cardService")
- public class CardServiceImpl implements CardService {
- @Autowired
- @Qualifier("cardMapper")
- private CardMapper cardMapper;
-
- @Override
- public Card selectById(int id) {
- Card card = cardMapper.selectById(id);
- return card;
- }
-
- @Override
- public int transferAccount(Card card) {
- return cardMapper.transferAccount(card);
- }
-
- @Override
- public int transferAccount2(Card card) {
- int count = cardMapper.transferAccount(card);
- int i= 1/0; //模拟转账失败
- return count;
- }
- }
- package com.cskt.service.impl;
-
- import com.cskt.domain.Card;
- import com.cskt.service.CardService;
- import org.junit.Test;
- import org.springframework.context.ApplicationContext;
- import org.springframework.context.support.ClassPathXmlApplicationContext;
-
- import java.math.BigDecimal;
-
- import static org.junit.Assert.*;
-
- public class CardServiceImplTest {
- private ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
- private CardService cardService =(CardService) context.getBean("cardService");
- @Test
- public void TestTransferAccounts(){
- Card card=cardService.selectById(1);
- card.setMoney(card.getMoney().subtract(new BigDecimal(1000)));
- int count = cardService.transferAccount(card);
- System.out.println("执行第一次转账:-----"+count+"---------");
- Card card2=cardService.selectById(2);
- card2.setMoney(card2.getMoney().add(new BigDecimal(1000)));
- int count2 = cardService.transferAccount2(card2);
- System.out.println("执行第二次转账:-----"+count2+"---------");
- }
-
- }
表没被修改,这样就成功了!当我们把模拟报错给删掉,让他正常执行
- <tx:annotation-driven transaction-manager="transactionManager"/>
- // 其它注解
- @EnableTransactionManagement // 开启事务的注解支持,会自动加载事务管理器
- public class SpringConfig {
- // 注册各种 bean
- }
开启事务的注解支持之后,可以不用使用 XML,而是直接将 @Transactional 注解作用在类或者方法上。如下所示:
- @Transactional(rollbackFor=RuntimeException.class) //如果方法抛出了RuntimeException或其子类的异常,事务将被回滚.
- @Override
- public Card selectById(int id) {
- Card card = cardMapper.selectById(id);
- int i=1/0;
- return card;
- }
脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,可能最终不会存到数据库中,也就是不存在的数据。读到了不一定存在的数据,这就是脏读。
脏读最大的问题就是可能会读到不存在的数据。比如在上图中,事务B的更新数据被事务A读取,但是事务B回滚了,更新数据全部还原,也就是说事务A刚刚读到的数据并没有存在于数据库中。
从宏观来看,就是事务A读出了一条不存在的数据,这个问题是很严重的。
不可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据出现不一致的情况。
不可重复读即当事务 A 按照查询条件得到了一个结果集,这时事务 B 对事务 A 查询的结果集数据做了修改操作,之后事务 A 为了数据校验继续按照之前的查询条件得到的结果集与前一次查询不同,导致不可重复读取原始数据。
幻读是指当事务 A 按照查询条件得到了一个结果集,这时事务 B 对事务 A 查询的结果集数据做新增操作,之后事务 A 继续按照之前的查询条件得到的结果集平白无故多了几条数据,好像出现了幻觉一样。
以上面表为例子:
1、事务A,查询是否存在 id=3 的记录,没有则插入,这是我们期望的正常业务逻辑。
2、这个时候 事务B 新增的一条 id=3 的记录,并提交事务。
3、事务A,再去查询 id=3 的时候,发现还是没有记录(因为这里是在RR级别下研究(可重复读),所以读到依然没有数据)
4、事务A,插入一条 id=3 的数据。
最终 事务A 提交事务,发现报错了。这就很奇怪,查的时候明明没有这条记录,但插入的时候 却告诉我 主键冲突,这就好像幻觉一样。这才是所有的幻读。
不可重复读侧重表达 读-读,幻读则是说 读-写,用写来证实读的是鬼影。
上述所说的"脏读",“不可重复读”,"幻读"这些问题,其实就是数据库读一致性问题,必须由数据库提供的事务隔离机制来进行解决。