数据库事务处理
面对网站的高并发场景,数据库事务机制能够在一定程度上保证数据的一致性,并且有效提高系统性能,避免系统宕机。
1.JDBC数据库事务
首先在application.properties属性文件中配置数据库信息:
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_chapter6
spring.datasource.username=root
spring.datasource.password=123456
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#最大等待连接数量,设0为没有限制
spring.datasource.tomcat.max-idle=10
#最大连接活动数
spring.datasource.tomcat.max-active=50
#最大等待毫秒数
spring.datasource.tomcat.max-wait=10000
#数据库连接池初始化连接数
spring.datasource.tomcat.initial-size=5
#日志配置
logging.level.root=DEBUG
logging.level.org.springframework=DEBUG
logging.level.org.org.mybatis=DEBUG
在Sping数据库事务中,既可以使用编程式事务,也可以使用声明式事务(广泛使用)。
先来看一段JDBC插入用户的代码测试:
/****imports****/
@Service
public class JvdcServiceImpl implements JdbcService{
@Autowired
DataSource dataSource;
@Override
public int inserUser(String userName, String note){
Connection conn = null;
int result = 0;
try{
//获取连接
conn = dataSource.getConnection();
//开启事务
conn.setAutoCommit(false);
//设置隔离级别
conn.setTranactionIsolation(TransactionIsolationLevel.READ_COMMITTED.getLevel());
//执行SQL
PreparedStatement ps = conn.prepareStatement("insert into t_user(user_name , note) values (?,?)");
ps.setString(1,userName);
ps.setString(2,userName);
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;
}
}
这里有大量的try...catch...finally语句,下图是上面代码执行SQL的流程图:
在图中,有业务逻辑的部分也只是执行SQL那一步骤,其他步骤比较固定,按照AOP的设计思想,就可以把除执行SQL这步之外的步骤单独实现,这就是Sping数据库事务编程的思想。
2.Spring数据库事务编程约定
对于声明式事务,使用@Transcation进行标注,可以标注到类或者方法上,当标注到类时,代表这个类所有public非静态方法都将启用事务功能。在@Transaction中,还允许配置许多属性,如事务隔离级别和传播行为,后续将详细说明。
有了@Transaction配置,Spring事务约定流程如图:
当Sping的上下文开始调用被@Transaction标注的类或者方法时,Sping就会产生AOP功能。当它启动事务时,就会根据事务定义器内的配置去设置事务,首先是根据传播行为去确定事务的策略,然后是隔离级别、超时时间、只读等内容配置(由Sping事务拦截器根据@Transaction配置的内容来完成)。
3.@Transaction配置项
数据库事务属性都可以由@Transaction来配置,先看它的源码:
value和transactionManager属性是配置一个Spring的事务管理器,后面详细讨论;timeout是事务可以允许存在的时间戳,单位为秒;readOnly属性定义的是事务是否是只读事务;真正麻烦的是propagation和isolation两个属性。
4.Spring事务管理器
在Spring中,事务管理器的顶层接口为PlatformTransactionManager,还有其他的一些接口和类,如图:
以MyBatis框架去讨论Spring诗剧苦事务问题,最常用到的事务管理器就是DataSpurceTransactionManager。它也是一个实现PlatformTransactionManager接口的类,PlatformTransactionManager源码如下:
public interface PlatformTransactionManager{
//获取事务,它还会设置数据属性
TransactionStatus getTransaction(TransactionDefinition defination) throws TransactionException;
//提交事务
void commit(TransactionStatus status) throws TransactionException;
//回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
Spring在事务管理中,就是将这些方法按照约定织入对应的流程中,其中getTransaction方法的参数是一个事务定义器(TransactionDefination),它是依赖于我们配置的@Transaction的配置项生成的,通过它设置事务的属性,提交和回滚事务通过commit和rollback方法来执行。
在SpingBoot中,当完成依赖mybatis-spring-boot-starter之后,它会自动创建一个DataSourceTransactionManager对象,最为事务管理器,如果依赖于sping-boot-starer-data-jpa,它会自动创建JpaTransactionManager对象作为事务管理器,所以我们一般不需要自己创建事务管理器而是直接使用。
5.测试数据库事务
首先,先创建一张用户表:
create table t_user(
id int(12) auto_increment,
user_name varchar(60) not null,
note varchar(512),
primary key(id)
);
为了与它映射起来,建立一个User类:
package com.springboot.chapter6.pojo;
import org.apache.ibatis.type.Alias;
/**** imports ****/
@Alias("user")
public class User {
private Long id;
private String userName;
private String note;
/**** setter and getter ****/
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
这里使用@Alias注解定义了别名,这样可以让MyBatis扫描到其上下文中,然后再给出一个MyBatis接口:
package com.springboot.chapter6.dao;
import org.springframework.stereotype.Repository;
import com.springboot.chapter6.pojo.User;
@Repository
public interface UserDao {
User getUser(Long id);
int insertUser(User user);
}
接着是与这个MyBatis接口文件对应的一个映射文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.springboot.chapter6.dao.UserDao">
<select id="getUser" parameterType="long" resultType="user">
select id, user_name as userName, note from t_user where id = #{id}
</select>
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user(user_name, note) value(#{userName}, #{note})
</insert>
</mapper>
这里<select>元素定义的resultType为user,是一个别名,指向User类。对于<insert>元素定义的属性userGeneratedKeys和keyProperty,表示在插入之后使用数据库生成机制回填对象的主键。
接着需要创建一个UserService和它的实现类UserServiceImpl,然后通过@Transactional启用Spring数据库事务机制,代码如下:
package com.springboot.chapter6.service;
import com.springboot.chapter6.pojo.User;
public interface UserService {
// 获取用户信息
public User getUser(Long id);
// 新增用户
public int insertUser(User user);
}
/**** imports ****/
@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 getUser(Long id) {
return userDao.getUser(id);
}
}
上面两个方法标注了注解@Transactional,意味着这两个方法将启用Spring数据库事务机制,在事务配置中,采用了读取提交的隔离级别,这里代码还会限制超时时间为1s,然后写一个控制器,用来测试事务的启用情况:
/**** imports ****/
@Controller
@RequestMapping("/user")
public class UserController {
// 注入Service
@Autowired
private UserService userService = null;
// 测试获取用户
@RequestMapping("/getUser")
@ResponseBody
public User getUser(Long id) {
return userService.getUser(id);
}
// 测试插入用户
@RequestMapping("/insertUser")
@ResponseBody
public Map<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 HashMap<>();
result.put("success", update == 1);
result.put("user", user);
return result;
}
}
有了这个控制器,我们还需要给SpringBoot配置MyBatis框架的内容,需要在配置文件appplication.properties文件中添加:
mybatis.mapper-locations=classpath:com/springboot/chapter6/mapper/*.xml
mybatis.type-aliases-package=com.springboot.chapter6.pojo
这样MyBatis框架就配置完成了,最后是SpringBoot启动文件:
/**** imports ****/
@MapperScan(
basePackages = "com.springboot.chapter6",
annotationClass = Repository.class)
@SpringBootApplication(scanBasePackages = "com.springboot.chapter6")
public class Chapter6Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Chapter6Application.class, args);
}
// 注入事务管理器,它由Spring Boot自动生成
@Autowired
PlatformTransactionManager transactionManager = null;
// 使用后初始化方法,观察自动生成的事务管理器
@PostConstruct
public void viewTransactionManager() {
// 启动前加入断点观测
System.out.println(transactionManager.getClass().getName());
}
}
在viewTransactionManager方法中,加入了注解@PostConstruct,所以在这个类对象被初始化后,会调用这个方法,在这个方法中,因为先前已经将IoC容器注入进来,所以可以通过IoC容器获取对应的Bean以监控它们。
6.隔离级别
数据库事务有4个基本特征:ACID
(1)原子性:事务中包含的操作被看作一个整体的业务,这个业务要么成功要么失败。
(2)一致性:事务在完成时,必须使所有数据都保持一致状态,保证了数据的完整性。
(3)隔离性:可能多个应用程序线程同时访问同一数据,这样数据库同样的数据在不同的各个事务中被访问,可能会产生丢失更新。
(4)持久性:事务结束后,所有的数据都会固化到一个地方,如保存到磁盘中,使断电重启后也可以提供给应用程序访问。
一般,存在两种类型的丢失更新,对于一个事务回滚另外一个事务提交而引发的数据不一致的情况称为第一类丢失更新。如:
目前大部分数据库已经克服了第一类丢失更新的问题。如果多个事务并发提交,就会导致第二类丢失更新的情况:
为了克服这类问题,数据库提出了事务之间的隔离级别。共有4类隔离级别:
(1)未提交读(最低隔离级别)
允许一个事务读取另外一个事务没有提交的数据,可能发生脏读场景。
(2)读写提交
指一个事务只能读取另外一个事务已经提交的数据,不能读取未提交的数据。
但是读写提交也会产生下面问题(不可重复读):
为了克服这个不足,数据库的隔离级别还提出了可重复度的隔离级别,能够消除不可重复度问题。
(3)可重复读
目标是克服读写提交中出现的不可重复读现象:
(4)串行化
数据库最高的隔离级别,它会要求所有的SQL都会按照顺序执行,这样可以克服上述隔离级别中出现的所有问题,能够完全保证数据的完整性。但追求更高的隔离性,也要付出锁的代价。有了锁,意味着性能的丢失,而且隔离级别越高,性能越是直线下降。。
配置默认的隔离级别(在application.properties属性文件中):
#隔离级别数字配置的含义
#-1 数据库默认隔离级别
#1 未提交读
#2 读写提交
#4 可重复读
#8 串行化
#tomca数据默认隔离级别
spring.datasource.tomcat.default-transaction-isolation=2
7.传播行为
传播行为是指方法之间调用事务采取的策略问题,具体内容参考 https://blog.csdn.net/soonfly/article/details/70305683
本节代码已上传Github: https://github.com/lizeyang18/SpringBoot-2.x/tree/master/chapter6
学习永不止步,一起加油~