author 曹扬进

createTime 2022-05-09


2022-05-19: 培训结束

# MySQL 事务及 Spring 中的@Transactional 注解

# MySQL 事务

MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你既需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务!

  • 在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
  • 事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
  • 事务用来管理 insert,update,delete 语句

一般来说,事务是必须满足4个条件(ACID)::原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

隔离级别详解

4种不同的隔离级别是出于性能的考虑

  • 未提交读(read uncommitted)**会产生脏读

未提交读是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。未提交读是一种危险的隔离级别,所以一般在我们实际的开发中应用不广 , 但是它的优点在于并发能力高,适合那些对数据一致性没有要求而追求高并发的场景 ,它的最大坏处是出现脏读 。

  • 读写提交(read committed)**会产生不可重复读

读写提交隔离级别,是指一个事务只能读取另一个事务已经提交的数据,不能读取未提交的数据。

  • 可重复读 会产生幻读

可重复读的目标是克服读写提交中出现的不可重复读的现象,因为在读写提交的时候,可能出现一些值的变化, 影响当前事务的执行,如上述的库存是个变化的值,这个时候数据库提出 了可重复读的隔离级别

  • 串行化(Serializable)

串行化(Serializable)是数据库最高的隔离级别,它会要求所有的 SQL 都会按照顺序执行,这样就可以克服上述隔离级别出现的各种问题,所以它能够完全保证数据的一致性 。

追求更高的隔离级别,它能更好地保证了数据的一致性,但是也要付出锁的代价 。有了锁,就意味着性能的丢失,而且隔离级别越高,性能就越是直线地下降 所以在现实中一般而言,选择隔离级别会以读写提交为主,它能够防止脏读,而不能避免不可重复读和幻读。

# @Transactional 注解

@Transactional是spring中常用的注解之一,通常情况下我们在需要对一个service方法添加事务时,加上这个注解,如果发生unchecked exception,就会发生回滚。在需要多表操作的业务中会出现部分表数据库操作失败的情况,这时将会在数据库中产生脏数据,而@Transactional注解可以将多表操作当作一个整体,从而避免脏数据的出现。

# 属性

属性 类型 描述
value String 可选的限定描述符,指定使用的事务管理器
propagation enum: Propagation 可选的事务传播行为设置,默认值为 REQUIRED
isolation enum: Isolation 可选的事务隔离级别设置,默认值为DEFAULT
readOnly boolean 读写或只读事务,默认读写
timeout int (in seconds granularity) 事务超时时间设置
rollbackFor Class对象数组,必须继承自Throwable 导致事务回滚的异常类数组
rollbackForClassName 类名数组,必须继承自Throwable 导致事务回滚的异常类名字数组
noRollbackFor Class对象数组,必须继承自Throwable 不会导致事务回滚的异常类数组
noRollbackForClassName 类名数组,必须继承自Throwable 不会导致事务回滚的异常类名字数组

Propagation 枚举介绍

枚举 含义
REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务,则加入到这个事务中
SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行
MANDATORY 表示当前方法必须在事务中运行。如果当前事务不存在,则会抛出一个异常
REQUIRES_NEW 表示当前方法必须运行在它自己的事务中(一个新的事务将被启动)。如果存在当前事务,在该方法执行期间,当前事务会被挂起
NOT_SUPPORTED 表示该方法不运行在事务中。如果当前存在事务,就把当前事务挂起
NEVER 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常
NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与REQUIRED类似的操作

Isolation 枚举介绍

枚举 含义
DEFAULT 使用后端数据库默认的隔离级别
READ_UNCOMMITTED 允许读取尚未提交的数据变更(最低的隔离级别)
READ_COMMITTED 允许读取并发事务已经提交的数据
REPEATABLE_READ 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改
SERIALIZABLE 完全服从ACID的隔离级别,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的(最高的隔离级别)

# 使用范围

@Transactional注解 可以作用于接口、接口方法、类以及类方法上。

  • 当用作类上时,该类上的所有 public 方法将都具有该类型的属性。
  • 当用作方法上是,该方法所在类上的注解将失效,该注解只能应用在 public 方法上。
  • 不建议用在接口或接口方法上,因为这只有在使用基于接口的动态代理是才会生效

# 使用说明

  • 在启动类上添加@EnableTransactionManagement注解。
  • 用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。
  • 在项目中,@Transactional(rollbackFor=Exception.class),如果类加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。
  • 在@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚。

# 注解失效问题

正常情况下,只要在方法上添加@Transactional注解就完事了,但是需要注意的是,虽然使用简单,但是如果不合理地使用注解,还是会存在注解失效的问题。

@Transactional 应用在非 public 修饰的方法上

事务拦截器在目标方法执行前后进行拦截,内部会调用方法来获取Transactional 注解的事务配置信息,调用前会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。

@Transactional 注解属性 rollbackFor 设置错误

rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定rollbackFor属性。

同一个类中方法调用,导致@Transactional失效

开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。 那为啥会出现这种情况?其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

异常被你的 catch“吃了”导致@Transactional失效

如果你手动的catch捕获这个异常并进行处理,事务管理器会认为当前事务应该正常commit,就会导致注解失效,如果非要捕获且不失效,就必须在代码块内throw new Exception抛出异常。

数据库引擎不支持事务

开启事务的前提就是需要数据库的支持,我们一般使用的Mysql引擎时支持事务的,所以一般不会出现这种问题。

# 多数据源配置下的注意点

MyBatis自动参与到spring事务管理中,无需额外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的数据源与DataSourceTransactionManager引用的数据源一致即可,否则事务管理会不起作用。数据库配置案例如下:

@Configuration
@MapperScan(basePackages = {"com.lowan.**.log_mapper.**"},
        sqlSessionFactoryRef = "logSqlSessionFactory")
@EnableConfigurationProperties({MybatisProperties.class, SpringBootShardingRuleConfigurationProperties.class, SpringBootPropertiesConfigurationProperties.class})
public class LogDataSourceConfig {

    @Value("${spring.datasource.iotlog.db-name}")
    private String logDataBase;

    @Autowired
    private SpringBootShardingRuleConfigurationProperties shardingProperties;

    @Autowired
    private SpringBootPropertiesConfigurationProperties shardingConfigurationProperties;

    @Bean(name = "logDataSource")
    @ConfigurationProperties("spring.datasource.iotlog")
    public DataSource logDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "shardingDataSource")
    public DataSource shardingDataSource(@Qualifier("logDataSource") DataSource logDataSource) throws SQLException {
        Map<String, DataSource> dataSourceMap = new LinkedHashMap<>();
        dataSourceMap.put(logDataBase, logDataSource);
        return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingProperties.getShardingRuleConfiguration(), new HashMap<>(), shardingConfigurationProperties.getProps());
    }

    @Bean(name = "logTransactionManager")
    public DataSourceTransactionManager setTransactionManager(@Qualifier("logDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }


    @Bean(name = "logSqlSessionFactory")
    public SqlSessionFactory setSqlSessionFactory(@Qualifier("shardingDataSource") DataSource dataSource
            , IotProperties iotProperties, MybatisProperties mybatisProperties) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setTypeHandlers(new BaseTypeHandler[]{new ArrayJsonHandler(), new ObjectJsonHandler()});
        if (mybatisProperties != null) {
            Properties configurationProperties = mybatisProperties.getConfigurationProperties();
            bean.setConfigurationProperties(configurationProperties);
        }
        if (iotProperties.getControl().isPrintSqlLog()) {
            org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
            configuration.setLogImpl(StdOutImpl.class);
            bean.setConfiguration(configuration);
        }
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:log_mapper/**/*.xml"));
        return bean.getObject();
    }

}