一、了解事务
1.什么是事务?
比如我给陈冠希转账300块,可以分为两步:
第一步:我的钱包-300
第二步:陈冠希的钱包+300
这就是一个事务,捆在一起的一组行为,就是事务
这样才算转账成功,但是如果,第二步失败了,第一步正常进行,那么我的钱包白白损失300块,那就是严重的事故了。
为了防止这种事故的出现,才出现的事务。
事务能保证的是,这个行为的原子性,一致性,隔离性,持久性:
- 两个操作都成功
- 两个操作都失败
要么一起成功,要么一起失败
如果没有事务两个操作逻辑是分开的。
二、Spring中事务的实现
Spring中的事务操作主要分为两类:
- 编程式事务(原生方式去写代码操作事务)
- 声明式事务(利用注解,“约定规则”去自动开启和提交事务)
2.1 事务针对的操作
事务一般针对的是
1.持久化相关的操作,如数据库操作,文件系统操作等。
比如刚刚两个用户转账操作
2.保证数据完整性的操作,比如消息队列
通过使用事务,可以在消息队列中提供可靠的消息传递机制,减少消息丢失或重复处理的可能性,同时确保系统在出现故障的情况下能够正常恢复。
事务的概念适用于需要保证一系列操作的原子性和一致性的任何场景。
2.2 MySQL事务的使用
--- 开启事务 start transaction; --- transaction就是事务的意思 --- 提交事务 commit; --- 回滚事务 rollback;
三个重要的操作:
1.开启事务
2.提交事务
3.回滚事务
2.3 spring 编程式事务(比较麻烦)
与MySQL操作事务类似:
- 开启事务(获取一个事务/创建一个事务并获取)
- 提交事务
- 回滚事务
SpringBoot 内置了两个对象:
DataSourceTransactionManager ⽤来获取事务(开启事务)、提交或 回滚事务的
TransactionDefinition 是事务的属性,
在获取事务的时候需要将 TransactionDefinition 传递进去从而获得⼀个事务TransactionStatus
实现代码如下:
@RestController
public class UserController {
@Resource
private UserService userService;
// JDBC 事务管理器
@Resource
private DataSourceTransactionManager dataSourceTransactionManager;
// ------------定义事务属性------------
@Resource
private TransactionDefinition transactionDefinition;
@RequestMapping("/sava")
public Object save(User user) {
// ------------开启事务------------
TransactionStatus transactionStatus = dataSourceTransactionManager
.getTransaction(transactionDefinition);
// ------------插⼊数据库------------
int result = userService.save(user);
// ------------提交事务------------
dataSourceTransactionManager.commit(transactionStatus);
// // ------------回滚事务------------
// dataSourceTransactionManager.rollback(transactionStatus);
return result;
}
}
以上的代码比较麻烦,众所周知框架是一个帮助我们提高效率的工具,所以下面的Spring声明式事务会简单很多。
2.4 Spring 声明式事务
实现很简单,只需要在需要的类或方法上添加@Transactional 注解就可以实现
无需手动开启/提交/回滚事务:
- 进入方法,自动开启事务
- 方法执行完会,自动提交事务
- 如果中途发生了没有处理的异常,自动回滚事务
具体规则/作用范围是:
- 加在类上,内部的所有非静态public方法 @Transactional 注解
- 加在非静态public方法
- 所在的类,必须被五大类注解修饰,这跟其事务的实现有关,Spring开发需要五大注解
代码实现如下:
@Service
@Transactional
public class Test {
@Autowired
private Mapper mapper;
public int save(User user) {
mapper.save(user);
}
}
Spring 编程式事务 VS Spring声明式事务:
1.编程式事务:更加灵活,可以实现想要的功能,自由度高,但是使用上比较复杂。
2.声明式事务:使用规则约束实现特定功能,方便,使用简单,如果需要实现复杂一点的功能建议还是使用编程式事务。
2.5 @Transactional注解
2.5.1 @Transactional注解原理
无需手动开启/提交/回滚事务
1.进入方法,自动开启事务
2.方法执行完会自动提交事务
3.如果途中发现了没有处理的异常,会自动回滚事务。
实际上就是AOP,对@Transactional注解下的代码,进行统一的处理。
当然对于不同的事务,处理可能不同。
@Transaction 实现思路图:
@Transactionl执行思路图:
默认就是这么一个事务管理器执行这样的逻辑
- 而如果配置了多个事务管理器,则需要通过参数value/transactionManager去指定
2.5.2 为什么必须被五大类注解修饰
@Transactional注解是基于Spring AOP的,而Spring AOP则通过JDK的或者CGLib的动态代理来实现AOP
对于使用@Transactional注解来实现事务管理,确实是通过动态代理来实现的
当你在一个类或方法上添加了@Transactional注解时,Spring会通过动态代理在运行时为该类或方法创建一个代理对象。这个代理对象会拦截调用,并在适当的时机开启、提交或回滚事务
由于动态代理的实现方式,确实需要满足一些条件才能使@Transactional注解生效
具体来说,被注解的类或方法必须是Spring容器中的bean,而Spring容器会自动为标注了@Service、@Controller、@Repository、@Component和@Configuration等注解的类创建bean实例。这也是为什么我之前提到了五大类注解
2.5.3 为什么@Transactional不支持static方法
无论JDK还是CGlib都无法对静态方法提供代理。原因在于静态方法是类级别的,调用需要知道类信息,而类信息在编译器就已经知道了,并 不支持在运行期的动态绑定
三、实践
3.1 项目创建
model.UserInfo:
@Component
@Data
public class UserInfo {
private int id;
private String username;
private String password;
private String photo;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private Integer state;
public UserInfo(String username, String password, Integer state) {
this.username = username;
this.password = password;
this.state = state;
}
public UserInfo() {
}
}
mapper.UserMapper:
@Mapper
public interface UserMapper {
List<UserInfo> getAll(); //获得所有用户信息
UserInfo getUserById(Integer id);
//通过id查找用户
UserInfo getUserByUsername(@Param("username") String username);
//通过username查找用户
List<UserInfo> getAll2(@Param("option") String option);
UserInfo login(@Param("username") String username, @Param("password") String password);
int update(UserInfo userInfo);
//删除状态为state的用户
int delete(@Param("state") Integer state);
//增加用户
int insert(UserInfo userInfo);
List<UserInfo> getAllLikeSome(@Param("likeString") String likeString);
//用户注册提交信息
// int add(UserInfo userInfo);
int add(String username, String password, Integer state, Integer id);
int add2(UserInfo userInfo);
List<UserInfo> select1(UserInfo userInfo);
int update2(UserInfo userInfo);
int deleteByIDs(List<Integer> list);
int insertByUsers(List<UserInfo> list, List<UserInfo> list2);
}
mybatis.UserInfoMapper.xml
<?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.example.demo.mapper.UserMapper">
<resultMap id="BaseMap" type="com.example.demo.model.UserInfo">
</resultMap>
<select id="getAll" resultMap="BaseMap">
select * from userinfo
</select>
<!-- <select id="getAll" resultType="com.example.demo.model.UserInfo">-->
<!-- select id, username as name, password, photo,-->
<!-- createtime, updatetime, state from userinfo-->
<!-- </select>-->
<select id="getUserById" resultType="com.example.demo.model.UserInfo">
select * from userinfo where id = #{id}
</select>
<select id="getUserByUsername" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username = ${username}
</select>
<select id="getAll2" resultType="com.example.demo.model.UserInfo">
select * from userinfo order by id ${option}
</select>
<select id="login" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username = '${username}'
and password = '${password}'
</select>
<update id="update">
update userinfo set state = #{state} where username = #{username}
</update>
<delete id="delete">
delete from userinfo where state = #{state}
</delete>
<insert id="insert" useGeneratedKeys="true"
keyColumn="id" keyProperty="id">
<!-- 自增主键 id 不能为null也没有默认值,如果id不设置或者设置为null,都会导致自增 -->
insert into userinfo (username, password) values (#{username}, #{password});
</insert>
<select id="getAllLikeSome" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username like concat('%', #{likeString}, '%')
</select>
<insert id="add">
insert into userinfo (
<if test="id != 0">
id,
</if>
<if test="username != null">
username,
</if>
<if test="password != null">
password,
</if>
<if test="state != null">
state
</if>
) values (
<if test="id != 0">
#{id},
</if>
<if test="username != null">
#{username},
</if>
<if test="password != null">
#{password},
</if>
<if test="state != null">
#{state}
</if>
)
</insert>
<insert id="add2">
insert into userinfo
<trim prefix="(" suffix=")"
suffixOverrides=",">
<if test="id != 0">
id,
</if>
<if test="username != null">
username,
</if>
<if test="password != null">
password,
</if>
<if test="state != null">
state
</if>
</trim>
values
<trim prefix="(" suffix=")"
suffixOverrides=",">
<if test="id != 0">
#{id},
</if>
<if test="username != null">
#{username},
</if>
<if test="password != null">
#{password},
</if>
<if test="state != null">
#{state}
</if>
</trim>
</insert>
<select id="select1" resultType="com.example.demo.model.UserInfo">
select * from userinfo
<where>
<if test="id != 0">
id = #{id}
</if>
<if test="username != null">
or username = #{username}
</if>
<if test="password != null">
or password = #{password}
</if>
<if test="state != null">
or state = #{state}
</if>
</where>
<!-- <trim prefix="where" prefixOverrides="and">-->
<!-- <trim prefixOverrides="or">-->
<!-- <if test="id != 0">-->
<!-- id = #{id}-->
<!-- </if>-->
<!-- <if test="username != null">-->
<!-- or username = #{username}-->
<!-- </if>-->
<!-- <if test="password != null">-->
<!-- or password = #{password}-->
<!-- </if>-->
<!-- <if test="state != null">-->
<!-- or state = #{state}-->
<!-- </if>-->
<!-- </trim>-->
<!-- </trim>-->
</select>
<update id="update2">
update userinfo
<set>
<if test="username != null">
username = #{username},
</if>
<if test="password != null">
password = #{password},
</if>
<if test="state != null">
state = #{state}
</if>
</set>
where id = #{id}
</update>
<delete id="deleteByIDs">
delete from userinfo where id in
<foreach collection="list" open="(" close=")" item="x" separator=",">
#{x}
</foreach>
</delete>
<insert id="insertByUsers">
insert into userinfo(username, password, state) values
<foreach collection="list" item="x" open="(" close=")" separator="),(">
#{x.username}, #{x.password}, #{x.state}
</foreach>
,
<foreach collection="list2" item="x" open="(" close=")" separator="),(">
#{x.username}, #{x.password}, #{x.state}
</foreach>
</insert>
</mapper>
application.properties:
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test_db?characterEncoding=utf8
# MyBatis 基于jdbc实现~ 底层用的就是jdbc:mysql协议,这个地址是本地数据库的地址,test_db就是我们的那个数据库
spring.datasource.username=root
# 用户名,默认固定是root
spring.datasource.password=root
# 密码,是数据库的密码
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# MyBatis配置信息
mybatis.mapper-locations=classpath:mybatis/*Mapper.xml
# 执行时打印SQL
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#由于其默认情况下的日志类型为Debug,重要程度不高,所以我们需要设置我们对应的目录下的日志级别
logging.level.com.example.demo.controller=debug
#将数据库中的下换线转换成驼峰,比如 user_name -> userName
mybatis-plus.configuration.map-underscore-to-camel-case=true
目录结构:
3.2 方法编写
Controller 接受请求,service 调用方法
TestController:
package com.example.num11.service;
import com.example.num11.mapper.UserMapper;
import com.example.num11.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TestService {
@Autowired
private UserMapper userMapper;
@Transactional
public int add(UserInfo userInfo){
return userMapper.add2(userInfo);
}
}
TestService:
package com.example.num11.service;
import com.example.num11.mapper.UserMapper;
import com.example.num11.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TestService {
@Autowired
private UserMapper userMapper;
@Transactional
public int add(UserInfo userInfo){
return userMapper.add2(userInfo);
}
}
3.3 测试
运行后:
浏览器:
控制台:
当我们去掉 错误代码 :int num=1/0; 的时候,再刷新:
3.4 注意事项
@Transactional 在异常被 try{}catch(){} 捕获的情况下,不会进行事务自动回滚,
因为 try{}catch(){} 后,后面的代码可以继续运行,这个异常是被我们写的
try{}catch(){} 抢走处理了,注解是捕获不到的
3.5 解决事务不会自动回滚的方案
3.5.1 重新抛出异常
3.5.2 手动回滚
TransactionAspectSupport.currentTransactionStatus() 可以得到当前的事务,然后设置回滚方法
setRollbackOnly就可以实现将当前事务的回滚了
- 跟切面有关=>aop