比如某个应用需要定时跑一些任务,为了保证高可用,做了三个节点的集群,后来发现集群中每个节点都跑了重复的任务。
首先创建一张表,用于存储锁标识的记录:
CREATE TABLE IF NOT EXISTS distribution_lock(
id BIGINT(20) UNSIGNED NOT NULL COMMENT '分布式锁id',
lock_name VARCHAR(50) DEFAULT '' COMMENT '分布式锁的名称',
node_number TINYINT(1) NOT NULL COMMENT '集群下节点的编号,用于重入锁',
gmt_create DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '行记录创建时间',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='存储分布式锁的表';
获取锁:
INSERT INTO distribution_lock(id, lock_name, node_number) VALUES(1001, 'order_task', 1);
插入成功表示获取锁,否是未获取锁;
获取重入锁:
SELECT
t0.id,
t0.lock_name,
t0.gmt_create
FROM distribution_lock t0
WHERE t0.id = 1001 AND t0.node_number = 1;
检查锁超时:
SELECT
t0.id,
t0.lock_name,
t0.gmt_create ,
TIMESTAMPDIFF(SECOND, t0.gmt_create, NOW()) 'duration_time'
FROM distribution_lock t0
WHERE t0.id = 1001 AND t0.node_number != 2;
释放锁:
DELETE FROM distribution_lock WHERE id = 1001 AND t0.node_number = 1;
上面是用
INSERT
+SELECT
+DELETE
来实现的,当然也可以使用UPDATE
+SELECT
实现(注意初始化锁标识的行记录)。
因为 id
设置为 主键,所以具有唯一约束属性,同一时刻只能被一个线程获取锁🔐。
注意问题:
- 单点故障: 数据库存在单点故障的风险,建议做成高可用版本;
- 重入性: 也就是重入锁,可以精确到线程级别;
- 锁失效机制: 如果一台服务获得锁后,再没有主动释放锁的情况下,其他服务也就无法获取锁。解决方法是获取锁时,判断锁是否超时;
- 非阻塞性: 比如一个定时任务每月跑一次,恰巧此时被其他节点服务占用锁,导致该节点获取锁失败,解决方法是循环多次获取锁;
- 性能: 基于数据库的性能比较低,不适合高并发场景。
获取锁
127.0.0.1:6379> SETNX lock:1001 uuid
巧用 SETNX
命令,其中 value
为唯一标识,比如 ip+线程id+时间戳
、 UUID
诸如此类,如果返回 1
表示获取锁成功,否则获取锁失败;
设置锁超时时间
127.0.0.1:6379> expire lock:1001 5
如果返回 1
表示设置锁超时成功,否则失败;
127.0.0.1:6379> SET lock:1001 uuid EX 5 NX
锁过期检查
127.0.0.1:6379> GET lock:1001
如果获得的 value
和之前设置的 value
一致,说明锁有效,业务提交;否则锁过期,业务回滚;
释放锁 Redis的事务不像MySQL的事务那样强大,所以释放锁时要防止误删 key
,使用Lua脚本来操作:
/**
* 释放分布式锁
*
* @param key key
* @param value value
* @return 主动成功释放锁返回 true,否则返回false
*/
public boolean release(String key, String value) {
// Null check
Assert.notNull(key, "The key is not allowed to be null");
Assert.notNull(value, "The value is not allowed to be null");
// result 表示是否删除指定的 key,1表示是,0表示否
long result = 0;
try {
String lua = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then \n" +
" return redis.call(\"del\",KEYS[1])\n" +
" else \n" +
" return 0\n" +
" end";
result = (Long) jedis.evalsha(jedis.scriptLoad(lua),
Collections.singletonList(key),
Collections.singletonList(value));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
try {
jedis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return result == 1;
}
获取锁
// TODO
释放锁
// TODO