在Redis上,可以通过对key值的独占来实现分布式锁,表面上看,Redis可以简单快捷通过set key这一独占的方式来实现,也有许多重复性轮子,但实际情况并非如此。
总得来说,Redis实现分布式锁,如何确保锁资源的安全&及时释放,是分布式锁的最关键因素。
如下逐层分析Redis实现分布式锁的一些过程,以及存在的问题和解决办法。

solution 1 :setnx

setnx命令设置key的方式实现独占锁

1,#并发线程抢占锁资源
setnx an_special_lock 1
2,#如果1抢占到当前锁,并发线程中的当前线程执行
if(成功获取锁)
  execute business_method()
  3,#释放锁
  del an_special_lock

存在的问题很明显:
从抢占锁,然后并发线程中当前的线程操作,到最后的释放锁,并不是一个原子性操作,
如果最后的锁没有被成功释放(del an_special_lock),也即2~3之间发生了异常,就会造成其他线程永远无法重新获取锁

solution 2:setnx + expire key

为了避免solution 1中这种情况的出现,需要对锁资源加一个过期时间,比如是10秒钟,一旦从占锁到释放锁的过程发生异常,可以保证过期之后,锁资源的自动释放

1,#并发线程抢占锁资源
setnx an_special_lock 1
2,#设置锁的过期时间
expire an_special_lock 10
3,#如果1抢占到当前锁,并发线程中的当前线程执行
if(成功获取锁)
  execute business_method()
  4,#释放锁
  del an_special_lock

通过设置过期时间(expire an_special_lock 10),避免了占锁到释放锁的过程发生异常而导致锁无法释放的问题,
但是仍旧存在问题:
在并发线程抢占锁成功到设置锁的过期时间之间发生了异常,也即这里的1~2之间发生了异常,锁资源仍旧无法释放
solution 2虽然解决了solution 1中锁资源无法释放的问题,但与此同时,又引入了一个非原子操作,同样无法保证set key到expire key的以原子的方式执行
因此目前问题集中在:如何使得设置一个锁&&设置锁超时时间,也即这里的1~2操作,保证以原子的方式执行?

solution 3 : set key value ex 10 nx

Redis 2.8之后加入了一个set key && expire key的原子操作:set an_special_lock 1 ex 10 nx

1,#并发线程抢占锁资源,原子操作
set an_special_lock 1 ex 10 nx
2,#如果1抢占到当前锁,并发线程中的当前线程执行
if(成功获取锁)
  business_method()
  3,#释放锁
  del an_special_lock

目前,加锁&&设置锁超时,成为一个原子操作,可以解决当前线程异常之后,锁可以得到释放的问题。

但是仍旧存在问题:
如果在锁超时之后,比如10秒之后,execute_business_method()仍旧没有执行完成,此时锁因过期而被动释放,其他线程仍旧可以获取an_special_lock的锁,并发线程对独占资源的访问仍无法保证。

solution 4: 业务代码加强

到目前为止,solution 3 仍旧无法完美解决并发线程访问独占资源的问题。
笔者能够想到解决上述问题的办法就是:
设置business_method()执行超时时间,如果应用程序中在锁超时的之后仍无法执行完成,则主动回滚(放弃当前线程的执行),然后主动释放锁,而不是等待锁的被动释放(超过expire时间释放)
如果无法确保business_method()在锁过期放之前得到成功执行或者回滚,则分布式锁仍是不安全的。

1,#并发线程抢占锁资源,原子操作
set an_special_lock 1 ex 10 n
2,#如果抢占到当前锁,并发线程中的当前线程执行
if(成功获取锁)
  business_method()#在应用层面控制,业务逻辑操作在Redis锁超时之前,主动回滚
  3,#释放锁
  del an_special_lock

solution 5 RedLock: 解决单点Redis故障

截止目前,(假如)可以认为solution 4解决“占锁”&&“安全释放锁”的问题,仍旧无法保证“锁资源的主动释放”:
Redis往往通过Sentinel或者集群保证高可用,即便是有了Sentinel或者集群,但是面对Redis的当前节点的故障时,仍旧无法保证并发线程对锁资源的真正独占。
具体说就是,当前线程获取了锁,但是当前Redis节点尚未将锁同步至从节点,此时因为单节点的Cash造成锁的“被动释放”,应用程序的其它线程(因故障转移)在从节点仍旧可以占用实际上并未释放的锁。
Redlock需要多个Redis节点,RedLock加锁时,通过多数节点的方式,解决了Redis节点故障转移情况下,因为数据不一致造成的锁失效问题。
其实现原理,简单地说就是,在加锁过程中,如果实现了多数节点加锁成功(非集群的Redis节点),则加锁成功,解决了单节点故障,发生故障转移之后数据不一致造成的锁失效。
而释放锁的时候,仅需要向所有节点执行del操作。

Redlock需要多个Redis节点,由于从一台Redis实例转为多台Redis实例,Redlock实现的分布式锁,虽然更安全了,但是必然伴随着效率的下降。

至此,从solution 1-->solution 2-->solution 3--solution 4-->solution 5,依次解决个前一步的问题,但仍旧是一个非完美的分布式锁实现。

以下通过一个简单的测试来验证Redlock的效果。

case是一个典型的对数据库“存在则更新,不存在则插入的”并发操作(这里忽略数据库层面的锁),通过对比是否通过Redis分布式锁控制来看效果。

#!/usr/bin/env python3
import redis
import sys
import time
import uuid
import threading
from time import ctime,sleep
from redis import StrictRedis
from redlock import Redlock
from multiprocessing import Pool
import pymssql
import random class RedLockTest: _connection_list = None
_lock_resource = None
_ttl = 10 #ttl def __init__(self, *args, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v) def get_conn(self):
try:
#如果当前线程获取不到锁,重试次数以及重试等待时间
conn = Redlock(self._connection_list,retry_count=100, retry_delay=10 )
except:
raise
return conn def execute_under_lock(self,thread_id):
conn = self.get_conn()
lock = conn.lock(self._lock_resource, self._ttl)
if lock :
self.business_method(thread_id)
conn.unlock(lock)
else:
print("try later") '''
模拟一个经典的不存在则插入,存在则更新,起多线程并发操作
实际中可能是一个非常复杂的需要独占性的原子性操作
'''
def business_method(self,thread_id):
print(" thread -----{0}------ execute business method begin".format(thread_id))
conn = pymssql.connect(host="127.0.0.1",server="SQL2014", port=50503, database="DB01")
cursor = conn.cursor()
id = random.randint(0, 100)
sql_script = ''' select 1 from TestTable where Id = {0} '''.format(id)
cursor.execute(sql_script)
if not(cursor.fetchone()):
sql_script = ''' insert into TestTable values ({0},{1},{1},getdate(),getdate()) '''.format(id,thread_id)
else:
sql_script = ''' update TestTable set LastUpdateThreadId ={0} ,LastUpdate = getdate() where Id = {1} '''.format(thread_id,id)
cursor.execute(sql_script)
conn.commit()
cursor.close()
conn.close()
print(" thread -----{0}------ execute business method finish".format(thread_id)) if __name__ == "__main__": redis_servers = [{"host": "*.*.*.*","port": 9000,"db": 0},
{"host": "*.*.*.*","port": 9001,"db": 0},
{"host": "*.*.*.*","port": 9002,"db": 0},]
lock_resource = "mylock"
ttl = 2000 #毫秒
redlock_test = RedLockTest(_connection_list = redis_servers,_lock_resource=lock_resource, _ttl=ttl) #redlock_test.execute_under_lock(redlock_test.business_method)
threads = []
for i in range(50):
#普通的并发模式调用业务逻辑的方法,会产生大量的主键冲突
#t = threading.Thread(target=redlock_test.business_method,args=(i,))
#Redis分布式锁控制下的多线程
t = threading.Thread(target=redlock_test.execute_under_lock,args=(i,))
threads.append(t)
begin_time = ctime()
for t in threads:
t.setDaemon(True)
t.start()
for t in threads:
t.join()

测试 1,简单多线程并发

简单地起多线程执行测试的方法,测试中出现两个很明显的问题
1,出现主键冲突(而报错)
2,从打印的日志来看,各个线程在测试的方法中存在交叉执行的情况(日志信息的交叉意味着线程的交叉执行)

测试 2,Redis锁控制下多线程并发

Redlock的Redis分布式锁为三个独立的Redis节点,无需做集群

当加入Redis分布式锁之后,可以看到,虽然是并发多线程操作,但是在执行实际的测试的方法的时候,都是独占性地执行,
从日志也能够看出来,都是一个线程执行完成之后,另一个线程才进入临界资源区。

Redlock相对安全地解决了一开始分布式锁的潜在问题,与此同时,也增加了复杂度,同时在一定程度上降低了效率。

以上粗浅分析了Redis分布式锁的各种实现以及潜在问题,即便是Redlock,也不是一个完美的分布式锁解决方案,关于Redis的Redlock的争议也有
http://zhangtielei.com/posts/blog-redlock-reasoning.html
仔细阅读会发现,恰恰这些“争议”本身,才是Redis分布式锁最大的精髓所在。

漫谈Redis分布式锁实现的更多相关文章

  1. 利用redis分布式锁的功能来实现定时器的分布式

    文章来源于我的 iteye blog http://ak478288.iteye.com/blog/1898190 以前为部门内部开发过一个定时器程序,这个定时器很简单,就是配置quartz,来实现定 ...

  2. Redis分布式锁

    Redis分布式锁 分布式锁是许多环境中非常有用的原语,其中不同的进程必须以相互排斥的方式与共享资源一起运行. 有许多图书馆和博客文章描述了如何使用Redis实现DLM(分布式锁管理器),但是每个库都 ...

  3. redis分布式锁和消息队列

    最近博主在看redis的时候发现了两种redis使用方式,与之前redis作为缓存不同,利用的是redis可设置key的有效时间和redis的BRPOP命令. 分布式锁 由于目前一些编程语言,如PHP ...

  4. redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  5. spring boot redis分布式锁

    随着现在分布式架构越来越盛行,在很多场景下需要使用到分布式锁.分布式锁的实现有很多种,比如基于数据库. zookeeper 等,本文主要介绍使用 Redis 做分布式锁的方式,并封装成spring b ...

  6. Redis分布式锁的正确实现方式

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  7. Redis分布式锁---完美实现

    这几天在做项目缓存时候,因为是分布式的所以需要加锁,就用到了Redis锁,正好从网上发现两篇非常棒的文章,来和大家分享一下. 第一篇是简单完美的实现,第二篇是用到的Redisson. Redis分布式 ...

  8. redis分布式锁实践

    分布式锁在多实例部署,分布式系统中经常会使用到,这是因为基于jvm的锁无法满足多实例中锁的需求,本篇将讲下redis如何通过Lua脚本实现分布式锁,不同于网上的redission,完全是手动实现的 我 ...

  9. Redis分布式锁的try-with-resources实现

    Redis分布式锁的try-with-resources实现 一.简介 在当今这个时代,单体应用(standalone)已经很少了,java提供的synchronized已经不能满足需求,大家自然 而 ...

随机推荐

  1. delphi之多线程编程(尚未学习)

    本文的内容取自网络,并重新加以整理,在此留存仅仅是方便自己学习和查阅.所有代码均亲自测试 delphi7下测试有效.图片均为自己制作. 多线程应该是编程工作者的基础技能, 但这个基础我从来没学过,所以 ...

  2. WPF 代码实现动画

    <Window x:Class="wpf180709.Window2"        xmlns="http://schemas.microsoft.com/win ...

  3. 基于IdentityServer4的单点登录——Client

    以MvcClient项目为例 1.新建项目并添加引用 新建一个asp .net core 2.0的项目引用IdentityModel 2.配置 比之前的控制台客户端多这个步骤,需要配置这个客户端的Cl ...

  4. 最简单的IdentityServer实现——IdentityServer

    1.新建项目 新建ASP .Net Core项目IdentityServer.EasyDemo.IdentityServer,选择.net core 2.0   1   2 引用IdentitySer ...

  5. 向WPF的Grid里面增加控件

    在c#中,有时需要通过代码创建面板控件,但是对于面板的操作与之前的控件并不是很直观,因此记下方法,以供后来复习使用: 创建两行两列的表格: 第一种方法: Grid grid = new Grid(); ...

  6. CORS 专题

    CORS(跨域资源共享,Cross-Origin Resource Sharing)CORS其实出现时间不短了,它在维基百科上的定义是:跨域资源共享(CORS )是一种网络浏览器的技术规范,它为Web ...

  7. VC中出现“烫”和“屯”的原因(栈区的每一个字节都被0xCC填充了,也就是int 3h的机器码,动态分配的堆,VC的Debug用0xCD填充堆的空间,就出现了“屯”)

    相信经常用VC的朋友对屏幕输出的一大堆“烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫烫”不会陌生,但是也许会很奇怪,为什么会出现“烫”字呢?莫非改程序导致系统运行缓慢,发热过高???非也!下面让我解释 ...

  8. 微信小程序把玩(三十三)Record API

    原文:微信小程序把玩(三十三)Record API 其实这个API也挺奇葩的,录音结束后success不走,complete不走,fail也不走, 不知道是不是因为电脑测试的原因,只能等公测或者等他们 ...

  9. 解决WPF中TextBox文件拖放问题

    在WPF中,当我们尝试向TextBox中拖放文件,从而获取其路径时,往往无法成功(拖放文字可以成功).造成这种原因关键是WPF的TextBox对拖放事件处理机制的不同,具体可参考这篇文章Textbox ...

  10. 腾讯网移动端H5页面设计实战分享

    分享 <关于我> 分享  [中文纪录片]互联网时代                 http://pan.baidu.com/s/1qWkJfcS 分享 <HTML开发MacOSAp ...