前言
工作与日常开发中,事务对数据进行在增删查改(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);
}
}
四、总结
以上可以在很大程度上避免数据库死锁的发生,确保系统的稳定性和高效性。具体选择哪种方法取决于项目的实际需求和业务逻辑。建议结合多种策略,综合考虑性能、并发性和可靠性。