Spring事务管理需要注意的点

大多数的人刚接触到数据库事务的时候肯定是一张懵圈脸,一脸迷茫透露着求知欲。在这给大家科普一些事务知识。:

  • 事务的概念
  • 事务的特征
  • Mysql数据库存储引擎支持
  • 修改Mysql数据库存储引擎
  • spring配置事务
  • 测试代码
  • 注意

事务的概念

事务(Transaction)是并发控制的单位,是用户定义的一个操作序列。这些操作要么都做,要么都不做,是一个不可分割的工作单位。通过事务将逻辑相关的一组操作绑定在一起,以便服务器保持数据的完整性。
COMMIT:表示提交,即提交事务的所有操作。具体地说就是将事务中所有对数据库的更新写回到磁盘上的物理数据库中去,事务正常结束。
ROLLBACK表示回滚,即在事务运行的过程中发生了某种故障,事务不能继续进行,系统将事务中对数据库的所有以完成的操作全部撤消,滚回到事务开始的状态。

ex:A用网银给B实现网上转账<这是一个场景>
第一种情况:A用户金额减少B用户金额增加<正常情况>
第二种情况:A用户金额减少而在途中出现异常B用户金额没有增加<异常情况>
第三种情况:A用户金额没有减少而B用户金额却增加了<异常情况>
。。。
除去第一种情况,其余的都是异常情况;这种情况下,就得使用事务。要么做要么不做即:A账户减少那么B账户一定增加,否则A账户就不会减少。

事务的特征

事务四大特性(简称ACID):原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)

原子性(Atomicity):事务中的全部操作在数据库中是不可分割的,要么全部完成,要么均不执行。

一致性(Consistency):几个并行执行的事务,其执行结果必须与按某一顺序串行执行的结果相一致。

隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。

持久性(Durability):对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。

Mysql数据库存储引擎支持

Mysql的存储引擎:MyIsAm、InnoDB、MEMORY、MERGE这四种,四种各有各的好处,技术选型可以针对不同的需求选择不同的方式。 MyIsAm
MyISAM是MySQL的默认存储引擎。MyISAM不支持事务、也不支持外键,但其访问速度快,对事务完整性没有要求。
MyISAM表还支持3中不同的存储格式
静态表
静态表是默认的存储格式,静态表中的字段都是非变长的字段,
优点是:存储非常迅速,容易缓存,出现故障容易恢复;
缺点是:占用的空间通常比动态表多。(注意: 在存储时,列的宽度不足时,用空格补足,当时在访问的时候并不会得到这些空格)
动态表
动态表的字段是变长的,优点是:占用的空间相对较少,但是频繁地更新删除记录会产生碎片,
需要定期改善性能,并且出现故障的时候恢复相对比较困难。
压缩表
压缩表占用磁盘空间小,每个记录是被单独压缩的,所以只有非常小的访问开支。
InnoDB
InnoDB存储引擎提供了具有提交、回滚和崩溃恢复能力的事务安全。但是比起MyISAM存储引擎,
InnoDB写的处理效率差一些并且会占用更多的磁盘空间以保留数据和索引。
而且MySQL支持外键存储引擎只有InnoDB,在创建外键的时候,
要求附表必须有对应的索引,子表在创建外键的时候也会自动创建对应的索引。(被关联表的外键必须是关联表的主键)
InnoDB的理想使用场合:高并发,更新操作比较多的表。需要使用事务的表。对自动灾难恢复有要求的表。
MEMORY
MEMORY存储引擎使用存在内存中的内容来创建表。
每个MEMORY表只实际对应一个磁盘文件。MEMORY类型的表访问非常得快,因为它的数据是放在内存中的,并且默认使用HASH索引。
但是一旦服务关闭,表中的数据就会丢失掉。Memory存储引擎的使用场合,速度要求快的,临时数据
MERGE
merge存储引擎是一组MyISAM表的组合,这些MyISAM表结构必须完全相同,MERGE表中并没有数据,
对MERGE类型的表可以进行查询、更新、删除的操作,这些操作实际上是对内部的MyISAM表进行操作。 对于对MERGE表进行的插入操作,是根据INSERT_METHOD子句定义的插入的表,可以有3个不同的值, first和last值使得插入操作被相应的作用在第一个或最后一个表上,不定义这个子句或者为NO,
表示不能对这个MERGE表进行插入操作。可以对MERGE表进行drop操作,这个操作只是删除MERGE表的定义, 对内部的表没有任何影响。MERGE在磁盘上保留2个以MERGE表名开头文件:.frm文件存储表的定义; .MRG文件包含组合表的信息,包括MERGE表由哪些表组成,插入数据时的依据。
可以通过修改.MRG文件来修改MERGE表,但是修改后要通过flush table刷新。

Mysql主流的引擎方式就是这4种,可以按照条件来选择相应的。

修改Mysql数据库存储引擎

show engines; #显示数据库是否支持InnoDB
更改方式1:修改配置文件my.cnf
打开my.cnf,在[mysqld]最后添加为上default-storage-engine=InnoDB,重启数据库服务,数据库默认的引擎修改为InnoDB
更改方式2:在建表的时候指定
create table tableName( id int primary key, name varchar(50) )type=InnoDB;
更改方式3:建表之后修改
alter table tableName ENGINE=InnoDB; #mysql5.0以后用这种方式
alter table tableName type = InnoDB; #mysql5.0之前用这种方式

spring配置事务

这里列举两种方式,一种是aop自动注入的方式,一种是基于注解的方式。

自动注入的方式
在aop横切的时候,按照规则在需要的地方,注入事务。这样造成的结果是可能不需要的地方也会被注入。造成资源的浪费。好处是不用忘记,你不用管有没有地方没有被事务管理到。
    <!-- 配置数据源 使用的是Druid数据源 -->
    <bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
        init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />

        <!-- 初始化连接大小 -->
        <property name="initialSize" value="0" />
        <!-- 连接池最大使用连接数量 -->
        <property name="maxActive" value="20" />

        <!-- 连接池最小空闲 -->
        <property name="minIdle" value="0" />
        <!-- 获取连接最大等待时间 -->
        <property name="maxWait" value="60000" />
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
            value="33" />
        <!-- 用来检测有效sql -->
        <property name="validationQuery" value="${validationQuery}" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="25200000" />
        <!-- 打开removeAbandoned功能 -->
        <property name="removeAbandoned" value="true" />
        <!-- 1800秒,也就是30分钟 -->
        <property name="removeAbandonedTimeout" value="1800" />
        <!-- 关闭abanded连接时输出错误日志 -->
        <property name="logAbandoned" value="true" />
        <!-- 监控数据库 -->
        <property name="filters" value="mergeStat" />
    </bean>
    <!-- myBatis文件 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!-- 添加mybatis的配置 -->
        <property name="configLocation" value="classpath:mybatis.xml"/>
        <!-- 自动扫描entity目录, 省掉Configuration.xml里的手工配置 -->
        <property name="mapperLocations" value="classpath:com/tanrice/dao/impl/*.xml" />
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.tanrice.dao" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    </bean>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- 配置事物 -->
    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="add*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="append*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="insert*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="save*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="update*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="modify*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="edit*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="delete*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="remove*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="repair" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="delAndRepair" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="get*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="find*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="load*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="search*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="datagrid*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
        </tx:attributes>
    </tx:advice>

    <!-- Spring aop事务管理 -->
    <aop:config>
        <aop:pointcut id="transactionPointcut" expression="execution(* com.projectname.service..*Impl.*(..))" />
        <aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice" />
    </aop:config>
    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate" scope="prototype">
         <constructor-arg index="0" ref="sqlSessionFactory" /> 
    </bean>
基于注解的方式
基于注解的方式是在需要使用的类或者方法上面加上@Transactional注解。这样做的好处是减少资源的浪费,不好的地方是有时候会忘记,一旦忘记的话就会造成有些方法没有事务。
    <!-- 配置数据源 使用的是Druid数据源 -->
    <bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
        init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />

        <!-- 初始化连接大小 -->
        <property name="initialSize" value="0" />
        <!-- 连接池最大使用连接数量 -->
        <property name="maxActive" value="20" />

        <!-- 连接池最小空闲 -->
        <property name="minIdle" value="0" />
        <!-- 获取连接最大等待时间 -->
        <property name="maxWait" value="60000" />
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
            value="33" />
        <!-- 用来检测有效sql -->
        <property name="validationQuery" value="${validationQuery}" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="25200000" />
        <!-- 打开removeAbandoned功能 -->
        <property name="removeAbandoned" value="true" />
        <!-- 1800秒,也就是30分钟 -->
        <property name="removeAbandonedTimeout" value="1800" />
        <!-- 关闭abanded连接时输出错误日志 -->
        <property name="logAbandoned" value="true" />
        <!-- 监控数据库 -->
        <property name="filters" value="mergeStat" />
    </bean>

    <!-- myBatis文件 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!-- 添加mybatis的配置 -->
        <property name="configLocation" value="classpath:mybatis.xml"/>
        <!-- 自动扫描entity目录, 省掉Configuration.xml里的手工配置 -->
        <property name="mapperLocations" value="classpath:com/tanrice/dao/impl/*.xml" />
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.tanrice.dao" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    </bean>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>


    <!-- 注解的方式配置事务 -->
    <!-- 在需要的地方配置 -->
    <tx:annotation-driven transaction-manager="transactionManager"  proxy-target-class="true"/>

    <!-- 注解方式配置事物 -->
    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="add*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="append*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="insert*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="save*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="update*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="modify*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="edit*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="delete*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="remove*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="repair" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="delAndRepair" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="get*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="find*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="load*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="search*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="datagrid*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
        </tx:attributes>
    </tx:advice>
    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate" scope="prototype">
         <constructor-arg index="0" ref="sqlSessionFactory" /> 
    </bean>
@Service
@Transactional
public class UserTaskServiceImpl implements UserTaskService{
    
    

    @Autowired
    private UserTaskDao userTaskDao;

    @Autowired
    private TaskDao taskDao;

    public boolean updateUserTask(UserTask userTask,int id) {
        boolean flag = false;
        try{
            flag = userTaskDao.saveUserTask(userTask)>0?true:false;
            System.out.println("插入:"+flag);
            flag = taskDao.updateTaskLeftcountById(id)>0?true:false;
            System.out.println("修改:"+flag);
        }catch(Exception e){
            throw new RuntimeException();
        }
        return flag;
    }
}

以上的两种方式自己考虑使用,我在xml配置文件的注释写的比较详细,就不一点点解释了。

测试代码

service方法已经写了。主要是把mybatis的代码贴一下。我把mybatis里面写的一个SQL语句故意写错了。让它运行的时候出错。

    <update id="updateTaskLeftcountById">
        <!-- leftcount为int类型,所以加'aas'的时候会出错 -->
        update t_tm_task_table set leftcount = leftcount+aas where id = #{id}
    </update>
    @org.junit.Test
    public void testTrx(){
        UserTask userTask = new UserTask();
        userTask.setFullname("wanda");
        userTask.setUserid(10071);
        userTask.setTaskid(10026);
        userTask.setGettime(new Date().getTime());
        userTask.setState(5);

        System.out.println("结果是:"+userTaskService.updateUserTask(userTask, 10026));
    }

注意

在使用事务的过程中要注意几个点。

  • 为什么不在dao层上设置事务
    • 最根本的原因:因为dao层直接操作数据库,责任单一无须回滚,成功就是成功,失败就是失败。
  • 事务的隔离级别
    • Read uncommitted 读未提交:我们所说的脏读,两个并发的事务,“事务A:领导给singo发工资”、“事务B:singo查询工资账户”,事务B读取了事务A尚未提交的数据。
    • Read committed 读提交:我们所说的不可重复读,两个并发的事务,“事务A:singo消费”、“事务B:singo的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。
    • Repeatable read 重复读:可以避免不可重复读。当singo拿着工资卡去消费时,一旦系统开始读取工资卡信息(即事务开始),singo的老婆就不可能对该记录进行修改,也就是singo的老婆不能在此时转账。MySQL的默认隔离级别就是Repeatable read。
    • Serializable 序列化:Serializable是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。
    <!-- 注解方式配置事物 -->
    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="add*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="append*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="insert*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="save*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="update*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="modify*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="edit*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="delete*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="remove*" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="repair" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="delAndRepair" propagation="REQUIRED" isolation="READ_COMMITTED" />
            <tx:method name="get*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="find*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="load*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="search*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="datagrid*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
            <tx:method name="*" propagation="SUPPORTS" isolation="READ_COMMITTED" />
        </tx:attributes>
    </tx:advice>

这是我上面配置的事务隔离,<tx:method name="repair" propagation="REQUIRED" isolation="READ_COMMITTED" /> 看isolation的配置项目,这个就是配置的隔离级别

猜你喜欢

转载自blog.csdn.net/my_God_sky/article/details/53291138