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

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

前言

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

一、死锁的概念:

1、死锁的原因

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

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

2、死锁的常见原因

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

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

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

二、死锁代码示例场景

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 先更新 product表(获得product表锁),然后尝试插入数据到 order表。事务2 先更新 order表(获得order表锁),然后尝试更新 Product表。

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

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

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

2、死锁的检测

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

3、死锁解决方法

统一锁获取顺序:确保多个事务获取锁的顺序一致。比如,始终按照 products表 -> order表的顺序获取锁,这样可以避免交叉锁竞争。减少事务的持有锁的时间:尽量减少事务中持有锁的时间,比如尽量避免长时间的数据库查询和业务逻辑处理。使用悲观锁(Pessimistic Locking):对于可能导致死锁的场景,使用悲观锁来避免并发冲突。可以通过 FOR UPTATE 子句在 SQL 查询中显式地锁住行。使用乐观锁(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、使用乐观锁

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

数据库表结构:

@Entity

public class Product {

@Id

private int id;

private String name;

private int stock;

@Version

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

// getters and setters

}

更新时检查版本号:

@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);

}

}

四、总结

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

🎊 相关推荐

安卓系统ROM空间对智能手机性能及用户体验的重要影响
苹果 iPad 4(16GB/WiFi版)
约彩365官旧版本网客户端下载

苹果 iPad 4(16GB/WiFi版)

📅 08-21 👀 8404
小熊电子美容仪JMY-C02L1超声波去黑头粉刺吸出器铲皮机毛孔深度清洁神器美容洁面仪