一、乐观锁(Optimistic Locking)
乐观锁(Optimistic Locking)是一种并发控制的机制,它的核心思想是:假设在大多数情况下数据库不会发生数据冲突,所以只有在进行更新(UPDATE)数据时才会检查数据是否被其他事务修改过。如果发生数据冲突,则回滚当前事务的操作并重试。
1. 乐观锁的核心特点
1.1 不依赖数据库锁
和悲观锁(如SELECT...FOR UPDATE)不同,悲观锁在读取数据时会锁定数据。而乐观锁在读取数据时则不会锁定数据,只有在更新(UPDATE)前,才会检查数据是否被修改过。
1.2 基于版本号或时间戳实现
乐观锁通常通过添加一个版本号字段(version)或时间戳字段(update_time)来实现。每次更新数据时,会检查版本号和读取数据时是否一致。如果一致;则更新数据并递增版本号;如果不一致,则判断为数据冲突,更新数据失败。
1.3 适用多读少写的业务场景
根据乐观锁的核心思想:假定大多数情况下不会发生数据冲突,所以不需要频繁加锁,适合多读少写、冲突较少的业务场景。
2. 乐观锁的实现
2.1 版本号version
-
在表中添加版本号字段,并初始化为1
-
每次更新数据时,检查当前版本号是否和读取数据时的一致。一致则更新并递增版本号;不一致则更新失败。
UPDATE user
SET name = 'new_name', version = version + 1
WHERE id = 1 AND version = 1;
2.2 时间戳(TimeStamp)
-
类似于版本号字段,在表中添加字段(last_update_time),记录最后更新时间。
-
当进行数据更新时,检查时间戳是否和查询时一致,一致则更新数据;否则更新失败。
二、悲观锁(Pessimistic Locking)
悲观锁(Pessimistic Locking)是一种并发控制机制,它的核心思想是:假设在访问数据时会发生冲突,所以在操作数据之前会先加锁,防止其它事务修改数据。它通过加锁,确保当前事务独占资源,直到事务提交或事务回滚后才释放锁。
1. 悲观锁的核心特点
1.1 先加锁,再操作:
在读取数据时,直接对数据加锁(如 SELECT ... FOR UPDATE
),其他事务无法修改被锁定的数据,直到当前事务完成。
1.2 依赖数据库锁机制:
通过数据库自身的锁功能(如行锁、表锁)实现,例如 MySQL 的 InnoDB
引擎支持行级锁。
1.3 适用于高冲突场景:
当预期数据会被频繁修改(写多读少)时,悲观锁能有效避免冲突,但可能降低并发性能。
2. 悲观锁的实现
2.1 数据库行级锁(常用)
-
使用
SELECT ... FOR UPDATE
语句锁定数据行,其他事务无法修改这些行,直到当前事务提交。
BEGIN TRANSACTION;
SELECT * FROM product WHERE id = 1 FOR UPDATE; -- 锁定 id=1 的行
UPDATE product SET stock = stock - 1 WHERE id = 1;
COMMIT;
2.2 表级锁
-
使用
LOCK TABLES
锁定整个表,其他事务无法操作该表(慎用,性能差)。
LOCK TABLES product WRITE; -- 锁定表
UPDATE product SET stock = 9 WHERE id = 1;
UNLOCK TABLES; -- 解锁
3. 注意事项
-
避免长事务: 长时间持有锁会增加死锁风险,并降低系统吞吐量。
-
锁粒度控制: 尽量使用行级锁而非表级锁,减少对其他事务的影响。
-
死锁处理: 通过按固定顺序加锁、设置超时时间(如 MySQL 的
innodb_lock_wait_timeout
)等方式规避。
三、分布式锁(Distributed Lock)
分布式锁(Distributed Lock) 是一种在分布式系统中协调多个进程或服务对共享资源进行互斥访问的机制。它的核心目标是:在多个独立的服务或节点之间,确保同一时间只有一个客户端能持有锁,从而安全地操作共享资源(如数据库、文件、缓存等)。
1. 为什么需要分布式锁
在单机系统中,可以使用本地锁(如 Java 的 synchronized
或 ReentrantLock
)控制并发。但在分布式系统中,多个服务实例或节点可能同时操作同一资源(例如电商系统中的库存扣减),此时需要一种跨进程、跨服务器的锁机制来保证数据一致性。
典型场景:
-
库存扣减:多个服务节点同时处理订单,需确保同一商品库存不会被超卖。
-
订单创建:防止重复下单。
-
配置更新:确保配置修改时只有一个节点生效。
2. 分布式锁的核心特点
-
互斥性:同一时刻只能有一个客户端持有锁。
-
可重入性:同一客户端可以多次获取同一把锁(避免死锁)。
-
锁超时:防止客户端崩溃后锁无法释放(需设置合理的超时时间)。
-
高可用:锁服务本身需高可用,避免单点故障。
-
容错性:网络分区或节点宕机时仍能保证锁的正确性。
3. 分布式锁的实现
1. 基于数据库
-
实现原理:通过数据库的唯一约束或乐观锁机制实现。
-- 创建锁表 CREATE TABLE distributed_lock (lock_key VARCHAR(64) PRIMARY KEY,client_id VARCHAR(64),expire_time TIMESTAMP ); -- 获取锁(插入记录) INSERT INTO distributed_lock (lock_key, client_id, expire_time) VALUES ('order_lock', 'client_1', NOW() + INTERVAL 10 SECOND) ON DUPLICATE KEY UPDATE client_id = VALUES(client_id), expire_time = VALUES(expire_time); -- 释放锁(删除记录) DELETE FROM distributed_lock WHERE lock_key = 'order_lock' AND client_id = 'client_1';
-- 创建锁表
CREATE TABLE distributed_lock (lock_key VARCHAR(64) PRIMARY KEY,client_id VARCHAR(64),expire_time TIMESTAMP
);
-- 获取锁(插入记录)
INSERT INTO distributed_lock (lock_key, client_id, expire_time)
VALUES ('order_lock', 'client_1', NOW() + INTERVAL 10 SECOND)
ON DUPLICATE KEY UPDATE client_id = VALUES(client_id), expire_time = VALUES(expire_time);
-- 释放锁(删除记录)
DELETE FROM distributed_lock WHERE lock_key = 'order_lock' AND client_id = 'client_1';
2. 基于Redis
-
实现原理:利用 Redis 的
SETNX
(SET if Not Exists)命令或 RedLock 算法。
# 获取锁(设置过期时间)
SET lock_key client_1 NX EX 10
# 释放锁(Lua 脚本保证原子性)
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock_key client_1
3. 基于Zoopkeeper
-
实现原理:利用 ZooKeeper 的临时有序节点(Ephemeral Sequential Node)和 Watcher 机制。
-
步骤:
-
客户端在指定路径下创建临时有序节点(如
/locks/lock_0001
)。 -
客户端获取所有子节点,判断自己是否是最小节点。若是,则获得锁。
-
其他客户端监听前一个节点的删除事件,等待锁释放。
-