在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等,那具体什么是分布式锁,分布式锁应用在哪些业务场景,如何来实现
问: 多线程并发的时候,如何实现对共享资源安全的访问
答:通过锁来实现
问:单机多进程多线程的时候,如何实现对共享资源安全的访问
答:通过共享内存、管道等实现
问: 多机多进程的时候,如何实现对共享资源安全的访问
答:通过分布式锁
我们在开发的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的锁进行处理,并且可以完美的运行,毫无bug
注意这是单机应用,后来业务发展,需要做集群,一个应用需要部署到几台机器上然后做负载均衡,大致如下图:
上图可以看到,
如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题!
分布式锁,顾名思义,就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源
思路是:在整个系统提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。至于这个“东西”,可以是Redis、Zookeeper,也可以是数据库。
在实现分布式锁的三种实现方法之前,先了解下分布式锁应该具备哪些条件。
目前几乎所有大型网站以及应用都是分布式的,分布式场景中的数据一致性一致是一个比较重要的问题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大部分场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在系统可以接受的范围内既可了。
在很多场景中,我们为了保证数据的最终一致性,需要很多技术方案来支持,比如分布式事务、分布式锁等。有时,我们需要保证一个方法在同一时间只能被同一个线程执行
基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含“方法名”等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法向表中插入数据,成功插入则获取锁,执行完成之后删除对应的行数据释放锁
(1)创建一个表:
DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
`desc` varchar(255) NOT NULL COMMENT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName');
因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
(3)成功插入则获取锁,执行完成后删除对应的行数据释放锁:
delete from method_lock where method_name ='methodName';
注意:这只是使用基于数据库的一种方法,使用数据库实现分布式锁还有很多其他的玩法!
使用基于数据库的这种实现方式很简单,但是对于分布式锁应该具备的条件来说,它有一些问题需要解决及优化:
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。
优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。
分布式锁的本质其实就是要在 Redis 里面占一个“坑”,当别的进程也要来占时,发现已经有人蹲了,就只好放弃或者稍做等待。
占坑一般使用setnx
指令,只允许被一个客户端占坑。先来先占,用完了,再调用del
指令释放“坑”
(1)SETNX
SETNX key val:
(2)expire
(3)delete
在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。
127.0.0.1:6379> setnx lock:codehole true
(integer) 1
127.0.0.1:6379> expire lock:codehole 5
(integer) 1
// ---- do sonmething ---
127.0.0.1:6379> del lock:codehole
实现思想:
简单实现代码:
#连接redis
redis_client = redis.Redis(host="localhost",
port=6379,
password=password,
db=10)
#获取一个锁
lock_name:锁定名称
acquire_time: 客户端等待获取锁的时间
time_out: 锁的超时时间
def acquire_lock(lock_name, acquire_time=10, time_out=10):
"""获取一个分布式锁"""
identifier = str(uuid.uuid4())
end = time.time() + acquire_time
lock = "string:lock:" + lock_name
while time.time() < end:
if redis_client.setnx(lock, identifier):
# 给锁设置超时时间, 防止进程崩溃导致其他进程无法获取锁
redis_client.expire(lock, time_out)
return identifier
elif not redis_client.ttl(lock):
redis_client.expire(lock, time_out)
time.sleep(0.001)
return False
#释放一个锁
def release_lock(lock_name, identifier):
"""通用的锁释放函数"""
lock = "string:lock:" + lock_name
pip = redis_client.pipeline(True)
while True:
try:
pip.watch(lock)
lock_value = redis_client.get(lock)
if not lock_value:
return True
if lock_value.decode() == identifier:
pip.multi()
pip.delete(lock)
pip.execute()
return True
pip.unwatch()
break
except redis.excetions.WacthcError:
pass
return False
测试刚才实现的分布式锁
def seckill():
identifier=acquire_lock('resource')
print(Thread.getName(),"获得了锁")
release_lock('resource',identifier)
for i in range(50):
t = Thread(target=seckill)
t.start()
但是上面逻辑还是会有问题,如下图,如果在setnx和expire之间服务器进程突然死掉了,就会导致expire得不到执行,导致死锁
这个问题的根源在于setnx和expire的两条指令不是原子指令。如果这两条指令可以一起执行就不会出现问题。为此在redis2.8版本中引入了set 指令的参数,使得setnx和expire指令可以一起执行
127.0.0.1:6379> set lock:codehole true ex 5 nx
(integer) 1
// ---- do sonmething ---
127.0.0.1:6379> del lock:codehole
场景分析
依赖分布式锁的机制,某个用户操作redis时对应的客户端宕机了,而且此时已经获取到了锁,导致锁一直被持有,其他客户端拿不到锁,这就是死锁,如何解决呢?
解锁方案
setnx lock-key 001 # 设置分布式锁
expire lock-key second # 设置单位为秒
pexpire lock-key milliseconds # 设置单位为毫秒
set lock-key value NX PX 毫秒数
# 比如为key为name设置分布式锁
set lock-name 001 NX PX 5000
实际开发中如何知道设置多少时间才合适
由于操作通过都是微秒或者毫秒级,因此锁设定时间不宜过大。具体时间需要业务测试后确定
比如:持有锁的操作最长执行时间127ms,最短执行时间7ms。
流程图
问题
del
指令来释放锁。但这时候线程 B 还没执行完,线程A实际上 删除的是线程 B 加的锁。解决
我们应该在删除锁之前, 判断这个锁是否是自己设置的锁, 如果不是(例如自己的锁已经超时释放), 那么就不要删除了.
怎么知道当前加锁的是不是自己呢?
但是,这样做又隐含了一个新的问题,判断和释放锁是两个独立操作,不是原子性。解决:用 lua 脚本做验证标识和解锁操作。
tag = random.nextint() $ 随机数
if redis.set(key, tag, nx = true, ex = 5):
do_something();
redis.delifequals(key, tag);
问题
解决
场景分析
就是实际开发过程中,一端代码内部会有嵌套方法,外层方法获取到锁之后,内层再去获取锁时由于锁已经存在了就无法获取了,但内层代码不执行完外层也释放不了锁啊,这就是方法嵌套导致的死锁问题,怎么解决呢?
解决方案
hset lock-key 线程信息, 重入次数(默认1) NX PX 毫秒数
key: lock-key
value-key:线程信息
value-value:重入次数
流程图
下面我们假设锁的key为“lock”,hashKey是当前线程的id:“threadId”,锁自动释放时间假设为20
获取锁的步骤:
1、判断lock是否存在 EXISTS lock
返回1则存在,说明有人获取锁了,下面判断是不是自己的锁
返回0则不存在,说明可以获取锁,并存入value值HSET key threadId 1
2、设置锁自动释放时间,EXPIRE lock 20、
释放锁的步骤:
1、判断当前线程id作为hashKey是否存在:HEXISTS lock threadId
2、判断重入次数是否为0:
问题
上述命令执行都是立即返回的,如果客户端可以等待锁释放就无法使用。
如果客户端在处理请求时加锁没加成功怎么办?一般有三种策略
(1)直接抛出特定类型的异常
这种方式比较适合由用户直接发起的请求。
用户看到错误对话框之后,会先阅读对话框的内容,在点击重试,这样就可以起到人工延迟的效果
如果考虑到用户体验,可以由前端的代码替代用户来进行延迟控制控制
它本质是对当前请求的放弃,有用户决定是否重新发起新的请求
(2)sleep
(3) 延迟队列
上面版本的锁有一个很大的缺点,就是它加锁时只能作用在一个redis节点上,即使redis通过sentinel保证高可用,如果这个master由于某些原因发送了主从切换,那么就会出现锁丢失的情况:
正因为如此,Redis作者antirez基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock。这种方法比原先单节点的方法更加安全,它可以保证一下特性:
Redlock也是Redis所有分布式锁实现方式中唯一能让面试官高潮的方式。
多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击);
这里的场景是假设有一个redis cluster,有5个redis master实例。然后执行如下步骤获取一把锁
假设有5个redis节点,这些节点之间既没有主从,也没有集群关系。客户端用相同的key和随机值在5个节点上请求锁,请求锁的超时时间应小于锁自动释放时间。当在3个(超过半数)redis上请求到锁的时候,才算是真正获取到了锁。如果没有获取到锁,则把部分已锁的redis释放掉。
场景
具体发生的过程介绍如下:
1)redis的部署方式为一主多从
2)如果客户端1给redis主节点加了分布式锁,但是redis主节点在异步复制给从节点之前就挂了,导致分布式锁丢失
3)某个从节点自动成为新的主节点后,因为同步锁丢失的原因,并不知道客户端1已经加锁,这时客户端2也来了加锁行为,这时客户端2自然加锁成功!!!
问题的关键是,客户端1没有主动释放锁,也没有因为过期而释放锁,这时客户端2就能得到锁,这不是很大的问题吗?我们的分布式锁,在这种情况下,不是失去意义了吗???
解决方法:红锁
关键思想:
比如说有1个主节点、4个从节点,那么一共有5个节点,红锁加锁时,会同时往5个节点加锁,至少有3个节点加锁成功,才算最终加锁成功
如果加了红锁机制后,主从情况下,分布式锁刚加完,主节点挂掉,那么新的从节点大概率会持有分布式锁。
为什么会是大概率?因为可能当时红锁加锁也成功了,但是其实5节点中,有一个从节点挂掉了,所以改从节点没有分布式锁,但是之后又被实施人员启动了,这种时候,如果主节点挂掉,而刚启动起来的从节点又刚好被作为新的主节点,那么还有会有问题。
单机模式下用本地锁
分布式模式下用分布式锁