MySQL 锁的分类
1. 按锁的粒度分类
1.1 全局锁(Global Lock)
- 作用范围:整个MySQL实例
- 典型场景:全库逻辑备份
- 实现方式:
FLUSH TABLES WITH READ LOCK
- 特点:阻塞所有DML和DDL操作
-- 加全局读锁
FLUSH TABLES WITH READ LOCK;
-- 执行备份操作
mysqldump --single-transaction --routines --triggers --all-databases > backup.sql
-- 释放锁
UNLOCK TABLES;
1.2 表锁(Table Lock)
- 作用范围:整张表
- 分类:
- 表共享读锁(Table Read Lock)
- 表独占写锁(Table Write Lock)
- 意向锁(Intention Lock)
-- 表级读锁
LOCK TABLES table_name READ;
-- 表级写锁
LOCK TABLES table_name WRITE;
-- 释放锁
UNLOCK TABLES;
实际生产应用场景:
场景1:数据库维护和批量导入
-- 场景:电商系统夜间批量导入商品数据
-- 防止导入过程中有用户查询,保证数据一致性
LOCK TABLES products WRITE;
LOAD DATA INFILE '/data/products_update.csv'
INTO TABLE products
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n';
-- 批量更新商品价格
UPDATE products SET price = price * 1.1 WHERE category = 'electronics';
UNLOCK TABLES;
场景2:报表生成时的数据一致性保证
-- 场景:金融系统生成月末对账报表
-- 确保生成报表期间数据不被修改
LOCK TABLES
transactions READ,
accounts READ,
balances READ;
-- 生成复杂的财务报表
SELECT
a.account_id,
a.account_name,
SUM(t.amount) as total_transactions,
b.current_balance
FROM accounts a
JOIN transactions t ON a.account_id = t.account_id
JOIN balances b ON a.account_id = b.account_id
WHERE t.transaction_date BETWEEN '2024-01-01' AND '2024-01-31'
GROUP BY a.account_id;
UNLOCK TABLES;
场景3:数据迁移和结构变更
-- 场景:系统升级时的数据迁移
-- 保证迁移过程中数据不被修改
LOCK TABLES old_user_table READ, new_user_table WRITE;
-- 数据迁移逻辑
INSERT INTO new_user_table (user_id, username, email, created_at)
SELECT user_id, user_name, email_address, create_time
FROM old_user_table;
UNLOCK TABLES;
1.3 行锁(Row Lock)
- 作用范围:表中的行记录
- 引擎支持:InnoDB引擎
- 分类:
- 共享锁(S Lock)
- 排他锁(X Lock)
2. 按锁的性质分类
2.1 共享锁(Shared Lock, S Lock)
- 特性:多个事务可以同时持有同一资源的共享锁
- 兼容性:与共享锁兼容,与排他锁不兼容
- SQL语句:
SELECT ... LOCK IN SHARE MODE
-- 加共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- MySQL 8.0 新语法
SELECT * FROM users WHERE id = 1 FOR SHARE;
实际生产应用场景:
场景1:订单库存检查和预扣减
-- 场景:电商下单时的库存检查
-- 多个用户同时下单同一商品,需要读取库存但防止被修改
BEGIN;
-- 使用共享锁读取库存,允许其他事务也读取,但不允许修改
SELECT stock_quantity
FROM products
WHERE product_id = 12345
FOR SHARE;
-- 业务逻辑检查库存是否充足
-- 如果库存充足,再获取排他锁进行扣减
IF stock_quantity >= order_quantity THEN
UPDATE products
SET stock_quantity = stock_quantity - order_quantity
WHERE product_id = 12345;
END IF;
COMMIT;
场景2:金融系统的余额查询
-- 场景:银行系统查询余额时防止余额被修改
-- 多个查询可以同时进行,但防止转账操作修改余额
BEGIN;
SELECT
account_id,
balance,
available_balance
FROM accounts
WHERE account_id = 'ACC001'
FOR SHARE;
-- 其他业务逻辑,如计算利息、生成报表等
COMMIT;
场景3:配置信息的并发读取
-- 场景:系统配置读取,允许多个服务同时读取但防止配置被修改
SELECT config_value
FROM system_config
WHERE config_key = 'payment_gateway_url'
FOR SHARE;
2.2 排他锁(Exclusive Lock, X Lock)
- 特性:只有一个事务可以持有排他锁
- 兼容性:与任何锁都不兼容
- SQL语句:
SELECT ... FOR UPDATE
、UPDATE
、DELETE
、INSERT
-- 加排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- DML操作自动加排他锁
UPDATE users SET name = 'John' WHERE id = 1;
实际生产应用场景:
场景1:秒杀系统的库存扣减
-- 场景:秒杀活动,确保库存扣减的原子性
-- 防止超卖问题
BEGIN;
-- 使用排他锁锁定商品记录
SELECT stock_quantity
FROM seckill_products
WHERE product_id = 99999 AND activity_id = 12345
FOR UPDATE;
-- 检查库存并扣减
UPDATE seckill_products
SET stock_quantity = stock_quantity - 1,
sold_quantity = sold_quantity + 1
WHERE product_id = 99999
AND activity_id = 12345
AND stock_quantity > 0;
-- 如果影响行数为0,说明库存不足
-- 创建订单记录
INSERT INTO orders (user_id, product_id, quantity, order_time)
VALUES (12345, 99999, 1, NOW());
COMMIT;
场景2:银行转账操作
-- 场景:银行转账,确保账户余额操作的原子性
BEGIN;
-- 锁定转出账户
SELECT balance
FROM accounts
WHERE account_id = 'FROM_ACCOUNT'
FOR UPDATE;
-- 锁定转入账户
SELECT balance
FROM accounts
WHERE account_id = 'TO_ACCOUNT'
FOR UPDATE;
-- 扣减转出账户余额
UPDATE accounts
SET balance = balance - 1000
WHERE account_id = 'FROM_ACCOUNT' AND balance >= 1000;
-- 增加转入账户余额
UPDATE accounts
SET balance = balance + 1000
WHERE account_id = 'TO_ACCOUNT';
-- 记录转账流水
INSERT INTO transactions (from_account, to_account, amount, trans_time)
VALUES ('FROM_ACCOUNT', 'TO_ACCOUNT', 1000, NOW());
COMMIT;
场景3:唯一序列号生成
-- 场景:生成全局唯一的订单号
BEGIN;
-- 锁定序列号表
SELECT current_value
FROM sequence_generator
WHERE seq_name = 'ORDER_NO'
FOR UPDATE;
-- 更新序列号
UPDATE sequence_generator
SET current_value = current_value + 1
WHERE seq_name = 'ORDER_NO';
-- 生成订单号:日期 + 序列号
SET @order_no = CONCAT(DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(current_value + 1, 8, '0'));
COMMIT;
场景4:状态机流转控制
-- 场景:工单状态流转,确保状态变更的唯一性
BEGIN;
-- 锁定工单记录
SELECT status, assigned_to
FROM work_orders
WHERE order_id = 'WO20241201001'
FOR UPDATE;
-- 检查状态流转是否合法
-- 只有状态为'PENDING'才能转为'IN_PROGRESS'
UPDATE work_orders
SET status = 'IN_PROGRESS',
assigned_to = 'USER123',
update_time = NOW()
WHERE order_id = 'WO20241201001'
AND status = 'PENDING';
-- 记录状态变更日志
INSERT INTO order_status_log (order_id, old_status, new_status, operator, change_time)
VALUES ('WO20241201001', 'PENDING', 'IN_PROGRESS', 'USER123', NOW());
COMMIT;
3. InnoDB特有的锁
3.1 意向锁(Intention Lock)
- 意向共享锁(IS):事务准备在某些行上加共享锁
- 意向排他锁(IX):事务准备在某些行上加排他锁
3.2 记录锁(Record Lock)
-- 对主键或唯一索引的等值查询
SELECT * FROM users WHERE id = 1 FOR UPDATE;
实际生产应用场景:
-- 场景:用户账户余额精确锁定
-- 只锁定特定用户记录,不影响其他用户操作
BEGIN;
SELECT balance
FROM user_accounts
WHERE user_id = 12345
FOR UPDATE; -- 只锁定user_id=12345这一行
UPDATE user_accounts
SET balance = balance - 100
WHERE user_id = 12345;
COMMIT;
3.3 间隙锁(Gap Lock)
-- 在RR隔离级别下,范围查询会产生间隙锁
SELECT * FROM users WHERE id BETWEEN 5 AND 10 FOR UPDATE;
实际生产应用场景:
场景1:防止幻读的范围查询
-- 场景:统计某个时间段的订单,防止统计期间有新订单插入
BEGIN;
-- 锁定时间范围,防止新记录插入到这个范围内
SELECT COUNT(*), SUM(amount)
FROM orders
WHERE create_time BETWEEN '2024-12-01 00:00:00' AND '2024-12-01 23:59:59'
FOR UPDATE;
-- 其他业务逻辑
COMMIT;
场景2:确保ID序列的连续性
-- 场景:确保批次号的连续性,防止中间插入其他批次
BEGIN;
-- 查询最大批次号并锁定间隙
SELECT MAX(batch_id) as max_batch
FROM production_batches
WHERE product_line = 'LINE_A'
FOR UPDATE;
-- 插入新的连续批次
INSERT INTO production_batches (batch_id, product_line, start_time)
VALUES (max_batch + 1, 'LINE_A', NOW());
COMMIT;
3.4 临键锁(Next-Key Lock)
- 定义:记录锁 + 间隙锁的组合
- 作用:解决幻读问题
实际生产应用场景:
场景1:防止幻读的分页查询
-- 场景:金融系统的交易记录分页,防止分页过程中数据变化
BEGIN;
-- 使用临键锁防止范围内的幻读
SELECT transaction_id, amount, transaction_time
FROM transactions
WHERE account_id = 'ACC001'
AND transaction_time >= '2024-12-01'
ORDER BY transaction_time
LIMIT 20
FOR UPDATE;
COMMIT;
场景2:库存预留和释放
-- 场景:电商系统库存预留,防止同一商品的并发预留冲突
BEGIN;
-- 锁定商品ID范围,防止相关记录的插入和修改
SELECT * FROM inventory_reservations
WHERE product_id BETWEEN 1000 AND 1100
AND status = 'ACTIVE'
FOR UPDATE;
-- 插入新的预留记录
INSERT INTO inventory_reservations (product_id, user_id, quantity, expire_time)
VALUES (1050, 12345, 2, DATE_ADD(NOW(), INTERVAL 30 MINUTE));
COMMIT;
3.5 插入意向锁(Insert Intention Lock)
-- 插入操作会先尝试获取插入意向锁
INSERT INTO users (id, name) VALUES (15, 'Alice');
实际生产应用场景:
场景1:高并发订单创建
-- 场景:电商秒杀,大量用户同时创建订单
-- 插入意向锁允许多个事务同时插入不同的记录
-- 事务1
BEGIN;
INSERT INTO orders (order_id, user_id, product_id, create_time)
VALUES ('ORD001', 1001, 888, NOW());
COMMIT;
-- 事务2(同时进行)
BEGIN;
INSERT INTO orders (order_id, user_id, product_id, create_time)
VALUES ('ORD002', 1002, 888, NOW()); -- 不会被事务1阻塞
COMMIT;
场景2:分布式ID生成
-- 场景:多个服务节点同时生成分布式ID
-- 不同节点可以同时插入不同范围的ID
CREATE TABLE distributed_ids (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
node_id INT,
business_type VARCHAR(50),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 节点1插入
INSERT INTO distributed_ids (node_id, business_type)
VALUES (1, 'ORDER');
-- 节点2同时插入(不会冲突)
INSERT INTO distributed_ids (node_id, business_type)
VALUES (2, 'ORDER');
3.6 意向锁的实际应用
实际生产应用场景:
场景1:表级操作与行级操作的协调
-- 场景:数据库维护期间,需要锁定整个表
-- 意向锁帮助快速判断表是否被行级锁占用
-- 某个事务正在执行行级操作
BEGIN;
SELECT * FROM orders WHERE order_id = 'ORD001' FOR UPDATE; -- 自动加IS锁到表级别
-- 长时间的业务处理...
-- 另一个维护操作尝试锁定整个表
LOCK TABLES orders WRITE; -- 会检查意向锁,发现表被占用,等待或失败
场景2:在线DDL操作的冲突检测
-- 场景:在线修改表结构时,检查是否有活跃的事务
-- 意向锁帮助快速判断表的使用情况
-- 正在进行的业务操作
BEGIN;
UPDATE orders SET status = 'SHIPPED' WHERE order_id = 'ORD001'; -- 加IX锁
-- DBA尝试修改表结构
ALTER TABLE orders ADD COLUMN shipping_address TEXT; -- 会检查意向锁状态
死锁判定原理
1. 死锁的定义
死锁是指两个或多个事务在执行过程中,因争夺资源而造成的一种互相等待的现象。
2. 死锁的四个必要条件
- 互斥条件:资源不能被多个事务同时使用
- 请求和保持条件:事务已经持有资源,又请求新的资源
- 不剥夺条件:已获得的资源不能被强制剥夺
- 环路等待条件:存在一个事务等待环路
3. MySQL死锁检测机制
3.1 等待图算法(Wait-for Graph)
事务T1 → 等待 → 资源R1 → 被持有 → 事务T2
事务T2 → 等待 → 资源R2 → 被持有 → 事务T1
3.2 死锁检测参数
-- 查看死锁检测相关参数
SHOW VARIABLES LIKE '%deadlock%';
SHOW VARIABLES LIKE '%timeout%';
-- 关键参数
-- innodb_deadlock_detect: 是否开启死锁检测(默认ON)
-- innodb_lock_wait_timeout: 锁等待超时时间(默认50秒)
3.3 死锁信息查看
-- 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS;
-- 或者查看错误日志
SHOW VARIABLES LIKE 'log_error';
死锁的具体场景
场景1:经典的两个事务相互等待
表结构:
CREATE TABLE account (
id INT PRIMARY KEY,
balance DECIMAL(10,2),
name VARCHAR(50)
);
INSERT INTO account VALUES (1, 1000.00, 'Alice'), (2, 1000.00, 'Bob');
死锁场景:
-- 事务1
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1; -- 获得id=1的行锁
-- 此时等待事务2执行
UPDATE account SET balance = balance + 100 WHERE id = 2; -- 等待id=2的行锁
-- 事务2
BEGIN;
UPDATE account SET balance = balance - 50 WHERE id = 2; -- 获得id=2的行锁
UPDATE account SET balance = balance + 50 WHERE id = 1; -- 等待id=1的行锁,死锁!
场景2:索引不当导致的死锁
表结构:
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
status VARCHAR(20),
amount DECIMAL(10,2),
INDEX idx_user_id (user_id)
);
死锁场景:
-- 事务1
BEGIN;
UPDATE orders SET status = 'paid' WHERE user_id = 100;
-- 事务2
BEGIN;
UPDATE orders SET status = 'shipped' WHERE user_id = 100;
-- 如果user_id有多条记录,可能因为锁定顺序不同导致死锁
场景3:间隙锁导致的死锁
-- 事务1
BEGIN;
SELECT * FROM orders WHERE id = 15 FOR UPDATE; -- 假设id=15不存在,产生间隙锁
-- 事务2
BEGIN;
SELECT * FROM orders WHERE id = 16 FOR UPDATE; -- 假设id=16不存在,产生间隙锁
INSERT INTO orders (id, user_id, status, amount) VALUES (15, 100, 'pending', 100.00);
-- 事务2尝试插入,等待事务1的间隙锁
-- 事务1继续
INSERT INTO orders (id, user_id, status, amount) VALUES (16, 101, 'pending', 200.00);
-- 事务1尝试插入,等待事务2的间隙锁,形成死锁
场景4:批量操作导致的死锁
-- 事务1:按id正序更新
BEGIN;
UPDATE products SET stock = stock - 1 WHERE id IN (1, 2, 3, 4, 5);
-- 事务2:按id倒序更新
BEGIN;
UPDATE products SET stock = stock - 1 WHERE id IN (5, 4, 3, 2, 1);
-- 可能因为锁定顺序不同导致死锁
死锁的解决方案
1. 预防死锁
1.1 统一锁定顺序
-- 错误示例:不同的锁定顺序
-- 事务1: 先锁A后锁B
-- 事务2: 先锁B后锁A
-- 正确示例:统一按主键大小顺序锁定
BEGIN;
-- 总是按照id从小到大的顺序锁定
SELECT * FROM account WHERE id = 1 FOR UPDATE;
SELECT * FROM account WHERE id = 2 FOR UPDATE;
-- 执行业务逻辑
COMMIT;
1.2 减少事务持锁时间
-- 优化前:长事务
BEGIN;
SELECT * FROM orders WHERE user_id = 100 FOR UPDATE;
-- 执行复杂的业务逻辑(网络调用、文件操作等)
UPDATE orders SET status = 'processed' WHERE user_id = 100;
COMMIT;
-- 优化后:短事务
-- 先查询数据
SELECT * FROM orders WHERE user_id = 100;
-- 执行业务逻辑
-- 最后快速更新
BEGIN;
UPDATE orders SET status = 'processed' WHERE user_id = 100;
COMMIT;
1.3 使用较低的隔离级别
-- 在允许的业务场景下使用READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
2. 检测和处理死锁
2.1 应用层重试机制
@Service
public class OrderService {
private static final int MAX_RETRY_TIMES = 3;
@Transactional
public void updateOrder(Long orderId) {
int retryCount = 0;
while (retryCount < MAX_RETRY_TIMES) {
try {
// 执行数据库操作
doUpdateOrder(orderId);
break; // 成功则跳出循环
} catch (SQLException e) {
if (isDeadlock(e) && retryCount < MAX_RETRY_TIMES - 1) {
retryCount++;
// 随机等待一段时间后重试
try {
Thread.sleep(50 + new Random().nextInt(100));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("操作被中断", ie);
}
} else {
throw new RuntimeException("更新订单失败", e);
}
}
}
}
private boolean isDeadlock(SQLException e) {
return e.getErrorCode() == 1213; // MySQL死锁错误码
}
}
2.2 设置合理的锁等待超时时间
-- 设置锁等待超时时间(单位:秒)
SET innodb_lock_wait_timeout = 5;
-- 全局设置
SET GLOBAL innodb_lock_wait_timeout = 10;
2.3 监控死锁
-- 创建死锁监控脚本
DELIMITER $$
CREATE PROCEDURE MonitorDeadlocks()
BEGIN
DECLARE deadlock_count INT DEFAULT 0;
-- 查询死锁计数
SELECT VARIABLE_VALUE INTO deadlock_count
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_deadlocks';
-- 记录死锁信息
INSERT INTO deadlock_log (deadlock_count, check_time)
VALUES (deadlock_count, NOW());
END$$
DELIMITER ;
-- 创建日志表
CREATE TABLE deadlock_log (
id INT AUTO_INCREMENT PRIMARY KEY,
deadlock_count INT,
check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
3. 数据库配置优化
-- 优化死锁检测
SET GLOBAL innodb_deadlock_detect = ON;
-- 设置合理的锁等待超时
SET GLOBAL innodb_lock_wait_timeout = 10;
-- 优化事务隔离级别
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
-- 调整锁相关参数
SET GLOBAL innodb_table_locks = OFF;
最佳实践
1. 设计层面
1.1 合理的索引设计
-- 为经常用于WHERE条件的列创建索引
CREATE INDEX idx_user_status ON orders(user_id, status);
-- 避免过多的索引,减少锁竞争
-- 定期检查和清理不必要的索引
1.2 表结构优化
-- 使用合适的数据类型
-- 避免过长的行,减少锁的粒度影响
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
amount DECIMAL(10,2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_status (user_id, status)
);
2. 应用层面
2.1 事务设计原则
// 1. 保持事务简短
@Transactional
public void shortTransaction() {
// 只包含必要的数据库操作
orderRepository.updateStatus(orderId, "PAID");
}
// 2. 避免在事务中进行外部调用
public void processOrder(Long orderId) {
// 外部调用放在事务外
PaymentResult result = paymentService.process(orderId);
// 事务内只做数据库操作
updateOrderStatus(orderId, result.getStatus());
}
2.2 批量操作优化
// 优化批量操作,使用统一的排序
public void batchUpdateOrders(List<Long> orderIds) {
// 对ID进行排序,保证锁定顺序一致
Collections.sort(orderIds);
for (Long orderId : orderIds) {
updateOrder(orderId);
}
}
3. 监控和排查
3.1 死锁监控SQL
-- 查看死锁状态
SELECT
ENGINE_TRANSACTION_ID,
THREAD_ID,
EVENT_NAME,
CURRENT_SCHEMA,
SQL_TEXT,
BLOCKING_ENGINE_TRANSACTION_ID
FROM performance_schema.events_statements_current
WHERE SQL_TEXT IS NOT NULL;
-- 查看锁等待情况
SELECT
waiting.ENGINE_TRANSACTION_ID as waiting_trx_id,
waiting.requesting_engine_lock_id,
blocking.ENGINE_TRANSACTION_ID as blocking_trx_id,
blocking.blocking_engine_lock_id
FROM performance_schema.data_lock_waits waiting
JOIN performance_schema.data_locks blocking
ON waiting.blocking_engine_lock_id = blocking.engine_lock_id;
3.2 性能分析
-- 启用慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
-- 分析锁竞争
SELECT
object_schema,
object_name,
count_read,
count_write,
count_read_with_shared_locks,
count_read_high_priority,
count_read_no_insert,
count_read_external
FROM performance_schema.table_io_waits_summary_by_table
WHERE object_schema NOT IN ('mysql', 'information_schema', 'performance_schema')
ORDER BY count_read + count_write DESC;
综合实际应用案例
电商秒杀系统的完整锁策略
这是一个综合运用多种锁机制的实际生产案例:
@Service
@Transactional
public class SeckillService {
/**
* 秒杀下单 - 综合锁策略应用
*/
public SeckillResult processSeckill(Long userId, Long productId, Long activityId) {
try {
// 1. 使用共享锁检查活动状态(允许多个用户同时检查)
String activityStatus = checkActivityStatus(activityId);
if (!"ACTIVE".equals(activityStatus)) {
return SeckillResult.fail("活动未开始或已结束");
}
// 2. 使用排他锁锁定库存记录(防止超卖)
if (!lockAndCheckStock(productId, activityId)) {
return SeckillResult.fail("库存不足");
}
// 3. 使用记录锁检查用户是否已参与(避免重复购买)
if (hasUserParticipated(userId, activityId)) {
return SeckillResult.fail("您已参与过此活动");
}
// 4. 创建订单(插入意向锁允许并发插入不同订单)
String orderId = createOrder(userId, productId, activityId);
// 5. 扣减库存
updateStock(productId, activityId);
return SeckillResult.success(orderId);
} catch (Exception e) {
// 死锁重试机制
if (isDeadlock(e)) {
return retrySeckill(userId, productId, activityId);
}
throw e;
}
}
@Transactional(readOnly = true)
private String checkActivityStatus(Long activityId) {
// 使用共享锁,允许多个事务同时读取活动状态
return jdbcTemplate.queryForObject(
"SELECT status FROM seckill_activities WHERE id = ? FOR SHARE",
String.class, activityId);
}
private boolean lockAndCheckStock(Long productId, Long activityId) {
// 使用排他锁锁定库存记录
Integer stock = jdbcTemplate.queryForObject(
"SELECT stock_quantity FROM seckill_products " +
"WHERE product_id = ? AND activity_id = ? FOR UPDATE",
Integer.class, productId, activityId);
return stock != null && stock > 0;
}
private boolean hasUserParticipated(Long userId, Long activityId) {
// 使用记录锁检查用户参与记录
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM seckill_orders " +
"WHERE user_id = ? AND activity_id = ? FOR UPDATE",
Integer.class, userId, activityId);
return count > 0;
}
private String createOrder(Long userId, Long productId, Long activityId) {
String orderId = generateOrderId();
// 插入订单,插入意向锁允许并发插入
jdbcTemplate.update(
"INSERT INTO seckill_orders (order_id, user_id, product_id, activity_id, status, create_time) " +
"VALUES (?, ?, ?, ?, 'PENDING', NOW())",
orderId, userId, productId, activityId);
return orderId;
}
private void updateStock(Long productId, Long activityId) {
// 扣减库存
int affected = jdbcTemplate.update(
"UPDATE seckill_products " +
"SET stock_quantity = stock_quantity - 1, sold_quantity = sold_quantity + 1 " +
"WHERE product_id = ? AND activity_id = ? AND stock_quantity > 0",
productId, activityId);
if (affected == 0) {
throw new RuntimeException("库存扣减失败");
}
}
}
分布式锁与数据库锁的配合使用
@Service
public class DistributedLockWithDbLockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 分布式锁 + 数据库锁的组合策略
* 外层使用Redis分布式锁减少数据库压力
* 内层使用数据库锁保证最终一致性
*/
public boolean transferMoney(String fromAccount, String toAccount, BigDecimal amount) {
// 1. 按账户ID排序,避免死锁
List<String> accounts = Arrays.asList(fromAccount, toAccount);
Collections.sort(accounts);
// 2. 获取分布式锁(减少数据库锁竞争)
String lockKey = "transfer:" + String.join(":", accounts);
return executeWithDistributedLock(lockKey, () -> {
return executeTransfer(fromAccount, toAccount, amount);
});
}
@Transactional
private boolean executeTransfer(String fromAccount, String toAccount, BigDecimal amount) {
// 3. 按固定顺序获取数据库锁,避免死锁
List<String> accounts = Arrays.asList(fromAccount, toAccount);
Collections.sort(accounts);
Map<String, BigDecimal> balances = new HashMap<>();
// 4. 依次锁定账户(排他锁)
for (String account : accounts) {
BigDecimal balance = jdbcTemplate.queryForObject(
"SELECT balance FROM accounts WHERE account_id = ? FOR UPDATE",
BigDecimal.class, account);
balances.put(account, balance);
}
// 5. 检查余额
if (balances.get(fromAccount).compareTo(amount) < 0) {
return false;
}
// 6. 执行转账
jdbcTemplate.update(
"UPDATE accounts SET balance = balance - ? WHERE account_id = ?",
amount, fromAccount);
jdbcTemplate.update(
"UPDATE accounts SET balance = balance + ? WHERE account_id = ?",
amount, toAccount);
return true;
}
private <T> T executeWithDistributedLock(String lockKey, Supplier<T> task) {
String lockValue = UUID.randomUUID().toString();
try {
// 获取分布式锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, Duration.ofSeconds(30));
if (!locked) {
throw new RuntimeException("获取锁失败");
}
return task.get();
} finally {
// 释放分布式锁
releaseLock(lockKey, lockValue);
}
}
}
生产环境最佳实践总结
1. 锁选择策略矩阵
应用场景 | 推荐锁类型 | 原因 | 注意事项 |
---|---|---|---|
库存扣减 | 排他锁(FOR UPDATE) | 防止超卖 | 事务要短,及时提交 |
余额查询 | 共享锁(FOR SHARE) | 允许并发读,防止读脏数据 | 避免长时间持锁 |
批量导入 | 表写锁(WRITE) | 保证数据一致性 | 选择业务低峰期 |
报表生成 | 表读锁(READ) | 保证报表数据一致性 | 使用只读副本更好 |
范围统计 | 临键锁 | 防止幻读 | 考虑使用快照读 |
高并发插入 | 插入意向锁 | 提高并发性能 | 避免间隙锁冲突 |
2. 死锁预防检查清单
-- 1. 检查事务隔离级别
SELECT @@transaction_isolation;
-- 2. 检查死锁检测设置
SHOW VARIABLES LIKE '%deadlock%';
-- 3. 检查锁等待超时设置
SHOW VARIABLES LIKE '%lock_wait_timeout%';
-- 4. 监控死锁发生情况
SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_deadlocks';
3. 代码层面最佳实践
// ✅ 正确:统一锁定顺序
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Long firstId = Math.min(fromId, toId);
Long secondId = Math.max(fromId, toId);
// 总是按ID大小顺序锁定
lockAccount(firstId);
lockAccount(secondId);
// 执行转账逻辑
}
// ❌ 错误:随意的锁定顺序
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
lockAccount(fromId); // 可能导致死锁
lockAccount(toId);
}
// ✅ 正确:短事务
@Transactional
public void quickUpdate(Long id, String status) {
orderService.updateStatus(id, status); // 只包含数据库操作
}
public void processOrder(Long id) {
// 复杂逻辑在事务外执行
PaymentResult result = paymentService.process(id);
sendNotification(id);
// 快速更新数据库
quickUpdate(id, result.getStatus());
}
// ❌ 错误:长事务
@Transactional
public void processOrder(Long id) {
orderService.updateStatus(id, "PROCESSING");
paymentService.process(id); // 外部调用
Thread.sleep(5000); // 长时间等待
sendNotification(id); // 网络调用
orderService.updateStatus(id, "COMPLETED");
}
总结
MySQL的锁机制是保证数据一致性的重要手段,但也是死锁产生的根源。理解不同类型的锁、死锁的形成原理和解决方案,对于开发高并发、高可靠的数据库应用至关重要。
关键要点:
- 预防优于治疗:通过合理的设计和编码规范预防死锁
- 统一锁定顺序:避免不同事务以不同顺序锁定资源
- 保持事务简短:减少锁持有时间
- 合理使用索引:提高查询效率,减少锁范围
- 建立监控机制:及时发现和处理死锁问题
- 应用层重试:在业务允许的情况下实现重试机制
- 选择合适的锁:根据业务场景选择最适合的锁类型
- 分层锁策略:结合分布式锁和数据库锁,减少竞争
通过这些最佳实践和实际应用场景的理解,可以大大降低死锁的发生概率,提高系统的稳定性和性能。在实际生产环境中,要根据具体的业务场景和性能要求,灵活运用各种锁机制,构建高效、可靠的数据库应用系统。