搞定项目中死锁问题!java项目中常见的死锁与解决办法

前言

       工作与日常开发中,事务对数据进行在增删查改(CRUD)操作难免会出现死锁情况,单体应用项目需要找出死锁原因还是比较容易,很多小伙伴在搭建复杂的微服务项目调用的时候经常出现死锁情况,微服务调用排查比较困难,需要把业务熟悉做好逻辑梳理排查问题更得心应手。数据库的死锁通常是由于多个数据库操作相互竞争锁资源,导致系统无法继续执行的情况。死锁发生的典型场景是两个或更多的事务在并发执行时,互相等待对方释放锁,最终导致每个事务都无法继续执行。博主经常遇到类似的问题,希望下面的这篇文章给您带来收获与解决意识。

一、死锁的概念:

1、死锁的原因

死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法继续前进。在Java多线程编程中,死锁通常发生在以下几种情况:

  1. 互斥条件:至少有一个资源必须处于非共享模式,即一次只有一个线程能使用资源。
  2. 占有并等待:一个线程至少持有一个资源,并等待另一个资源,而该资源为其他线程所持有。
  3. 非抢占式(不可剥夺:资源不能被抢占,即资源只能被线程显式地释放。
  4. 循环等待:存在一种线程资源的循环等待链,每个线程持有一个线程在下一个环节中所
    在这里插入图片描述

2、死锁的常见原因

  1. 事务中的多个查询和更新操作(行级锁竞争)。
  2. 长时间持有锁,导致其他事务无法执行。
  3. 不同事务以不同的顺序获取锁(例如先获取锁A,再获取锁B,而另一个事务则先获取锁B,再获取锁A)。

3、在增删查改(CRUD)操作中,死锁可能发生在以下几种情况下:

  1. 两个或更多事务竞争对数据库的资源(如行锁、表锁等)。
  2. 事务顺序错误或并发控制不当。

二、死锁代码示例场景

1、死锁发生场景

假设我们有一个简单的库存管理系统,包含两个表:products 和 orders。两个用户(线程)同时尝试更新库存数量和创建订单,可能会导致死锁。

  • 表结构
CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(255),
    stock INT
);

CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    product_id INT,
    quantity INT,
    FOREIGN KEY (product_id) REFERENCES products(id)
);

  • 案例代码

1.第一个事务:

@Service
public class InventoryService1 {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private OrderRepository orderRepository;

    @Transactional
    public void placeOrder(int productId, int quantity) {
        Product product = productRepository.findById(productId).orElseThrow(() -> new RuntimeException("Product not found"));
        if (product.getStock() < quantity) {
            throw new RuntimeException("Insufficient stock");
        }

        // 更新库存
        product.setStock(product.getStock() - quantity);
        productRepository.saveOrUpdate(product);

        // 创建订单
        Order order = new Order();
        order.setProductId(productId);
        order.setQuantity(quantity);
        orderRepository.save(order);

        System.out.println("Order placed successfully");
    }
}

2.第二个事务:

@Service
public class InventoryService2 {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private OrderRepository orderRepository;

    @Transactional
    public void placeOrder(int productId, int quantity) {

         // 创建订单
        Order order = new Order();
        order.setProductId(productId);
        order.setQuantity(quantity);
        orderRepository.save(order);
        
        Product product = productRepository.findById(productId).orElseThrow(() -> new RuntimeException("Product not found"));
        if (product.getStock() < quantity) {
            throw new RuntimeException("Insufficient stock");
        }

        // 更新库存
        product.setStock(product.getStock() - quantity);
        productRepository.saveOrUpdate(product);

        System.out.println("Order placed successfully");
    }
}

在上面的两个事务中:

  1. 事务1 先更新 product表(获得product表锁),然后尝试插入数据到 order表。
  2. 事务2 先更新 order表(获得order表锁),然后尝试更新 Product表。

如果两个事务同时运行,它们可能会互相等待对方释放锁:

  • 事务1 拿到了 product表的锁,但在更新 order表时,需要等待事务2 释放 order表的锁。
  • 事务2 拿到了 order表的锁,但在更新 product表时,需要等待事务1 释放 product表的锁。

由于它们互相等待对方释放锁,导致了死锁。

2、死锁的检测

数据库一般会在检测到死锁时自动回滚一个事务,以解除死锁。例如,MySQL 的 InnoDB 存储引擎会自动检测死锁,并回滚其中一个事务,以便其他事务能够继续执行。死锁会通过日志记录,开发人员可以查看日志来诊断问题。

3、死锁解决方法

  1. 统一锁获取顺序:确保多个事务获取锁的顺序一致。比如,始终按照 products表 -> order表的顺序获取锁,这样可以避免交叉锁竞争。
  2. 减少事务的持有锁的时间:尽量减少事务中持有锁的时间,比如尽量避免长时间的数据库查询和业务逻辑处理。
  3. 使用悲观锁(Pessimistic Locking):对于可能导致死锁的场景,使用悲观锁来避免并发冲突。可以通过 FOR UPTATE 子句在 SQL 查询中显式地锁住行。
  4. 使用乐观锁(Optimistic Locking):如果业务场景允许,可以使用乐观锁机制,避免事务之间的相互依赖。乐观锁常常通过版本号或者时间戳来管理并发。

三、死锁解决方法代码示例

1、通过统一锁获取顺序来避免死锁:

@Service
public class InventoryService1 {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private OrderRepository orderRepository;

    @Transactional
    public void placeOrder(int productId, int quantity) {
        Product product = productRepository.findById(productId).orElseThrow(() -> new RuntimeException("Product not found"));
        if (product.getStock() < quantity) {
            throw new RuntimeException("Insufficient stock");
        }

        // 更新库存
        product.setStock(product.getStock() - quantity);
        productRepository.saveOrUpdate(product);

        // 创建订单
        Order order = new Order();
        order.setProductId(productId);
        order.setQuantity(quantity);
        orderRepository.save(order);

        System.out.println("Order placed successfully");
    }
}

@Service
public class InventoryService2 {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private OrderRepository orderRepository;

    @Transactional
    public void placeOrder(int productId, int quantity) {
        Product product = productRepository.findById(productId).orElseThrow(() -> new RuntimeException("Product not found"));
        if (product.getStock() < quantity) {
            throw new RuntimeException("Insufficient stock");
        }

        // 更新库存
        product.setStock(product.getStock() - quantity);
        productRepository.saveOrUpdate(product);

        // 创建订单
        Order order = new Order();
        order.setProductId(productId);
        order.setQuantity(quantity);
        orderRepository.save(order);

        System.out.println("Order placed successfully");
    }
}

2、设置事务超时时间

在数据库配置中设置事务的超时时间,当事务等待超过指定时间后自动回滚,避免长时间等待导致系统资源浪费。

spring:
  jpa:
    properties:
      hibernate:
        transaction:
          timeout: 30 # 设置事务超时时间为30秒

3、使用悲观锁

通过使用悲观锁,我们可以确保每个事务在对数据进行操作时会显式地锁定行,防止其他事务同时操作这些数据。例如,使用 FOR UPDATE 锁定某一行:使用 SELECT … FOR UPDATE 显式加锁

@Transactional
public void placeOrder(int productId, int quantity) {
    // 使用 SELECT ... FOR UPDATE 显式加锁
    Product product = productRepository.findForUpdate(productId);
    if (product.getStock() < quantity) {
        throw new RuntimeException("Insufficient stock");
    }

    // 更新库存
    product.setStock(product.getStock() - quantity);
    productRepository.saveOrUpdate(product);

    // 创建订单
    Order order = new Order();
    order.setProductId(productId);
    order.setQuantity(quantity);
    orderRepository.save(order);

    System.out.println("Order placed successfully");
}

4、使用乐观锁

乐观锁是一种并发控制机制,它假设冲突不常发生,因此不会主动加锁,而是在提交时检查是否有冲突。如果发生冲突,则重试事务。通过这种方式,如果两个事务尝试并发修改相同的用户数据,后提交的事务会因版本号冲突而失败,从而避免死锁。

  1. 数据库表结构
@Entity
public class Product {
    @Id
    private int id;
    private String name;
    private int stock;

    @Version
    private int version; // 添加版本字段用于乐观锁

    // getters and setters
}
  1. 更新时检查版本号
@Transactional
public void placeOrder(int productId, int quantity) {
    try {
        // 读取数据时同时获取版本号
        Product product = productRepository.findById(productId).orElseThrow(() -> new RuntimeException("Product not found"));
        if (product.getStock() < quantity) {
            throw new RuntimeException("Insufficient stock");
        }
        // 执行业务逻辑时,检查版本号是否一致

        // 更新库存
        product.setStock(product.getStock() - quantity);
        //productRepository.save(product);
        int updatedRows = jdbcTemplate.update(
            "UPDATE product SET stock = product.getStock() - quantity, version = version + 1 WHERE id = ? AND version = ?",
            product.getId(), product.getVersion()
        );

        if (updatedRows == 0) {
            throw new OptimisticLockException("Version mismatch, transaction aborted");
        }

        // 创建订单
        Order order = new Order();
        order.setProductId(productId);
        order.setQuantity(quantity);
        orderRepository.save(order);

        System.out.println("Order placed successfully");
    } catch (OptimisticLockException e) {
        // 处理乐观锁异常,重试事务
        System.out.println("Concurrency conflict detected, retrying...");
        placeOrder(productId, quantity);
    }
}

四、总结

       以上可以在很大程度上避免数据库死锁的发生,确保系统的稳定性和高效性。具体选择哪种方法取决于项目的实际需求和业务逻辑。建议结合多种策略,综合考虑性能、并发性和可靠性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值