likes
comments
collection
share

事务的使用与事务失败的场景分析

作者站长头像
站长
· 阅读数 21

事务(Transaction)是一个数据库管理系统中的逻辑工作单元,由一组操作组成,这些操作要么全部成功,要么全部失败。事务的主要目的是保证数据库的完整性和一致性,即使在系统故障或其他异常情况下也能如此。

事务的特性 (ACID)

事务有四个关键特性,统称为 ACID 特性:

  1. 原子性 (Atomicity) :事务是一个不可分割的工作单元,事务中的所有操作要么全部成功,要么全部失败。即使在出现系统故障的情况下,事务也会回滚到事务开始前的状态。
  2. 一致性 (Consistency) :事务执行前和执行后,数据库都必须处于一致状态。事务的执行不能违反数据库的约束条件和业务规则。
  3. 隔离性 (Isolation) :多个事务并发执行时,一个事务的执行不能被其他事务干扰,各个事务之间是相互隔离的。不同的隔离级别定义了事务之间的相互可见性。
  4. 持久性 (Durability) :事务一旦提交,其对数据库的改变是永久性的,即使系统崩溃也不会丢失。数据库系统会将事务的结果写入持久性存储。

事务的使用示例

在实际应用中,事务通常用来确保一组相关的数据库操作要么全部成功,要么全部回滚。例如,银行转账操作需要将款项从一个账户扣除并存入另一个账户,这两个操作必须在一个事务中执行,以确保数据一致性。

以下是一个简单的 Spring 框架中的事务管理示例:

1. 创建一个用户实体 (User)

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class User {
    @Id
    private Long id;
    private String name;
    private double balance;

    // Getters and setters
}

2. 创建一个用户仓库 (UserRepository)

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

3. 创建一个用户服务 (UserService) 来管理事务

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional(rollbackFor = Exception.class)
    public void transferBalance(Long fromUserId, Long toUserId, double amount) {
        User fromUser = userRepository.findById(fromUserId).orElseThrow(() -> new RuntimeException("User not found"));
        User toUser = userRepository.findById(toUserId).orElseThrow(() -> new RuntimeException("User not found"));

        if (fromUser.getBalance() < amount) {
            throw new RuntimeException("Insufficient balance");
        }

        fromUser.setBalance(fromUser.getBalance() - amount);
        toUser.setBalance(toUser.getBalance() + amount);

        userRepository.save(fromUser);
        userRepository.save(toUser);
    }
}

4. 创建一个控制器 (UserController) 来触发服务


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/transfer")
    public String transferBalance(@RequestParam Long fromUserId, @RequestParam Long toUserId, @RequestParam double amount) {
        try {
            userService.transferBalance(fromUserId, toUserId, amount);
            return "Transfer successful";
        } catch (Exception e) {
            return "Transfer failed: " + e.getMessage();
        }
    }
}

使用 @Transactional 注解来管理事务是 Spring 框架中非常常见的做法。事务没有起作用可能有多种原因。下面是一些常见的排查步骤:

事务执行失败的场景分析

1. 确保事务注解在正确的层次

  • 通常,@Transactional 注解应用于服务层(Service 层)的类或方法,而不是在 DAO 层。确保注解加在正确的类或方法上。

详细说明

  • Service 层:负责业务逻辑的实现,通常会调用多个 DAO 方法。事务注解应该应用在服务层的方法上,以确保这些操作在一个事务中执行。
  • DAO 层:负责具体的数据访问操作(例如查询、插入、更新、删除)。DAO 层的方法不应负责事务管理。

示例

假设我们有一个简单的应用,需要处理用户的账户余额更新操作。我们有一个 UserRepository 来访问数据库,以及一个 UserService 来处理业务逻辑。

DAO 层

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // CRUD methods provided by JpaRepository
}
Service 层

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    // 正确的做法:在服务层的方法上使用 @Transactional 注解
    @Transactional(rollbackFor = Exception.class)
    public void updateUserBalance(Long userId, double amount) {
        User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("User not found"));
        user.setBalance(user.getBalance() + amount);
        userRepository.save(user);
        
        // 假设这里有更多的业务逻辑,例如记录操作日志
        // 如果这些操作中的任何一个失败,整个事务应该回滚
    }
}
常见的错误做法

在 DAO 层使用 @Transactional 注解是不推荐的,因为 DAO 层不应该管理事务。


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class UserRepositoryImpl {

    @Autowired
    private UserRepository userRepository;

    // 错误的做法:在 DAO 层使用 @Transactional 注解
    @Transactional(rollbackFor = Exception.class)
    public void updateUserBalance(Long userId, double amount) {
        User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("User not found"));
        user.setBalance(user.getBalance() + amount);
        userRepository.save(user);
    }
}

2. 确保方法是通过 Spring 管理的 bean 调用

  • 代理机制:Spring 使用代理来管理事务。如果方法直接从同一个类中的另一个方法调用的,事务管理将不会生效。这是因为代理对象不会拦截对自身方法的调用。因此,确保事务方法是在通过 Spring 容器管理的 bean 间调用。(即:申明@Transactional事务的方法(doTransactionalMethod),应直接被Service调用,即service.doTransactionalMethod(),而不能间接被调用)

示例场景

我们将使用一个 UserService,其中包含一个事务方法 updateUserBalance,以及一个控制器 UserController 来触发这些服务。

UserService

负责更新用户的账户余额。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional(rollbackFor = Exception.class)
    public void updateUserBalance(Long userId, double amount) {
        User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("User not found"));
        user.setBalance(user.getBalance() + amount);
        userRepository.save(user);
    }

    // 这个方法用于演示直接调用事务方法不会生效
    public void updateUserBalanceDirect(Long userId, double amount) {
        // 直接调用内部的事务方法
        updateUserBalance(userId, amount);
    }
}
UserController

一个控制器类,用于外部调用 UserService 的事务方法和非事务方法。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    @GetMapping("/updateBalanceDirect")
    public String updateBalanceDirect(@RequestParam Long userId, @RequestParam double amount) {
        try {
            // 直接调用不会触发事务
            userService.updateUserBalanceDirect(userId, amount);
        } catch (Exception e) {
            return "Transaction failed: " + e.getMessage();
        }
    }

    @GetMapping("/updateBalance")
    public String updateBalance(@RequestParam Long userId, @RequestParam double amount) {
        try {
            // 通过代理调用会触发事务
            userService.updateUserBalance(userId, amount);
        } catch (Exception e) {
            return "Transaction failed: " + e.getMessage();
        }
    }
}

测试

通过 HTTP 请求调用 UserController 的方法来验证事务管理的行为。

  1. 启动应用。

  2. 发送请求:http://localhost:8080/updateBalanceDirect?userId=1&amount=1500

    • 期望结果:此调用不会触发事务,因为它是直接调用的 updateUserBalance 方法
  3. 发送请求:http://localhost:8080/updateBalance?userId=1&amount=1500

    • 期望结果:此调用会触发事务,因为它是通过代理调用的 updateUserBalance 方法

3. 检查异常类型

  • 确保抛出的异常类型与 rollbackFor 参数匹配。如果没有指定 rollbackFor,默认只在 RuntimeExceptionError 的情况下回滚。

    
    @Transactional(rollbackFor = Exception.class)
    public void myMethod() {
        try {
            // some database operations
        } catch (Exception e) {
            throw new MyCustomException(e);
        }
    }
    

4. 确保事务传播属性正确

  • 事务的传播属性可能会影响事务行为。默认情况下,@Transactional 的传播属性是 REQUIRED。检查是否需要其他传播属性。

5. 确保数据库支持事务

  • 确保你使用的数据库支持事务,并且使用了支持事务的存储引擎(例如,在 MySQL 中使用 InnoDB 而不是 MyISAM)。
转载自:https://juejin.cn/post/7382980041398911011
评论
请登录