Linux c/c++ 高性能后端
锁与分布式数据库
分布式锁本质上是解决了分布式事务中隔离性问题
在分布式系统中,一个应用部署在多台机器当中,某些场景下,为了保证数据据一致性,要求同一时刻,同一任务只在一个节点上运行,即保证某个行为在同一时刻只能被一个线程执行;在单机单进程多线程环境,通过锁很容易做到,比如mutex、spinlock、信号量等;而在多机多进程环境中,此时就需要分布式锁来解决了
锁与无锁回顾
互斥锁(公平锁)、自旋锁(非公平锁):实现本次分布式系统的两个锁机制
// 互斥锁的使用:
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
// ....
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
读写锁:主要用于数据库中行锁
信号量IPC:同步类型的锁,pipe、FIFO、信号量、信号、消息队列、共享内存、socket
条件变量
无锁:原子变量、内存屏障
分布式系统

上图为基本实现,其中lock为锁资源R1-R6;S1-S4为不同的机器上的进程,通过网络获取lock资源;用于对数据库DB的读写操作等等
上述分布式锁,常见的实现方式有:基于mysql,基于缓存redis
分布式系统关注点
- 互斥性:同时只允许一个持锁对象(S1-S4)进入临界资源;其他待持锁对象要么等待,要么轮询检测是否能获取锁;
- 锁超时:允许持锁对象持锁最长时间;如果持锁对象宕机,需外力解除锁定,方便其他持锁对象获取锁;
- 高可用:锁存储位置若宕机,可能引发整个系统不可用;应有备份存储位置和切换备份存储的机制,从而确保服务可用;
- 容错性:若存储位置宕机,恰好锁丢失的话,是否能正确处理,raft一致性算法;
分布式锁的类型
- 重入锁和非重入锁:是否允许持锁对象再次获取锁;S1是个多线程环境,S1中的某个线程获取了R1锁,是否允许S1的其他线程获取R1锁
- 公平锁和非公平锁:如果同时争夺锁是否获取锁的几率一样?公平锁(互斥锁)通常通过阻塞队列排队来实现;非公平锁(自旋锁)通常通过不断尝试获取锁来实现;
数据库分布式锁的一般实现逻辑
- 锁是一种资源,需要存储;要保证可用性,避免锁失效
- 加锁对象和解锁对象必须为同一个
- 互斥语义;将锁打标记
- 加锁解锁行为是网络通信;需要锁超时
- 怎么获取持有锁对象释放锁?
- 主动探寻,非公平锁
- 被动通知:广播(非公平锁),排队单独通知(公平锁)
- 是否允许同一持锁对象多次加锁? 重入锁/非重入锁
MySQL实现分布式锁
主要利用MySQL唯一键的唯一性约束来实现互斥性
MySQL实现分布式锁是通过"创建锁表格"来实现,表中每条数据就是一个锁
相比用c/c++自己来实现的分布式系统,MySQL分布式数据库性能是否能满足使用场景是一个比较值得关注的点
表结构
DROP TABLE IF EXISTS `dislock`;
CREATE TABLE `dislock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', #非空唯一索引
`lock_type` varchar(64) NOT NULL COMMENT '锁类型', #自旋锁还是互斥锁,重入锁还是非重入锁
`owner_id` varchar(255) NOT NULL COMMENT '持锁对象', #记录R1-R4标识
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, #记录时间,解决锁超时问题
PRIMARY KEY (`id`),
UNIQUE KEY `idx_lock_type` (`lock_type`) #唯一键互斥
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT
CHARSET=utf8 COMMENT='分布式锁表';
加锁与解锁
解锁关键点:怎么确保不释放别人的锁? owner_id
INSERT INTO dislock (`lock_type`, `owner_id`) VALUES ('act_lock', 'ad2daf3'); #往dislock表中插入一行,即为加锁
DELETE FROM dislock WHERE `lock_type` = 'act_lock' AND `owner_id` = 'ad2daf3'; #删除自己加的(owner_id)锁
主动探寻加解锁代码
// 分布式锁的使用案例
while (1) {
CLock my_lock;
bool flag = dlm->Lock("foo", 100000, my_lock);
if (flag) {
printf("获取成功, Acquired by client name:%s, res:%s, vttl:%d\n",
my_lock.m_val, my_lock.m_resource, my_lock.m_validityTime);
// do resource job
sleep(80);
dlm->Unlock(my_lock);
// do other job
sleep(2);
return;
} else {
printf("获取失败, lock not acquired, name:%s\n", my_lock.m_val);
sleep(rand() % 3);
}
}
怎么实现锁超时检测?超进程,定时器定时检测这张表,拿当前时间减去update_time,如果超过最大持锁时间,删除那一行(释放锁)
怎么实现重入锁?dislock表里加一行count 为什么要实现重入锁:分布式业务上需要嵌套锁,即临界资源上还要加锁 count++,释放锁count--;
总结
可用性依赖数据库,若数据库是单点,挂掉将导致业务系统不可用;
还需额外实现锁失效的问题;解锁失败,其他线程将无法获得锁;
MySQL适合中小级别数据存储,分布式存储在速度和配置上还需要比较复杂的优化,分布式数据库推荐TiDB;
redis实现非公平锁(自旋锁)
redis 内存数据库、数据结构数据库、kv数据库
redis操作的数据都是在内存中,速度比较快;提供了丰富的数据结构list、hash、string、set、zset等等;通过k来操作value(list、hash、string...)来增删改查
那么如何实现redis的分布式锁呢?
mysql加锁通过sql表来实现的,redis是nosql数据库,通过相关命令来加减锁
redis分布式锁流程
- 尝试获取锁
- 获取锁成功,操作临界资源,操作结束尝试释放锁;
- 获取锁失败,订阅解锁信息;
- 尝试释放锁
- 释放锁成功,广播解锁信息;
- 释放锁失败(说明此时持有锁对象不是自己),获得锁失效时间;
加解锁等基础命令
set act_lock 123456 NX //加锁,NX: not exist,如果不存在k; set k v:k(act_lock) v(123456);如果k不存在返回OK加锁成功,如果存在返回nil加锁失败
del ack_lock //解锁,删除k为ack_lock的锁,返回(integer) 1代表成功
set ack_lock 123456 NX EX 10 //定时加锁10秒,EX: expire 到期,终止;成功返回OK
ttl ack_lock //查询锁剩余时间,ttl: time to life 锁的生命周期
get ack_lock //查询锁是否存在,不存在返回nil,存在返回value值,分布式redis中value的值为加锁进程的进程号
原子性问题
分布式中的原子性问题

以解锁为例:首先先获取key,判断val是否为自己家的锁,如果是再进行解锁;1.get act_lock 2.val == uuid 3.del act_lock;要实现1、2、3的原子性问题,否则数据会出问题,怎么解决?
Redis 的原子性保证机制
在 Redis 中,Lua 脚本是实现原子性操作的核心机制之一。其原理主要基于 Redis 的单线程模型和 Lua 脚本的执行方式,确保脚本内的多个命令作为一个整体执行;
单线程模型:Redis 使用单线程处理命令(6.0+ 版本引入多线程仅用于网络 I/O,核心逻辑仍单线程)。
天然隔离性:同一时间只有一个命令被执行,Lua 脚本执行期间,其他客户端命令必须等待。
Lua 脚本的原子性:脚本整体执行:Redis 将整个 Lua 脚本作为一个命令执行,脚本中的所有 Redis 操作(如 GET/SET/DEL)会被连续执行,中间不会插入其他客户端的命令。
无并发冲突:类似数据库的“事务隔离”,脚本内的逻辑看不到外部中间状态。
--[[
KEYS[1] lock_name
KEYS[2] uuid
]]
local uuid = redis.call("get", KEYS[1])
if uuid == KEYS[2] then
redis.call("del", KEYS[1])
end
redis加锁
--[[
KEYS[1] lock_name
KEYS[2] lock_channel_name
ARGV[1] lock_time (ms)
ARGV[2] uuid
]]
if redis.call('exists', KEYS[1]) == 0 then
redis.call('hset', KEYS[1], ARGV[2], 1)
redis.call('pexpire', KEYS[1], ARGV[1])
return
end
-- 若支持锁重入,将注释去掉
-- if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
-- redis.call('hincrby', KEYS[1], ARGV[2], 1)
-- redis.call('pexpire', KEYS[1], ARGV[1])
-- return
-- end
redis.call("subscribe", KEYS[2])
return redis.call('pttl', KEYS[1])
redis解锁
--[[
KEYS[1] lock_name
KEYS[2] uuid
]]
local uuid = redis.call("get", KEYS[1])
if uuid == KEYS[2] then
redis.call("del", KEYS[1])
end
--[[
KEYS[1] lock_name
KEYS[2] lock_channel_name
ARGV[1] 0 sign of unlock
ARGV[2] lock_expire_time
ARGV[3] uuid
]]
if redis.call('exists', KEYS[1]) == 0 then
redis.call('publish', KEYS[2], ARGV[1])
return 1
end
if redis.call('hexists', KEYS[1], ARGV[3]) == 0
then
return
end
-- 若支持锁重入,将注释去掉
-- local cnt = redis.call('hincrby', KEYS[1], ARGV[3], -1)
-- if cnt > 0 then
-- redis.call('pexpire', KEYS[1], ARGV[2])
-- return 0
-- else
redis.call('del', KEYS[1])
redis.call('publish', KEYS[2], ARGV[1])
return 1
-- end
总结
可用性依赖 redis 的可用性;
容错性很差,redis 采用的异步复制,数据可能丢失;
效率最高的一种分布式锁;
最安全的 redis 分布式锁:https://github.com/jacket-code/red
lock-cpp.git;
http://redis.cn/topics/distlock.html
浙公网安备 33010602011771号