对于多线程并发,可以使用锁定来确保一个代码块同时只能由一个线程访问。 例如Java的同步关键字或Reentrantlock类等。
这可以确保同一JVM进程中的多个线程同时运行。
在分布式群集环境中,如何确保不同节点的线程同步运行?
如何在分布式系统中实现不同线程对代码和资源的并发访问?
在单进程并发方案中,可以使用语言和类库提供的锁定。 分布式场景可以使用分布式锁定。
那么,如何实现分布式系统的锁定呢?
分布式锁有很多中实现方法,我们来简单列举一下。
分布式锁的实现有哪些?
1.Memcached分布式锁
Memcached的add命令此命令是原子操作,表示add仅在key不存在时成功,线程已锁定。
2.Redis分布式锁
与Memcached的方式相似,Redis的setnx命令该命令也是原子操作,只有在key不存在时set才成功。 (setnx命令不完整,稍后将介绍替代方案。)
3.Zookeeper分布式锁
利用Zookeeper的顺序临时节点实现分布式锁定和队列。 Zookeeper是为实现分布式锁定服务而设计的。
首先谈谈Redis的分布式锁定。 这种实现方式很有代表性。
如何用Redis实现分布式锁?
Redis分布式锁的基本流程不容易理解,但要写得完美并不是那么容易。 在这里,首先需要了解分布式锁实现的三个主要要素。
1.加锁
最简单的方法是使用setnx命令。 key是钥匙的唯一标志,由业务决定命名。 例如,如果想要锁定某个商品的秒杀事件,key可以被命名为“lock_sale_商品ID”。 value设置为什么? 锁定的值是随机生成的UUID。 我们可以先设定为1。 锁定的伪代码如下:
setnx(key,1 )如果一个线程运行setnx并返回1,则key原本就不存在,表示线程已成功锁定。 如果线程执行setnx并返回0,则表示key已存在,并且线程解锁失败。
2.解锁
有锁的话就需要解锁。 锁定线程完成任务后,必须解锁该任务,以便其他线程可以访问。 解锁的最简单方法是运行del命令。 伪代码如下。
当Del(key )解除锁定时,其他线程可以继续运行setnx命令获取锁定。
3.锁超时
锁定超时是什么意思? 如果锁定的线程在执行任务时锁定,无法显式解锁,则此资源将被永久锁定,其他线程不应再进入。
因此,即使没有明确释放setnx的密钥,锁定也会在一定时间后自动释放,为了避免死锁,必须设置second单位的超时时间。 setnx不支持超时参数,因此需要额外的指令。 伪代码如下。
综合expire(key,30 ),我们的分布式锁定实现的第一版伪代码如下。
if(setNX(key,1 )==1) expire (key,30 ) try (do something……) finally (del ) key ) }上的伪代码是分布式锁定的简单实现,实际响应
1.setnx和expire的非原子性
想象一下极端的情景。 假设一个线程运行了setnx,并且锁定成功。
setnx刚成功运行,还没有时间运行expire命令,节点1 Duang突然中断。
这样,该锁定就没有过期,成为“长生不老”,另一个线程将无法获得锁定。
怎么解决? setnx指令本身不支持来电超时时间。 幸运的是,redis2.6. 12或更高版本在set指令中添加了可选参数,伪代码如下:
set(key,1,30,NX )这样就代替了setnx指令。
2.del 导致误删
此外,这是一个极端场景,其中一个线程成功锁定,设置的超时时间为30秒。
如果线程a因为任何原因运行缓慢,并且30秒后仍无法运行,则锁定将过期并自动释放
线程B得到了锁。随后,线程A执行完了任务,线程A接着执行del指令来释放锁。但这时候线程B还没执行完,线程A实际上删除的是线程B加的锁。
怎么避免这种情况呢?可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁。
至于具体的实现,可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。
加锁:
String threadId = Thread.currentThread().getId()set(key,threadId ,30,NX)解锁:
if(threadId .equals(redisClient.get(key))){ del(key)}也可以在释放锁的时候,通过锁的默认value值UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
但是,这样做又隐含了一个新的问题,判断和释放锁是两个独立操作,不是原子性的。
要想实现验证和删除过程的原子性,可以使用Lua脚本来实现。这样就能保证验证和删除过程的正确性了。
3. 出现并发的可能性
还是刚才第二点所描述的场景,虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然是不完美的。
怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。
当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。
当线程A执行完任务,会显式关掉守护线程。
另一种情况,如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。
关于Redis分布式锁的内容就介绍到这里啦。
参考:程序员zjdby—《什么是分布式锁》