Springboot中的数据库事务
对于一些业务网站而言 , 产 品库存的扣减、 交易记录以及账户都必须是要么 同时成功, 要么 同时失败 ,这便是一种事务机制,而在一些特殊的场景下 ,如一个批处理 ,它将处理多个交易 ,但是在一些交易中发生了异常 , 这个时候则不能将所有的交易都回滚。如果所有的交易都回渎,那么那些本能够正常处理的业务也无端地被回滚。 通过 Spring 的数据库事务传播行为,可以很方便地处理这样的场景 。
首先配置数据库信息
spring.datasource.url=jdbc:mysql://localhost:3306/demo
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver=com.mysql.jdbc.Driver
spring.datasource.tomcat.max-idle=10
spring.datasource.tomcat.max-active=50
spring.datasource.tomcat.max-wait=10000
spring.datasource.tomcat.initial-size=5
一、JDBC数据库事务
package com.demo.servicee.impl
@Service
public class JdbcServiceImpl implements JdbcService{
@Autowired
private DataSource dataSource=null;
@Override
public int insertUser(String name,String note){
Connection conn=null;
int result=0;
try{
//获取连接
conn=dataSource.getConnection();
//开启事务
conn.setAutoCommit(false);
//设置隔离级别
conn.setTransactionIsolation(TransactionIsolationLevel.RRAD_COMMITED.getLevel());
//执行SQL
PreparedStatement ps=conn.prepareStatement("insert into t_user(user_name,note)values(?,?)");
ps.setString(1,userName);
ps,setString(2,note);
result=ps.executeUpdate();
//提交事务
conn.commit();
}catch(Exception e){
//回滚事务
if(conn !=null){
try{
conn.rollback();
}catch(SqlException e1)
e1.printStackTrace();
}
}
e.printStackTrace();
}finally{
try{
if(conn !=null && !conn.isClosed()){
conn.close()
}
}catch(SQLException e){
e.printStackTrace();
}
}
return result;
}
}
使用JDBC需要使用大量的try...catch...finally...语句,和关于连接的获取关闭,事务的提交和回滚。使用Hibernate、myBatis可以减少try...catch...finally的使用,但是依旧不能减少开闭数据库连接和事务控制的代码。而AOP可以解决这样的问题。
二、Spring 声明式事务的使用
Spring AOP 会把我们的代码织入到约定的流程中,同样,同样执行的SQL的代码也可以织入的哦Spring 约定的数据库事务的流程中。首先要掌握这个约定
1.Spring 声明式数据库事务约定
对于事务需要通过标注告诉Spring在什么地方启用数据库事务功能,对于声明式数据库,是使用@Transactional进行标注的。
@Transactional 这个注解可以标注类和方法上,当它标注在类上时,代表这个类所有公共(public)非静态的方法东将启用事务功能。在@Transactonal 中还可以进行事务的隔离级别和传播行为,异常类型的配置。这些配置,是在Sprng IoC容器在加载时就会将这些配置信息解析出来,然后帮这些喜喜存储到事务定义器(TransactonDefinition接口实现的类)里,并且记录了那些类或者方法需要启动事务的功能,采取什么策略去执行事务。在这个过程中我们所需要做的就是给需要事务的类和方法标注@Transactional并配置属性
spring的事务处理机制
Spring 通过对注解@Transactional 属性配置去设置数据库事务 , 跟着 Spring 就会
开始调用开发者编写 的业务代码 。 执行开发者 的业务代码,可能发生异常,也可能不发生异常 。 在Spring 数据库事务 的流程 中,它会根据是否发生异常采取不同的策略 如果都没有发生异常, Spring 数据库拦截器就会帮助我们提交事务 , 这点也并不需要我们干预 。如果发生异常,就要判断一次事务定义器内的配置,如果事务定义器己经约定了该类型的异常不回段事务就提交事务 , 如果没有任何配置或者不是配置不回滚事务的异常,则会回滚事务,并且将异常抛出 , 这步也是由事务拦截器完成的。论发生异常与否, Spring 都会释放事务资源,这样就可以保证数据库连接池正常可用了,这
也是由 Spring 事务拦截器完成的内容 。
public class UserServiceImpl implements UserService{
@Autowired
private UserDao userDao=null;
@Override
@Transactional
public int insertUser(User user){
return userDao.insertUser(user);
}
}
2.@Transactional配置项
spring中关于数据库属性是由@Transactional 来配置的,源码如下所示
package org.springframework.transaction.annotation;
/**** imports****/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetenttionPolicy.RUNTIME)
@InHerited
@Documented
public @interface Transactional{
//通过bean name指定事务管理器
@ALiasFor("transactionManager")
String value() default"";
//同value属性
@ALiasFor(value)
String transactionManager() default"";
//指定传播行为
Propagation propagation() default Propagetion.REQUIRED;
//指定隔离级别
Isolation isolation() default IsoLation.DEFAULT
//指定超时时间(单位为秒)
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
//时候只读事务
boolean readOnly() default false;
//方法发生指定异常时回滚,默认是所有的异常都回滚
Class<? extends Throwable>[] rollbackFor() default();
//方法发生指定异常名称时回滚,默认是所有异常都回滚
String[] rollbackForClassName() default();
//方法发生指定异常时不回滚,默认是所有的 异常都回滚
Class<? extends Throwable>[] noRollBackFor() default();
//方法在发生指定异常名称时不回滚,默认所有异常都回滚
String noRollbackForClassName() default();
}
- value和transactionManager属性是配置一个Spring的事务管理器
- timeout是事务允许存在的时间戳
- read Only 属性 定义的是事务是否是只读事务;
- rollbackFor 、 rollbackForClassName 、 noRollbackFor 和 noRollbackForClassName 都是指定异常 ,我们从流程中可以看到在带有事务的方法时,可能发生异常,通过这些属性的设置可以指定在什么异常的情况下依旧提交事务,在什么异常的情况下回滚事务 , 这些可以根据自己的需要进行指定
- propagation 传播行为(重点)
- isolation隔离级别(重点)
@Transactional可以放在接口上也可以放在实现类上,spring推荐放在实现类上。
3.Spring 事务管理器
在spring的事务流程中事务的打开回滚和提交是由事务管理器来完成的。事务管理器的顶层接口是PlatformTransactionManager.
当我们使用myBatis框架时最常用的的是DataSourceTransactionManager它实现了PlatformTransactionManager接口,关于PlatformTransactionManager的源码如下
package org.springframework transaction;
public interface PlatformTransactionManager{
//获取事务,它还会设置数据属性
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
//提交事务
void commit(TransactionStatus status) throws TransactionException;
//回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
Spring在事务管理时,就是将这些方法按照约定织入对应的流程,其中getTransaction方法的参数是一个事务定义器,它依赖于我们配置的@Transactional的配置项生成的。于是通过它就能够设置事务的属性了
在 Spring Boot 中,当你依赖于 mybatis-spring-boot-starter 之后 , 它会自动创建一个 DataSourceTransactionManager 对象 ,作为事务管理器 ,如果依赖于 spring-boot-starter-data-j pa ,则它会自动创建JpaTransactionManager 对象作为事务管理器 ,所以我们一般不需要自己创建事务管理器而直接使用它们即可。
4.事务的使用
创建一张表
create table t_user(
id int(12) auto_increment,
user_name varchar(60) not null,
note varchar(512),
primary key(id)
);
创建POJO
package com.demo.pojo
@Alias("user")
public class User{
private Long id;
private String userName;
private String note;
/**** setter and getter****/
}
myBatis接口
package com.demo.dao
@Repository
public interface UserDao{
User getUser(Long id);
int intsertUser(User user);
}
mapper映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "- //mybatis . org//DTD Mapper 3 . 0//EN"
" http : //mybatis . org/dtd/mybat 工 s - 3 - mapper . dtd ">
<mapper namespace="com.demo.dao.UserDao">
<select id="getUser" parameterType="long" resultType="user">
select id,user_name user,note from t_user where id= #{id}
</select>
<insert id="insertUser" useGeneratedKays="true" resulType="user">
insert into t_user(user_name,note)value(#{userName},#{note})
</insert>
</mapper>
服务接口
package com.demo.service
public interface UserService{
//获取用户信息
public User getUser(Long id);
//新增用户
public int inserUser(User user);
}
服务接口实现类
package com.demo.service.demo
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserDao userDao=null;
@Override
@Transactional(isolation=Isolation.READ_COMMITTED,timeout=1)
public int insertUser(User user){
return userDao.insertUser(user);
}
@Override
@Transactional(isolation=Isolation.READ_COMMITTED,timeout=1)
public User insertUser(id){
return userDao.getUser(id);
}
}
代码中的方法上标注了注解@Transactional , 意味着这两个方法将启用 Spring 数据库事务机制。在事务配置中,采用了读写提交的隔离级别
编写Controller
package com.springboot.chapter6.controller;
@Controller
@RequestMapping("/user")
public class UserController{
@AutoWired
private UserSevice userService=null;
@RequestMapping("/getUser")
@ResposeBody
public user getUser(Long id){
return userService.getUser(id);
}
@RequestMapping("/insertUser")
@ResponseBody
public Mapping<String,Object> insertUser(String userName,String note){
User user=new User();
user.setUserName(userName);
user.setNote(note);
int update = userService.insertUser(user);
Map<String,Object> result =new HaspMap();
result.put("success",update==1);
result.put("user",user);
return result;
}
}
在application.properties中添加配置
mybatis.mapper-locations=classpath:com/demo.mapper/*xml
mybatis.type-aliases-package=com.demo.pojo
依赖于 mybatis叩ring-boot”starter 之后, Spring Boot 会自动创建事务管理器、 MyBatis 的 SqlSessionFactory 和 SqlSessionTemplate 等 内容 。 下面我们需要配置 SpringBoot 的 运行文件 ,
package com.demo.main
@MapperScan(
basePackages="com.demo",
annotationClass=Repository.class
)
@SpringBootApplication(scanBasePackage="com.demo")
public class DemoApplication{
public static void main(String[] args)throws Exception{
SpringApplication.run(DemoApplication.class,args);
}
}
先这里使用 了 @MapperScan 扫描对应的包 , 并限定了只有被注解@Repository 标注的接口 ,这样就可以把 MyBati s 对应的接口文件扫描到 Spring IoC 容器 中 了
三、隔离级别
Spring事务机制中最重要的两个配置项,隔离级别和传播行为。
对于隔离级别,因为互联网应用时刻面对着高并发的环境 ,时刻都是多个线程共
享的数据。对于数据库而言 , 就会出现多个事务同时访问同一记录的情况 , 这样引起数据 出 现不一致的情况,便是数据库的丢失更新( Lost Update ) 问题。应该说 , 隔离级别是数据库的概念 。
1.数据库事务相关知识
数据库的的4个基本特征,ACID
- Atomic(原子性):事务中包含的操作被看作一个整体的业务单元,这个业务单元中的操作要么全部成功、要么全部失败,不会出现部分失败,部分成功的场景。
- Consistency(一致性):在事务完成时,必须使所有的数据保持一致状态
- Isolation(隔离性):由于多个引用程序线程访问同一数据,这样数据库的数据就会在各个不同的事务中被访问。这样会产生丢失更新。为了压制丢失更新的产生,数据库定义了隔离级别的概念,通过它的选择,可以在不同程度上压制丢失更新的发生。
- Durability(持久性):事务结束后,所有的数据会固化到一个地方,如保存到磁盘中。
下面深入讨论隔离性
在多个事务同时操作数据库的情况下会引发丢失更新的场景,例如,电商有一种商品电商有一种商品,在疯狂抢购中,会出现多个事务同时访问商品库存的场景 ,这样就会产生丢失更新。一般而言 ,存在两种类型的丢失更新 ,让我们 了解下它们 。 下面假设一种商品的库存数量还有 100 , 每次抢购都只能抢购 l 件商品,那么在抢购中就可能出现如下的场景。
时刻 | 事务1 | 事务2 |
---|---|---|
T1 | 初始库存100 | 初始库存100 |
T2 | 库存-1,余99 | ...... |
T3 | ...... | 库存-1余99 |
T4 | 提交事务,库存变为99 | |
T5 | 回滚事务,库存100 |
对于这样一个事务回滚一个事务提交引发的数据不一致的情况,我们称为第一类丢失更新。现今大部分的数据库都已经克服了第一类丢失更新的问题
时刻 | 事务1 | 事务2 |
---|---|---|
T1 | 初始库存100 | 初始库存100 |
T2 | 库存-1,余99 | ...... |
T3 | ...... | 库存-1余99 |
T4 | ...... | 提交事务,库存变为99 |
T5 | 提交事务,库存变为99 |
对于这样多个事务都提交引发的丢失更新称为第二类丢失更新
2.隔离级别详解
我们 主要针对第二种丢失更新,为了压制丢失更新,数据库标准提出了4类不同的隔离级别,分别为未提交读(read uncommitted)、读写提交(read commited)、可重复读和串行化。提出4种不同的隔离级别是出于性能的考虑
未提交读(read uncommitted)会产生脏读
未提交读是最低的隔离级别,其含义是允许一个事务读取另外一个事务没有提交的数据。未提交读是一种危险的隔离级别,所以一般在我们实际的开发中应用不广 , 但是它的优点在于并发能力高,适合那些对数据一致性没有要求而追求高并发的场景 ,它的最大坏处是出现脏读 。
读写提交(read committed)会产生不可重复读
读写提交隔离级别,是指一个事务只能读取另一个事务已经提交的数据,不能读取未提交的数据。
可重复读 会产生幻读
可重复读的目标是克服读写提交中出现的不可重复读的现象,因为在读写提交的时候,可能出现一些值的变化, 影响当前事务的执行,如上述的库存是个变化的值,这个时候数据库提出 了可重复读的隔离级别
串行化(Serializable)
串行化(Serializable)是数据库最高的隔离级别,它会要求所有的 SQL 都会按照顺序执行,这样就可以克服上述隔离级别出现的各种问题,所以它能够完全保证数据的一致性 。
使用合理的隔离级别
关于隔离级别和可能发生的现象
项目类型 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读 | o | o | o |
读写提交 | x | o | o |
可重复度 | x | x | o |
串行化 | x | x | x |
追求更高的隔离级别,它能更好地保证了数据的一致性,但是也要付出锁的代价 。 有了锁,就意味着性能的丢失,而且隔离级别越高,性能就越是直线地下降 所以在现实中一般而言,选择隔离级别会以读写提交为主,它能够防止脏读,而不能避免不可重复读和幻读。为了克服数据不一致和性能问题,程序开发者还设计了乐观锁,甚至不再使用数据库而使用其他的手段,例如,使用 Redis 作为数据载体
关于隔离级别的使用
@Transactional(isolation=Isolationn.SERIALIZABLE)
public int insertUser(User user){
return userDao.insertUser(user);
}
也可以在application.properties中指定默认的隔离级别
#隔离级别数字配置的含义:
#-1 数据库默认隔离级别
#1 未提交读
#2 读写提交
#4 可重复读
#8 串行化
#tomcat数据源默认隔离级别
#spring.datasource.tomcat.default-transaction-isolation=2
#dbcp2数据库连接池默认隔离级别
#spring.datasource.dbcp2.default-transaction-isolation=2
四、传播行为
传播行为是方法之间调用事务采取的策略问题 。 在绝大部分的情况下,我们会认为数据库事务要么全部成功 , 要么全部失败。但现实中也许会有特殊的情况。例如,执行一个批量程序,它会处理很多 的交易,绝大部分交易是可以顺利完成的,但是也有极少数的交易因为特殊原因不能完成而发生异常,这时我们不应该因为极少数的交易不能完成而回滚批量任务调用的其他交易,使得那些本能完成的交易也变为不能完成了 。 此时,我们真实的需求是,在一个批量任务执行的过程中,调用多个交易时,如果有一些交易发生异常 ,只是回滚那些出现异常的交易,而不是整个批量任务,这样就能够使得那些没有 问题的交易可以顺利完成,而有问题的交易则不做任何事情 。
在 Spring 中, 当一个方法调用另外一个方法时,可以让事务采取不同的策略工作,如新建事务或者挂起当前事务等,这便是事务的传播行为。
批量任务我们称之为当前方法,那么批量事务就称为当前事务,当它调用单个交易时,称单个交易为子方法,当前方法调用子方法的时候,让每一个子方法不在当前事务 中执行,而是创建一个新的事务去执行子方法,我们就说当前方法调用子方法的传播行为为新建事务。此外 , 还可能让子方法在无事务、独立事务中执行,这些完全取决于你的业务需求。
1.传播行为的定义
在Spring事务机制中对数据库存在7种传播行为。它是通过枚举类Propagation定义的,其源码为
pacakage org.springframework.transaction.annotation;
/****imports****/
public enum Propagation{
//需要事务,它是默认传播行为,如果当前存在事务,就沿用当前事务。否则新建一个事务运行子方法
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
//支持事务,如果当前存在事务,就沿用当前事务,如果不存在,则无事务的方式运行子方法
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
//须使用事务,如果当前没有事务,则会抛出异常,如果存在当前事务 , 就沿用当前事务
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
//无论当前事务是否存在,都会创建新事务运行方法,这样新事务就可以拥有新的锁和隔离级别等特性,与当前事务相互独立
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
//不支持事务,当前存在事务时,将挂起事务,运行方法
NOT_SUPPORTS(TransactionDefinition.PROPAGATION_NOT_SUPPORTS),
//不支持事务,如果当前方法存在事务,则抛出异常,否则继续使用无事务机制运行
NEVER(TransactionDefinition.PROPAGATION_NEVER),
//在当前方法调用子方法时,如果子方法发生异常,只回滚子方法执行过的SQL,而不回滚当前方法的事务
NESTED(TransactionDefinition.PROPAGATION_NESTED);
private final int value;
Propagetion(int value){this Value=value;}
public int value(){
return this.value;
}
}