spring的声明式事务

spring的声明式事务

什么是事务?

一组操作,形成一个业务,那么这组操作要么都成功,要么都失败。保证业务操作完整性一种操作。

示例:比如转账,张三账户扣钱,和李四账户加钱,这两个操作一定要同时成功。

Spring JdbcTemplate

​ 在spring中为了更加方便的操作JDBC,在JDBC的基础之上定义了一个抽象层,此设计的目的是为不同类型的JDBC操作提供模板方法,每个模板方法都能控制整个过程,并允许覆盖过程中的特定任务,通过这种方式,可以尽可能保留灵活性,将数据库存取的工作量降到最低。

引入依赖

        <!--spring整合第三方ORM框架的包,这个依赖还会同时引入spring-jdbc和spring-tx(事务)的包还有springIoc的基础jar包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>5.2.6.RELEASE</version>
        </dependency>

<!--        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.6.RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.2.6.RELEASE</version>
            <scope>compile</scope>
        </dependency>-->

驱动和数据库版本对应关系:https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-versions.html

配置连接池和JdbcTemplate对象

    <!--配置扫描-->
    <context:component-scan base-package="com.blog"/>
    <!--引入外部配置文件-->
    <context:property-placeholder location="db.properties"/>
    <!--配置连接池对象-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="username" value="${mysql.username}"/>
        <property name="password" value="${mysql.password}"/>
        <property name="url" value="${mysql.url}"/>
        <property name="driverClassName" value="${mysql.driverClassName}"/>
    </bean>
    <!--配置jdbc-->
    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--具名参数jdbc处理类-->
    <bean class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate" id="namedParameterJdbcTemplate">
        <constructor-arg type="javax.sql.DataSource" ref="dataSource"/>
    </bean>

测试方法展示

ClassPathXmlApplicationContext context;

@Before
public void before(){
    context = new ClassPathXmlApplicationContext("classpath:spring-ioc.xml");
}
@Test
public void test1(){
    DruidDataSource dataSource = context.getBean("dataSource", DruidDataSource.class);
    System.out.println(dataSource);
}
/*
* jdbcTemplate连接测试
* */
@Test
public void test2(){
    JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
    Integer integer = jdbcTemplate.queryForObject("select  count(1) from user", Integer.class);
    System.out.println(integer);
}

/*
 * jdbcTemplate连接测试
 * */
@Test
public void test3(){
    JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
    /*
    * 如果和数据库字段一致
    * */
    //        User user = jdbcTemplate.queryForObject("select  count(1) from User", new BeanPropertyRowMapper<>(User.class));
    User o = jdbcTemplate.queryForObject("select  * from user where id=1",
            (resultSet, i) -> {
                /*从结果集中获取数据*/
                User user = new User();
                user.setBalance(resultSet.getInt("BALANCE"));
                user.setId(resultSet.getInt("ID"));
                user.setCardno(resultSet.getString("CARD_NO"));
                return user;
            });
    System.out.println(o);
}

/*
 * 查询实体list
 * */
@Test
public void test4(){
    JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);

    List<User> userList = jdbcTemplate.query("select  * from user", new RowMapper<User>() {
        @Override
        public User mapRow(ResultSet resultSet, int i) throws SQLException {
            /*从结果集中获取数据*/
            User user = new User();
            user.setBalance(resultSet.getInt("BALANCE"));
            user.setId(resultSet.getInt("ID"));
            user.setCardno(resultSet.getString("CARD_NO"));
            return user;
        }
    });
    System.out.println(userList);
}

/*
 * 新增
 * */
@Test
public void test5(){
    JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
    int i = jdbcTemplate.update("insert into user (REAL_NAME, CARD_NO, BALANCE) VALUES (?,?,?)", "李四", "133", "546");
    System.out.println(i);
}
/*
 * 修改
 * */
@Test
public void test6(){
    JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
    int i = jdbcTemplate.update("update user set BALANCE = ? where ID = 3",  "546");
    System.out.println(i);
}


/*
 * 删除
 * */
@Test
public void test7(){
    JdbcTemplate jdbcTemplate = context.getBean("jdbcTemplate", JdbcTemplate.class);
    int i = jdbcTemplate.update("delete from user where ID =?",  3);
    System.out.println(i);
}

/**
 * 具名参数处理NamedParameterJdbcTemplate
 */
@Test
public void test08(){
    NamedParameterJdbcTemplate jdbcTemplate = context.getBean(NamedParameterJdbcTemplate.class);

    Map<String,Object> map=new HashMap<>();
    map.put("id",2);

    // 修改类同
    User user = jdbcTemplate.queryForObject("select * from user where ID = :id", map, new RowMapper<User>() {
        @Override
        public User mapRow(ResultSet resultSet, int i) throws SQLException {

            /*从结果集中获取数据*/
            User user = new User();
            // 如果查询到的数据为0条,返回null
            if(i == 0){
                return null;
            }
            user.setBalance(resultSet.getInt("BALANCE"));
            user.setId(resultSet.getInt("ID"));
            user.setCardno(resultSet.getString("CARD_NO"));
            return user;
        }
    });

    System.out.println(user);
}

dao层使用示例

@Repository
public class UserDaoImpl implements UserDao {
    private JdbcTemplate jdbcTemplate;
    /*
    * JdbcTemplate线程安全的,这以为你可以在多个dao中使用同一个JdbcTemplate的实例,当然也可以在同一个dao中定义为私有属性
    * 官方:https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-JdbcTemplate-idioms
    * 这个是官方推荐用法。一个dao对应一个JdbcTemplate
    * */
    @Autowired
    public UserDaoImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    }

事务四大特性ACID

ACID 四大特性

  • A (Atomicity)原子性:原子性指的是 在一组业务操作下 要么都成功 要么都失败在一组增删改查的业务下 要么都提交 要么都回滚

  • C(Consistency) 一致性:事务前后的数据要保证数据的一致性。由一个一致性状态变为另一个一致性状态。比如张三给李四转1000元,但是张三李四总的金额保持不变。

  • I (Isolation)隔离性:多线程情况下,一个事务的执行不应该被另一个事务影响。

  • D (Durability)持久性:事务提交后对数据的改变时永久性的。

总结:在事务控制方面,主要有两个分类:

编程式事务

在代码中直接加入处理事务的逻辑,可能需要在代码中显式调用开启事务,提交,回滚,例、beginTransaction()、commit()、rollback()等事务管理相关的方法
connetion.autoCommit(false);
­­­­­­
­­­­
­­­
connction.commint()
catch(){
connction.rollback();
}

声明式事务

​ 在方法的外部添加注解或者直接在配置文件中定义,将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。spring的AOP恰好可以完成此功能:事务管理代码的固定模式作为一种横切关注点,通过AOP方法模块化,进而实现声明式事务

spring声明式事务使用

​ 配置事务管理器和开启基于注解的事务控制模式

<!--配置事务管理器,由于事务底层操作都是通过连接来进行事务开启,提交回滚等操作,所以需要配置数据源-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="dataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!--开启基于注解的事务控制模式,基于tx命名空间
xmlns:tx="http://www.springframework.org/schema/tx
如果注解的配置和xml都配置,注解优先

@EnableTransactionManagement 加在配置类上
-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>

​ 在需要的方法上面添加注解 @Transactional

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
    @Override
    public User getUser(){
        return userDao.getUser();
    }
    /*
    * 可以标记在类上面(表示所有方法都加上这个注解),也可以标记在方法上面,如果同时有这个注解,以方法上的为准
    * 建议写在方法上,力度更细
    * 建议下载业务逻辑层。
    * */
    @Override
    @Transactional
    public void trans() {
        userDao.sub();
        System.out.println("张三扣钱完成");
        int i = 1/0;
        userDao.add();

    }
}

@Transactional事务属性配置

	isolation:设置事务的隔离级别
    propagation:事务的传播行为
    noRollbackFor:那些异常事务可以不回滚
    noRollbackForClassName:填写的参数是全类名
    rollbackFor:哪些异常事务需要回滚
    rollbackForClassName:填写的参数是全类名
    readOnly:设置事务是否为只读事务  
    timeout:事务超出指定执行时长后自动终止并回滚,单位是秒
isolation设置事务的隔离级别

​ 用来处理并发事务下的一些问题

    /*
    *Isolation设置事务的隔离级别
    * Isolation.DEFAULT 使用数据库默认的数据库隔离级别--默认
    * Isolation.READ_UNCOMMITTED   读未提交
    * Isolation.READ_COMMITTED    读已提交(不可重复读)
    * Isolation.REPEATABLE_READ 可重复读
    * Isolation.SERIALIZABLE 串行话
    * */
    @Override
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void trans() {
    	//修改,删除等多个操作
    }
事务隔离级别
事务隔离级别脏读不可重复读幻读
读未提交(read-uncommitted)
读已提交(read-committed)
可重复读(repeatable-read)
串行化(serializable)

脏读:一个事务有a和b两个操作,a操作修改了数据库,这个时候另一个事务读取到了a操作修改的数据,然后b操作执行失败,事务回滚。

不可重复读: 事务a读取数据库数据,事务b在此过程中修改了数据库的数据,造成事务a两次读取事务结果不一致。而且不一致后还可以修改。

可重复读:事务a读取数据后,事务b修改事务a读取的数据后,所以事务a再次获取数据还是一样的。

串行话:a和b两个事务不在同时执行,而是强行控制先后执行

查询数据库的默认隔离级别

select @@tx_isolation

程序测试-可重复读–幻读:

参考:

https://blog.csdn.net/sanyuesan0000/article/details/90235335?utm_term=mysql%E5%B9%BB%E8%AF%BB%E7%9A%84%E5%BD%B1%E5%93%8D&utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2allsobaiduweb~default-0-90235335&spm=3001.4430

https://blog.csdn.net/qq_31930499/article/details/110393988

可重复读下出现幻读情况测试

mvcc -->多版本并发控制

在《高性能MySQL》中对MVCC的解释如下

InnoDB的mvcc,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number),每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。下面看一下在REPEATABLE READ隔离级别下, MVCC具体是如何操作的。

SELECT

InnoDB会根据以下两个条件检查每行记录:

a. InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。

b.行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。只有符合上述两个条件的记录,才能返回作为查询结果。

INSERT

InnoDB为新插入的每一行保存当前系统版本号作为行版本号。

DELETE

InnoDB为删除的每一行保存当前系统版本号作为行删除标识。

UPDATE

InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系纺版本号到原来的行作为行删除标识。

一下是测试过程

事务a事务b
T1select * from user
T2update user set balance=balance-200
T3update user set balance=balance-200insert into user (REAL_NAME, CARD_NO, BALANCE) VALUES (?,?,?)", “李四”, “133”, “546”
T4commit
T5select * from user
T6commit
T7

image-20210223173449370

测试结果理解:事务a查询后执行更新操作,由于和事务b更新的是相同的数据,且事务b先执行,所以此时事务a因为锁的问题陷入等待,所以此时事务b提交后才可以修改,此时事务b修改和新增的数据会标记上对应的版本号,此时事务a修改数据,由于修改时当前读的操作,会获取到事务b已经提交的数据,事务b再进行修改后再次更新了所有数据的版本号,包括事务b刚新增的那一条,所以事务a再次查询会多查出来一条。

propagation事务的传播特性

​ 事务的传播特性指的是当一个事务方法被另一个事务方法调用时,这个事务方法
应该如何进行?
希望如果外部存在事务就用外部的, 外部不存在就自己开启事务

a上开启的事务叫当前事务,相对于b和c也叫外部事务

a(){
b();
c();
}
事务传播行为外部不存在事务外部存在事务使用场景
REQUIRED(默认)开启新的事务融合到外部事务中@Transactional(propagation = Propagation.REQUIRED)适用增删改查
SUPPORTS不开启新的事务融合到外部事务中@Transactional(propagation = Propagation.SUPPORTS)适用查询
REQUIRES_NEW开启新的事务挂起外部事务,创建新的事务@Transactional(propagation = Propagation.REQUIRES_NEW)适用内部事务和外部事务不存在业务关联情况,如日志
NOT_SUPPORTED不开启新的事务挂起外部事务@Transactional(propagation =Propagation.NOT_SUPPORTED)不常用
NEVER不开启新的事务抛出异常@Transactional(propagation = Propagation.NEVER )不常用
MANDATORY抛出异常融合到外部事务中@Transactional(propagation = Propagation.MANDATORY)不常用

image-20210223181308560

timeout事务执行超时时间

指定事务等待的最长时间(秒)
当前事务访问数据时,有可能访问的数据被别的数据进行加锁的处理,那么此时事务就必须等待,如果等待时间过长给用户造成的体验感差。

@Transactional(timeout = 2)// 秒作为单位
设置事务只读(readOnly)

readonly:只会设置在查询的业务方法中
connection.setReadOnly(true) 通知数据库,当前数据库操作是只读,数据库就会对当前只读做相应优化
使用场景:
如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默
认支持SQL执行期间的读一致性;
如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场
景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后
条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据
不一致的状态,此时,应该启用事务支持(如:设置不可重复度、幻影读级
别)。

异常属性

设置 当前事务出现的那些异常就进行回滚或者提交。
默认对于RuntimeException 及其子类 采用的是回滚的策略。
默认对于Exception 及其子类 采用的是提交的策略。
1、设置哪些异常不回滚(noRollbackFor)
2、设置哪些异常回滚(rollbackFor )
@Transactional(timeout = 3,rollbackFor = {FileNotFoundException.class})

在实战中事务的使用方式

如果当前业务方法是一组 增、改、删 可以这样设置事务
@Transactional
如果当前业务方法是一组 查询 可以这样设置事务
@Transactionl(readOnly=true)
如果当前业务方法是单个 查询 可以这样设置事务
@Transactionl(propagation=propagation.SUPPORTS ,readOnly=true)

基于xml的使用方式
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
       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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置扫描-->
    <context:component-scan base-package="com.blog"/>
    <!--引入外部配置文件-->
    <context:property-placeholder location="db.properties"/>
    <!--配置连接池对象-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="username" value="${mysql.username}"/>
        <property name="password" value="${mysql.password}"/>
        <property name="url" value="${mysql.url}"/>
        <property name="driverClassName" value="${mysql.driverClassName}"/>
    </bean>
    <!--配置jdbc-->
    <bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--具名参数jdbc处理类-->
    <bean class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate" id="namedParameterJdbcTemplate">
        <constructor-arg type="javax.sql.DataSource" ref="dataSource"/>
    </bean>
    <!--配置事务管理器,由于事务底层操作都是通过连接来进行事务开启,提交回滚等操作,所以需要配置数据源-->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="dataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--开启基于注解的事务控制模式,基于tx命名空间
    xmlns:tx="http://www.springframework.org/schema/tx -->
    <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>

    <!--声明式事务通过aop实现,在方法的不同位置,通过连接对象开启,回滚,提交事务 -->
    <aop:config>
        <!--匹配业务实现层所有类和方法-->
        <aop:pointcut id="transactionCut" expression="execution(* com.blog.service.impl.*.*(..))"/>
        <aop:advisor advice-ref="myAdvice" pointcut-ref="transactionCut"/>
    </aop:config>

    <!--明确切点匹配到的方法那些要声明事务-->
    <tx:advice id="myAdvice" transaction-manager="dataSourceTransactionManager">
        <tx:attributes>
            <!-- 通配符-->
            <tx:method name="update*"/>
            <tx:method name="delete*"/>
            <tx:method name="add*"/>
            <!--配置get开头的方法为只读,且当当前事务不存在时不开启事务,存在时融入当前事务-->
            <tx:method name="get" read-only="true" propagation="SUPPORTS"/>
        </tx:attributes>
    </tx:advice>

</beans>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值