在多线程并发的情况下,单个节点内的线程安全可以通过synchronized关键字和Lock接口来保证。
当开发的应用程序处于一个分布式的集群环境中,涉及到多节点,多进程共同完成时,如何保证线程的执行顺序是正确的。比如在高并发的情况下,很多企业都会使用Nginx反向代理服务器实现负载均衡的目的,这个时候很多请求会被分配到不同的Server上,一旦这些请求涉及到对统一资源进行修改操作时,就会出现问题,这个时候在分布式系统中就需要一个全局锁实现多个线程(不同进程中的线程)之间的同步。
常见的处理办法有三种:数据库、缓存、分布式协调系统。数据库和缓存是比较常用的,但是分布式协调系统是不常用的。
常用的分布式锁的实现包含:
Redis分布式锁、Zookeeper分布式锁、Memcached
基于 Redis 做分布式锁
Redis提供的三种方法:
(1)锁 SETNX:只在键 key
不存在的情况下, 将键 key
的值设置为 value
。若键 key
已经存在, 则 SETNX
命令不做任何动作。SETNX
是『SET if Not eXists』(如果不存在,则 SET)的简写。命令在设置成功时返回 1
, 设置失败时返回 0
。
redis> SETNX job "programmer" # job 设置成功 (integer) 1 redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败
(2)解锁 DEL:删除给定的一个或多个 key
。
(3)锁超时 EXPIRE: 为给定 key
设置生存时间,当 key
过期时(生存时间为 0
),它会被自动删除。
每次当一个节点想要去操作临界资源的时候,我们可以通过redis来的键值对来标记一把锁,每一进程首先通过Redis访问同一个key,对于每一个进程来说,如果该key不存在,则该线程可以获取锁,将该键值对写入redis,如果存在,则说明锁已经被其他进程所占用。具体逻辑的伪代码如下:
try{ if(SETNX(key, 1) == 1){ //do something ...... }finally{ DEL(key); }
但是此时,又会出现问题,因为SETNX和DEL操作并不是原子操作,如果程序在执行完SETNX后,而并没有执行EXPIRE就已经宕机了,这样一来,原先的问题依然存在,整个系统都将被阻塞。
幸亏Redis又提供了SET key value timeout NX方法,可以以原子操作的方式完成SETNX和EXPIRE的操作。此时只需如下操作即可。
try{ if(SET(key, 1, 30, timeout, NX) == 1){ //do something ...... } }finally{ DEL(key); }
解决了原子操作,仍然还有一点需要注意,例如,A节点的进程获取到锁的时候,A进程可能执行的很慢,在do something未完成的情况下,30秒的时间片已经使用完,此时会将该key给深处掉,此时B进程发现这个key不存在,则去访问,并成功的获取到锁,开始执行do something,此时A线程恰好执行到DEL(key),会将B的key删除掉,此时相当于B线程在访问没有加锁的临界资源,而其余进程都有机会同时去操作这个临界资源,会造成一些错误的结果。对于该问题的解决办法是进程在删除key之前可以做一个判断,验证当前的锁是不是本进程加的锁。
String threadId = Thread.currentThread().getId() try{ if(SET(key, threadId, 30, timeout, NX) == 1){ //do something ...... } }finally{ if(threadId.equals(redisClient.get(key))){ DEL(key); } }
上面的改进虽然解决锁被不同的进程释放的危险,但并没有解决获取到锁的进程在指定的时间内未完成do something操作(上面的代码还有一点小问题,就是判断操作和释放锁是两个独立的操作,不具备原子性。假设线程A判断完确实是自己加的锁 , 这时还没del ,这时有效的时间用完了 , 紧接着线程B又马上抢到了锁 , 然后线程A才执行del命令 , 就会把B抢到的锁给误删了),使得卡住的进程有可能与后来的进程同时同问临界资源,而出现问题,因此一旦某个进程无法在超时时间内完成对临界资源的操作,就需要延长超时的时间。此时可以启动一个守护进程,监视指定时间内获取锁的进程是否完成操作,如果没有,则添加超时时间,让程序继续执行。
String threadId = Thread.currentThread().getId() try{ if(SET(key, threadId, 30, timeout, NX) == 1){ new Thread(){ @Override public void run() { //start Daemon } } //do something ...... } }finally{ if(threadId.equals(redisClient.get(key))){ DEL(key); } }
基于以上的分析,基本上可以通过Redis实现一个分布式锁,如果我们想提升该分布式的性能,我们可以对连接资源进行分段处理,将请求均匀的分布到这些临界资源段中,比如一个买票系统,我们可以将100张票分为10 部分,每部分包含10张票放在其他的服务节点上,这些请求可以通过Nginx被均匀的分散到这些处理节点上,可以加快对临界资源的处理。
参考资料
B站视频上一部分讲解