10 KiB
上小节中,系统自动注册用户逻辑,是需要保证其原子性的,要么所有操作全部失败,要么全部成功,绝对不允许一部分成功,一部分失败,这样会导致脏数据。我们通过在方法头上添加 @Transacational 事务注解,以实现事务的控制。
但是,这块其实是有坑的,不知道小伙伴们发现了没有。
模拟注册用户中间发生了错误
我们来模拟一下,在新增用户入库后,分配用户角色之前,手动添加一个运行时异常 —— 分母不能为零。如下图所示:
运行时异常添加完毕后,重启项目,通过 Apipost 测试一下登录接口,记得登录一个数据库中不存在的手机号,看看是个什么情况。
可以看到,提示系统错误,查看数据库会发现用户数据插入成功了,但是角色数据、Redis 缓存都添加失败了,事务并没有回滚 !
声明式注解事务,为啥失效了?
声明式注解事务失效原因,主要由以下几点:
-
方法可见性:
@Transactional仅在public方法上生效。 -
自调用:当类中的方法调用同一个类中的另一个
@Transactional方法时,事务可能不会生效。这是因为事务注解是通过 AOP 实现的,而 Spring 的 AOP 代理机制在这种情况下不会被触发。 -
异常处理:只有
RuntimeException和Error类型的异常会触发事务回滚。如果你抛出的是checked exception,事务不会回滚,除非你明确指定rollbackFor属性。 -
代理对象:确保你是在 Spring 管理的代理对象上调用方法。如果你直接使用
new关键字实例化对象,Spring 的 AOP 代理机制将不会被应用。
很显然,我们是自调用这种情况。
使用编程式事务
什么是编程式事务?有哪些优点?
编程式事务(Programmatic Transaction)是一种通过代码显式地管理事务的方式,而不是依赖声明式事务(Declarative Transaction)中使用的注解或 XML 配置。在编程式事务中,开发人员通过编写代码来开启、提交和回滚事务,以精细控制事务的边界和行为。
使用编程式事务优点如下:
-
精细控制:编程式事务允许开发者通过代码精细地控制事务的生命周期,包括开始、提交和回滚。可以根据具体业务需求,灵活地管理事务。
-
动态处理:在运行时可以根据业务逻辑的不同情况动态决定事务的行为。特别适合需要在代码执行过程中,根据某些条件来开启、提交或回滚事务的场景。
-
适用于复杂事务:在一个方法中需要多次开启和关闭事务,或需要嵌套事务的复杂场景中,编程式事务可以提供更大的灵活性和控制力。
-
灵活性高:能够在代码中实现复杂的事务逻辑,可以精确控制事务的边界和行为。这在需要多个步骤或调用之间共享事务上下文时非常有用。
-
性能提升:通过精细控制事务的边界,减少不必要的事务开启和提交,从而减少事务开销;通过明确控制事务的开始和结束,可以确保事务范围尽可能小,减少长时间占用数据库资源,提高系统的并发性;通过灵活的事务管理,可以在必要时才进行事务回滚,减少回滚操作带来的性能开销。
使用示例
以下是 Spring Boot 中使用编程式事务两种方式,示例代码如下:
第一种方式
@Service
public class MyService {
@Resource
private PlatformTransactionManager transactionManager;
@Resource
private MyMapper myMapper;
public void myTransactionalMethod() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 业务逻辑代码
myMapper.insertSomething(...);
transactionManager.commit(status); // 提交事务
} catch (Exception ex) {
transactionManager.rollback(status); // 回滚事务
throw ex; // 重新抛出异常
}
}
}
着重解释一下下面这行代码:
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
- 这行代码通过
transactionManager.getTransaction方法获取一个新的事务状态。DefaultTransactionDefinition用于定义事务的默认属性,例如传播行为和隔离级别。该方法会返回一个TransactionStatus对象,用于管理事务的状态。
第二种方式
TransactionTemplate是一个简化了事务管理的工具类,可以避免直接处理 TransactionStatus。
@Service
public class MyService {
@Resource
private TransactionTemplate transactionTemplate;
@Resource
private MyMapper myMapper;
public void myTransactionalMethod() {
transactionTemplate.execute(status -> {
try {
// 业务逻辑代码
myMapper.insertSomething(...);
} catch (Exception ex) {
status.setRollbackOnly(); // 标记事务为回滚
throw ex; // 重新抛出异常
}
return null;
});
}
}
为项目整上编程式事务
了解如何使用后,我们为用户注册方法添加上编程式事务,代码如下:
package com.quanxiaoha.xiaohashu.auth.service.impl;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.quanxiaoha.framework.common.enums.DeletedEnum;
import com.quanxiaoha.framework.common.enums.StatusEnum;
import com.quanxiaoha.framework.common.exception.BizException;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.framework.common.util.JsonUtils;
import com.quanxiaoha.xiaohashu.auth.constant.RedisKeyConstants;
import com.quanxiaoha.xiaohashu.auth.constant.RoleConstants;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.PermissionDO;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.RoleDO;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.UserDO;
import com.quanxiaoha.xiaohashu.auth.domain.dataobject.UserRoleDO;
import com.quanxiaoha.xiaohashu.auth.domain.mapper.UserDOMapper;
import com.quanxiaoha.xiaohashu.auth.domain.mapper.UserRoleDOMapper;
import com.quanxiaoha.xiaohashu.auth.enums.LoginTypeEnum;
import com.quanxiaoha.xiaohashu.auth.enums.ResponseCodeEnum;
import com.quanxiaoha.xiaohashu.auth.model.vo.user.UserLoginReqVO;
import com.quanxiaoha.xiaohashu.auth.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
@Service
@Slf4j
public class UserServiceImpl implements UserService {
// 省略...
@Resource
private TransactionTemplate transactionTemplate;
// 省略...
/**
* 系统自动注册用户
* @param phone
* @return
*/
private Long registerUser(String phone) {
return transactionTemplate.execute(status -> {
try {
// 获取全局自增的小哈书 ID
Long xiaohashuId = redisTemplate.opsForValue().increment(RedisKeyConstants.XIAOHASHU_ID_GENERATOR_KEY);
UserDO userDO = UserDO.builder()
.phone(phone)
.xiaohashuId(String.valueOf(xiaohashuId)) // 自动生成小红书号 ID
.nickname("小红薯" + xiaohashuId) // 自动生成昵称, 如:小红薯10000
.status(StatusEnum.ENABLE.getValue()) // 状态为启用
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
.isDeleted(DeletedEnum.NO.getValue()) // 逻辑删除
.build();
// 添加入库
userDOMapper.insert(userDO);
int i = 1 / 0;
// 获取刚刚添加入库的用户 ID
Long userId = userDO.getId();
// 给该用户分配一个默认角色
UserRoleDO userRoleDO = UserRoleDO.builder()
.userId(userId)
.roleId(RoleConstants.COMMON_USER_ROLE_ID)
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
.isDeleted(DeletedEnum.NO.getValue())
.build();
userRoleDOMapper.insert(userRoleDO);
// 将该用户的角色 ID 存入 Redis 中
List<Long> roles = Lists.newArrayList();
roles.add(RoleConstants.COMMON_USER_ROLE_ID);
String userRolesKey = RedisKeyConstants.buildUserRoleKey(phone);
redisTemplate.opsForValue().set(userRolesKey, JsonUtils.toJsonString(roles));
return userId;
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务为回滚
log.error("==> 系统注册用户异常: ", e);
return null;
}
});
}
}
自测一波
编写完毕后,重启项目,再次测试登录接口,看看这次分母不能为零运行时错误发生时,事务控制是否生效。不出意外,这次就没问题了,我就不截图了,小伙伴们可以自己测试一波。测试完毕后,记得将 int i = 1 / 0; 这行代码删除掉哟~