百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

资深架构师经典总结:Redis分布式锁实现理解

mhr18 2024-10-24 11:08 17 浏览 0 评论

在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/envPython3

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

相关推荐

使用 Docker 部署 Java 项目(通俗易懂)

前言:搜索镜像的网站(推荐):DockerDocs1、下载与配置Docker1.1docker下载(这里使用的是Ubuntu,Centos命令可能有不同)以下命令,默认不是root用户操作,...

Spring Boot 3.3.5 + CRaC:从冷启动到秒级响应的架构实践与踩坑实录

去年,我们团队负责的电商订单系统因扩容需求需在10分钟内启动200个Pod实例。当运维组按下扩容按钮时,传统SpringBoot应用的冷启动耗时(平均8.7秒)直接导致流量洪峰期出现30%的请求超时...

《github精选系列》——SpringBoot 全家桶

1简单总结1SpringBoot全家桶简介2项目简介3子项目列表4环境5运行6后续计划7问题反馈gitee地址:https://gitee.com/yidao620/springbo...

Nacos简介—1.Nacos使用简介

大纲1.Nacos的在服务注册中心+配置中心中的应用2.Nacos2.x最新版本下载与目录结构3.Nacos2.x的数据库存储与日志存储4.Nacos2.x服务端的startup.sh启动脚...

spring-ai ollama小试牛刀

序本文主要展示下spring-aiollama的使用示例pom.xml<dependency><groupId>org.springframework.ai<...

SpringCloud系列——10Spring Cloud Gateway网关

学习目标Gateway是什么?它有什么作用?Gateway中的断言使用Gateway中的过滤器使用Gateway中的路由使用第1章网关1.1网关的概念简单来说,网关就是一个网络连接到另外一个网络的...

Spring Boot 自动装配原理剖析

前言在这瞬息万变的技术领域,比了解技术的使用方法更重要的是了解其原理及应用背景。以往我们使用SpringMVC来构建一个项目需要很多基础操作:添加很多jar,配置web.xml,配置Spr...

疯了!Spring 再官宣惊天大漏洞

Spring官宣高危漏洞大家好,我是栈长。前几天爆出来的Spring漏洞,刚修复完又来?今天愚人节来了,这是和大家开玩笑吗?不是的,我也是猝不及防!这个玩笑也开的太大了!!你之前看到的这个漏洞已...

「架构师必备」基于SpringCloud的SaaS型微服务脚手架

简介基于SpringCloud(Hoxton.SR1)+SpringBoot(2.2.4.RELEASE)的SaaS型微服务脚手架,具备用户管理、资源权限管理、网关统一鉴权、Xss防跨站攻击、...

SpringCloud分布式框架&amp;分布式事务&amp;分布式锁

总结本文承接上一篇SpringCloud分布式框架实践之后,进一步实践分布式事务与分布式锁,其中分布式事务主要是基于Seata的AT模式进行强一致性,基于RocketMQ事务消息进行最终一致性,分布式...

SpringBoot全家桶:23篇博客加23个可运行项目让你对它了如指掌

SpringBoot现在已经成为Java开发领域的一颗璀璨明珠,它本身是包容万象的,可以跟各种技术集成。本项目对目前Web开发中常用的各个技术,通过和SpringBoot的集成,并且对各种技术通...

开发好物推荐12之分布式锁redisson-sb

前言springboot开发现在基本都是分布式环境,分布式环境下分布式锁的使用必不可少,主流分布式锁主要包括数据库锁,redis锁,还有zookepper实现的分布式锁,其中最实用的还是Redis分...

拥抱Kubernetes,再见了Spring Cloud

相信很多开发者在熟悉微服务工作后,才发现:以为用SpringCloud已经成功打造了微服务架构帝国,殊不知引入了k8s后,却和CloudNative的生态发展脱轨。从2013年的...

Zabbix/J监控框架和Spring框架的整合方法

Zabbix/J是一个Java版本的系统监控框架,它可以完美地兼容于Zabbix监控系统,使得开发、运维等技术人员能够对整个业务系统的基础设施、应用软件/中间件和业务逻辑进行全方位的分层监控。Spri...

SpringBoot+JWT+Shiro+Mybatis实现Restful快速开发后端脚手架

作者:lywJee来源:cnblogs.com/lywJ/p/11252064.html一、背景前后端分离已经成为互联网项目开发标准,它会为以后的大型分布式架构打下基础。SpringBoot使编码配置...

取消回复欢迎 发表评论: