1 Star 2 Fork 3

Rey Wong / 最佳实践

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
springtransaction.md 13.20 KB
一键复制 编辑 原始数据 按行查看 历史
wr090097 提交于 2021-07-05 11:53 . init

事务基本概念及Spring事务管理机制

内容总览

备注:本文主要针对Spring对数据库的事务管理,及项目中注事项

事务基本概念

事务的基本特性(ACID)

  • 原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

  • 一致性(Consistency)

事务前后数据的完整性必须保持一致。

  • 隔离性(Isolation)

事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。

  • 持久性(Durability)

持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

事务的并发问题

  • 脏读

事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

  • 不可重复读

事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。

  • 幻读

系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

如图

分布式事务

分布式事物基本理论:基本遵循CPA理论,采用柔性事物特征,软状态或者最终一致性特点保证分布式事物一致性问题

CAP理论作为分布式系统的基础理论,它描述的是一个分布式系统在以下三个特性中:

一致性(Consistency) 可用性(Availability) 分区容错性(Partition tolerance)

无法全部满足三个特性

2PC两段提交协议

3PC三阶段提交协议

分布式事务应用

  • AT

  • XA

  • SAGA

  • TCC

Mysql事务的隔离级别

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

Spring事务管理基本概念

Spring事务传播行为 Propagation(7种)

|变量|内容|是否默认|备注| |:-|:-|:-| |Propagation.REQUIRED|支持当前事务,如果不存在 就新建一个|是|保证同一个事务中| |Propagation.SUPPORTS|支持当前事务,如果不存在,就不使用事务|否|保证同一个事务中| |Propagation.MANDATORY|支持当前事务,如果不存在,抛出异常|否|保证同一个事务中| |Propagation.REQUIRES_NEW|如果有事务存在,挂起当前事务,创建一个新的事务|否|保证没有在同一个事务中| |Propagation.NOT_SUPPORTED|以非事务方式运行,如果有事务存在,挂起当前事务|否|保证没有在同一个事务中| |Propagation.NEVER|以非事务方式运行,如果有事务存在,抛出异常|否|保证没有在同一个事务中| |Propagation.NESTED|如果当前事务存在,则嵌套事务执行|否|保证没有在同一个事务中|

Spring事务隔离级别 Isolation (5种)

变量 内容 描述
Isolation.DEFAULT 默认隔离级别 使用数据库默认的事务隔离级别
Isolation.READ_UNCOMMITTED 未提交读(read uncommited) 脏读,不可重复读,虚读都有可能发生
Isolation.READ_COMMITTED 已提交读(read commited) 避免脏读。但是不可重复读和虚读有可能发生(需事务提交)
Isolation.REPEATABLE_READ 可重复读(repeatable read) 避免脏读和不可重复读.但是虚读有可能发生(行锁)
Isolation.SERIALIZABLE 串行化的(serializable) 避免以上所有读问题(表锁)
 Mysql 默认:可重复读
 Oracle 默认:读已提交

最佳实践

  • 数据库管理必须使用池化技术,最好交给Spring管理

常见的有C3P0,dbcp,druid,hikari(Springboot默认) 目前FSP和EDS使用的是druid

  • 避免开启不必要的事务

只要被事务覆盖到的方法都会占用连接,如果随意开启事务,会造成连接池连接连接不够用

    //通过@Transactional注解开启事务
    //虽然没有操作数据库,但会占用一个连接池中的一个连接
    //会直接占用一个连接20S
    //此时会产生,数据库没有什么请求,但应用报无法从连接池获取连接
    @Transactional
    public void findTransactional() {
        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

开启事务的连接数

没有开启事务的连接数

报错如下

  • 事务作用在最小单元
    //通过@Transactional注解开启事务
    // 1 中为调用接口,不涉及事务,如果该操作耗时太长,会一直占用连接
    @Transactional
    public void insertTransactional() {
         //1.调用接口获取数据http,doubbo
        String code=HttpClient.getCode();
        //2. 操作数据库
        jdbcTemplate.queryForList("insert into subtable(sub) values('123'));
    }
    
    
    //修改如下,将事务作用在最小单元
    public void insertInfo(){
       String code=HttpClient.getCode();  
      
       insertTransactional
    }
    
    @Transactional
    public void insertTransactional() {
            //2. 操作数据库
        jdbcTemplate.queryForList("insert into subtable(sub) values('123'));
    }
    
  • 带事务的update没有使用索引会锁表
//开启了事务后,执行update操作,如果查询条件没有索引,会锁表
//在开启事务的情况下,执行update操作 where条件尽量带索引字段
  • 禁止使用切面的方式注入事务
  //如果有下面的配置,都要进行修改
  //找出需要使用事务的代码(多张表必须同时更新成功的情况),使用@Transactional注解开启事务开启事务
  //在配置中删除下面的事务管理切面
  <tx:advice id="txAdvice" transaction-manager="transactionManager">
      <tx:attributes>
          <tx:method name="create*" propagation="REQUIRED" rollback-for="Exception"/>
          <tx:method name="update*" propagation="REQUIRED" rollback-for="Exception"/>
          <tx:method name="delete*" propagation="REQUIRED" rollback-for="Exception"/>
          <tx:method name="do*" propagation="REQUIRED" rollback-for="Exception"/>
          <tx:method name="save*" propagation="REQUIRED" rollback-for="Exception"/>
          <!-- 上传文件,需要将文件信息添加到数据库中. -->
          <tx:method name="upload" propagation="REQUIRED" rollback-for="Exception"/>
          <tx:method name="find*" read-only="true"/>
      </tx:attributes>
  </tx:advice>
  • 避免不必要的事务嵌套
     
     @Transactional
     public void updateTransactionNestd(int propagation) {
        //带事务
        nestTransactionSupports();
        
        updateNestd()
        
     }

    //@Transactional 相当于  @Transactional(propagation = Propagation.REQUIRED)
    @Transactional
    public void nestTransactionSupports() {
        updateNestd();
    }
    //事务的传播特性
    public void updateNestd() {
        logger.info("【{}】开启事务,当前活跃连接数={}", "update", ((DruidDataSource) jdbcTemplate.getDataSource()).getActiveCount());
        String id = "nest" + UUID.randomUUID().toString();
        String parentTableSql = "insert into parenttable(parent) values('" + id + "')" ;
        String subTableSql = "insert into subtable(sub) values('" + id + "')" ;
        logger.info("【{}】执行sql={}", "updateNestd", parentTableSql);
        jdbcTemplate.update(parentTableSql);
        logger.info("【{}】执行sql={}", "updateNestd", subTableSql);
        jdbcTemplate.update(subTableSql);
    }
  • 注意自定义事务回滚的写法
    //错误写法
    @Transactional
    public void updateTransactional(String id) {
        logger.info("【{}】开启事务,当前活跃连接数={}", "updateTransactional", ((DruidDataSource) jdbcTemplate.getDataSource()).getActiveCount());
        String parentTableSql = "insert into parenttable(parent) values('" + id + "')" ;
        String subTableSql = "insert into subtable(sub) values('" + id + "')" ;
        //捕获了一次无法回滚
        try {
            jdbcTemplate.update(parentTableSql);
            if (true) {
                throw new Exception();
            }
            jdbcTemplate.update(subTableSql);
        } catch (Exception e) {

        }
    }

    //正确写法
    //1.需要抛出异常
    //2.需要添加rollbackFor参数
    @Transactional(rollbackFor = Exception.class)
    public void updateTransactionalRollback(String id) throws Exception {
        logger.info("【{}】开启事务,当前活跃连接数={}", "updateTransactional", ((DruidDataSource) jdbcTemplate.getDataSource()).getActiveCount());
        String parentTableSql = "insert into parenttable(parent) values('" + id + "')" ;
        String subTableSql = "insert into subtable(sub) values('" + id + "')" ;
        
        jdbcTemplate.update(parentTableSql);
        if (true) {
            throw new Exception();
        }
        jdbcTemplate.update(subTableSql);
    }

    //Integer.parseInt("aaa"); 模拟异常,不进行捕获
    //下面也可以回滚
    @Transactional
    public void updateTransactionalError(String id) {
        logger.info("【{}】开启事务,当前活跃连接数={}", "updateTransactional", ((DruidDataSource) jdbcTemplate.getDataSource()).getActiveCount());
        String parentTableSql = "insert into parenttable(parent) values('" + id + "')" ;
        String subTableSql = "insert into subtable(sub) values('" + id + "')" ;
        jdbcTemplate.update(parentTableSql);
        Integer.parseInt("aaa");
        jdbcTemplate.update(subTableSql);
    }
  • 同一个事务不能再中间提交

public int insertBatchs(Account account) {
  int j=0;
  SqlSession sqlSession = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH);
  AccountMapper accountMapper= sqlSession.getMapper(AccountMapper.class);

  for (int i = 0; i < 100; i++) {
      account = new Account();
      account.setId(40+i);
      account.setUserId("test:"+i);
      account.setMoney(5+i);
      j= accountMapper.insertBatchs(account);
      //该处提交过后,真个连接就关闭回收了,后面的数据库操作都会报错
      //
      sqlSession.commit();
  }
  sqlSession.commit();
  return j;
}

报错如下

其它

Druid推荐配置


<bean id="dataSource"
 class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy">
    <property name="targetDataSource">
        <bean id="dataSourceTarget" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
            <property name="driverClassName" value="${db.driver}"/>
            <property name="url" value="${db.url}"/>
            <property name="username" value="${db.username}"/>
            <property name="password" value="${db.password}"/>
            <property name="initialSize" value="${db.initialSize:5}"/>
            <property name="minIdle" value="${db.minIdle:10}"/>
            <property name="maxActive" value="${db.maxActive:20}"/>
            <!-- 配置获取连接等待超时的时间 -->
            <property name="maxWait" value="${db.maxWait:60000}"/>
            <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
            <property name="timeBetweenEvictionRunsMillis" value="${db.timeBetweenEvictionRunsMillis:60000}"/>
            <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
            <property name="minEvictableIdleTimeMillis" value="${db.minEvictableIdleTimeMillis:300000}"/>
            <!--不开启事务,默认自动提交-->
            <property name="defaultAutoCommit" value="${db.defaultAutoCommit:true}"/>
            <!-- 打开PSCache,如果用Oracle,则把poolPreparedStatements配置为true,mysql可以配置为false。分库分表较多的数据库,建议配置为false -->
            <property name="poolPreparedStatements" value="${db.poolPreparedStatements:false}"/>
            <!--自动回收已断开链接-->
            <property name="validationQuery" value="SELECT 1 FROM DUAL"></property>
            <property name="testWhileIdle" value="${db.testWhileIdle:true}"></property>
            <property name="testOnBorrow" value="${db.testOnBorrow:false}"></property>
            <property name="testOnReturn" value="${db.testOnReturn:false}"></property>
            <property name="logAbandoned" value="${db.logAbandoned:true}"></property>
            <property name="removeAbandoned" value="${db.removeAbandoned:true}"></property>
            <property name="removeAbandonedTimeout" value="${db.removeAbandonedTimeout:3600}"></property>

        </bean>
    </property>
</bean>

1
https://gitee.com/reywong/best_practices.git
git@gitee.com:reywong/best_practices.git
reywong
best_practices
最佳实践
master

搜索帮助

53164aa7 5694891 3bd8fe86 5694891