MYSQL中的锁
一、 MySQL锁概述
MySQL锁机制是数据库管理系统实现并发控制的核心技术,它通过在共享资源上实施访问限制,确保数据的一致性和完整性。作为PHP高级开发者,深入理解MySQL锁机制对于构建高性能、高并发的Web应用至关重要。
二、 MySQL锁分类体系
1. 按锁的粒度划分
1. 表级锁
特点
- 开销小,加锁快
- 锁定整个表
- 并发度低 使用场景
-- 显式加表锁
LOCK TABLES users READ; -- 共享锁
LOCK TABLES users WRITE; -- 排他锁
-- 操作完成后释放
UNLOCK TABLES;
注意事项:
- MyISAM引擎默认使用表锁
- 在InnoDB上应谨慎使用,会严重影响并发
2. 行级锁
特点
- 开销大,加锁慢
- 只锁定需要的行
- 并发度高 使用场景
-- 共享锁(S锁)
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;
-- MySQL 8.0+推荐语法
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
-- 排他锁(X锁)
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
注意事项:
- MyISAM引擎默认使用表锁
- 在InnoDB上应谨慎使用,会严重影响并发
2. 按锁的性质划分
1. 共享锁(S锁)
- 允许多个事务同时读取
- 阻止其他事务获取排他锁
2. 排他锁(X锁)
- 独占锁,阻止其他任何锁
- 自动加于UPDATE/DELETE操作
三、InnoDB高级锁机制
1. 意向锁(Intention Locks)
作用: 快速判断表中是否有行被锁定,避免全表扫描
- IS锁:意向共享锁
- IX锁:意向排他锁
2. 记录锁(Record Locks)
锁定索引中的特定记录:
-- 锁定id=5的记录
SELECT * FROM products WHERE id = 5 FOR UPDATE;
3. 间隙锁(Gap Locks)
锁定索引记录间的间隙,防止幻读:
-- 锁定id在10到20之间的间隙
SELECT * FROM products WHERE id BETWEEN 10 AND 20 FOR UPDATE;
4. 临键锁(Next-Key Locks)
InnoDB默认行锁算法,组合了记录锁和间隙锁:
- 锁定记录及之前的间隙
- 解决幻读问题的关键
5. 插入意向锁(Insert Intention Locks)
特殊间隙锁,允许不同事务在相同间隙插入不同记录,提高并发插入性能。
四、 锁的应用实践(PHP)
1. 悲观锁实现
// PHP中使用悲观锁示例
$pdo->beginTransaction();
try {
// 1. 锁定账户记录
$stmt = $pdo->prepare('SELECT * FROM accounts WHERE user_id = ? FOR UPDATE');
$stmt->execute([$userId]);
$account = $stmt->fetch();
// 2. 检查余额
if ($account['balance'] < $amount) {
throw new Exception('余额不足');
}
// 3. 更新余额
$update = $pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE user_id = ?');
$update->execute([$amount, $userId]);
// 4. 记录交易
$insert = $pdo->prepare('INSERT INTO transactions (...) VALUES (...)');
$insert->execute([...]);
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
// 错误处理
}
2. 乐观锁实现
// 使用版本号实现乐观锁
$pdo->beginTransaction();
try {
// 1. 获取当前版本
$stmt = $pdo->prepare('SELECT id, balance, version FROM accounts WHERE id = ?');
$stmt->execute([$accountId]);
$account = $stmt->fetch();
// 2. 业务处理
$newBalance = $account['balance'] - $amount;
// 3. 带版本检查的更新
$update = $pdo->prepare('UPDATE accounts SET balance = ?, version = version + 1
WHERE id = ? AND version = ?');
$affected = $update->execute([$newBalance, $accountId, $account['version']]);
if ($affected === 0) {
throw new Exception('并发修改冲突');
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
// 错误处理
}
五、锁的监控与优化
1. 锁等待监控
-- 查看当前锁等待情况
SHOW ENGINE INNODB STATUS;
-- 查看锁等待统计
SHOW STATUS LIKE 'innodb_row_lock%';
-- 查看正在执行的SQL和锁信息
SELECT * FROM information_schema.INNODB_TRX;
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
2. 性能优化建议
- 索引优化:确保查询使用适当的索引,避免全表扫描导致表锁
- 事务设计:
- 尽量缩短事务长度
- 避免在事务中进行耗时操作(如网络请求)
- 隔离级别选择:根据业务需求选择合适的事务隔离级别
- 死锁预防:
- 按固定顺序访问多表
- 使用NOWAIT或SKIP LOCKED(MySQL 8.0+)
SELECT * FROM table FOR UPDATE NOWAIT; SELECT * FROM table FOR UPDATE SKIP LOCKED;
六、常见问题解决方案
1. 死锁处理
当检测到死锁时,InnoDB会自动回滚代价较小的事务。开发者应:
- 实现重试机制
- 分析死锁日志优化业务逻辑
2. 锁等待超时
-- 设置锁等待超时时间(秒)
SET innodb_lock_wait_timeout = 50;
3. 高并发场景优化
- 考虑使用Redis等缓存层减少数据库压力
- 对于计数器等场景可使用原子操作
UPDATE counters SET value = value + 1 WHERE id = 1;
七、Laravel ORM 中 MySQL 锁的应用
1.悲观锁在 Laravel 中的应用
1. 共享锁 (Shared Lock)
应用场景:读取数据时允许其他事务也读取,但阻止写入操作,适用于读多写少的场景。
// 使用 sharedLock() 方法
$products = Product::where('category_id', 5)
->sharedLock()
->get();
// 等同于原生SQL: SELECT * FROM products WHERE category_id = 5 LOCK IN SHARE MODE
实际案例:生成报表时需要确保数据在读取过程中不被修改。
2. 排他锁 (Exclusive Lock)
应用场景:需要修改数据时使用,阻止其他事务读取或写入。
// 使用 lockForUpdate() 方法
$product = Product::where('stock', '>', 0)
->lockForUpdate()
->first();
if ($product) {
$product->decrement('stock');
$product->save();
}
// 等同于原生SQL: SELECT * FROM products WHERE stock > 0 LIMIT 1 FOR UPDATE
实际案例:库存扣减、账户余额变更等需要原子性操作的场景。
2. 事务中的锁使用
Laravel 提供了简洁的事务处理机制,结合锁使用可以确保数据一致性
DB::transaction(function () {
$account = Account::where('user_id', 123)
->lockForUpdate()
->first();
if ($account->balance >= 100) {
$account->balance -= 100;
$account->save();
Payment::create([
'account_id' => $account->id,
'amount' => 100
]);
}
});
最佳实践:
- 尽量缩短事务执行时间
- 锁应该尽早获取
- 按照固定顺序获取多个锁以避免死锁
3. 高级锁策略
1. 悲观锁的变体 (MySQL 8.0+)
应用场景:高并发环境下优化锁行为。
// 跳过被锁定的行
$products = Product::where('category_id', 5)
->skipLocked()
->get();
// 等同于: SELECT * FROM products WHERE category_id = 5 SKIP LOCKED
// 不等待立即返回
$product = Product::where('id', 10)
->lockForUpdate()
->noWait()
->first();
// 等同于: SELECT * FROM products WHERE id = 10 FOR UPDATE NOWAIT
实际案例:秒杀系统中处理高并发请求时。
2. 乐观锁实现
**应用场景:**读多写少且冲突较少的场景。 在模型中使用版本号:
Schema::table('products', function (Blueprint $table) {
$table->integer('version')->default(0);
});
$product = Product::find(1);
// 模拟其他进程修改了数据
Product::where('id', 1)->update(['price' => 200, 'version' => DB::raw('version + 1')]);
try {
$product->price = 150;
$product->save();
} catch (\Illuminate\Database\QueryException $e) {
// 捕获乐观锁冲突
if (str_contains($e->getMessage(), 'version conflict')) {
// 处理冲突
}
}
自定义乐观锁实现:
public function save(array $options = [])
{
if ($this->exists) {
$affected = DB::table($this->getTable())
->where($this->getKeyName(), $this->getKey())
->where('version', $this->version)
->update([
'price' => $this->price,
'version' => DB::raw('version + 1'),
'updated_at' => $this->freshTimestamp(),
]);
if ($affected === 0) {
throw new OptimisticLockException('数据已被其他进程修改');
}
return true;
}
return parent::save($options);
}
4. 锁的应用场景详解
1. 电商库存管理
DB::transaction(function () use ($productId, $quantity) {
$product = Product::where('id', $productId)
->where('stock', '>=', $quantity)
->lockForUpdate()
->firstOrFail();
$product->decrement('stock', $quantity);
Order::create([
'product_id' => $productId,
'quantity' => $quantity
]);
});
2. 财务系统转账
try {
DB::transaction(function () use ($from, $to, $amount) {
$fromAccount = Account::where('id', $from)
->lockForUpdate()
->firstOrFail();
$toAccount = Account::where('id', $to)
->lockForUpdate()
->firstOrFail();
if ($fromAccount->balance < $amount) {
throw new InsufficientFundsException();
}
$fromAccount->decrement('balance', $amount);
$toAccount->increment('balance', $amount);
Transaction::create([
'from_account' => $from,
'to_account' => $to,
'amount' => $amount
]);
});
} catch (\Exception $e) {
Log::error('转账失败: '.$e->getMessage());
return back()->with('error', '操作失败,请重试');
}
3. 分布式任务处理
$task = Task::where('status', 'pending')
->skipLocked()
->lockForUpdate()
->first();
if ($task) {
$task->update(['status' => 'processing']);
// 处理任务...
$task->update(['status' => 'completed']);
}
5. 性能优化与陷阱规避
1. 常见性能问题
- 锁范围过大:
// 不好的做法 - 锁定了整个表 Product::lockForUpdate()->get(); // 好的做法 - 只锁定需要的行 Product::where('id', 1)->lockForUpdate()->first(); - 长事务问题:
DB::transaction(function () { $data = Product::lockForUpdate()->first(); // 这里执行了耗时操作(如API调用) // 应该把耗时操作移到事务外 });
2. 锁监控与诊断
// 记录慢查询和锁等待
DB::listen(function ($query) {
if ($query->time > 100) { // 超过100ms的查询
Log::channel('slow_queries')->info('Slow query', [
'sql' => $query->sql,
'bindings' => $query->bindings,
'time' => $query->time
]);
}
});
3. 最佳实践总结
- 索引优化:确保锁操作使用索引,避免全表扫描
// 好的做法 - 使用索引列 Product::where('id', 1)->lockForUpdate()->first(); // 不好的做法 - 无索引会导致表锁 Product::where('name', 'Laptop')->lockForUpdate()->first(); - 锁顺序:按照固定顺序获取锁避免死锁
// 正确的顺序 $accountA = Account::where('id', 1)->lockForUpdate()->first(); $accountB = Account::where('id', 2)->lockForUpdate()->first(); // 错误的顺序(可能导致死锁) // 事务1: 锁1然后锁2 // 事务2: 锁2然后锁1 - 隔离级别:根据业务需求设置合适的事务隔离级别
config(['database.connections.mysql.isolation' => 'REPEATABLE READ']);