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 | |
---|---|---|
T1 | select * from user | |
T2 | update user set balance=balance-200 | |
T3 | update user set balance=balance-200 | insert into user (REAL_NAME, CARD_NO, BALANCE) VALUES (?,?,?)", “李四”, “133”, “546” |
T4 | commit | |
T5 | select * from user | |
T6 | commit | |
T7 | ||
测试结果理解:事务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)不常用 |
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>