Redis如何实现分布式锁


Posted in Redis onAugust 23, 2021

今天我们来聊一聊分布式锁的那些事。

相信大家对锁已经不陌生了,我们在多线程环境中,如果需要对同一个资源进行操作,为了避免数据不一致,我们需要在操作共享资源之前进行加锁操作。在计算机科学中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。

比如你去相亲,发现你和一大哥同时和一个女的相亲,那怎么行呢...,搞不好还要被揍一顿。

那什么是分布式锁呢。当多个客户端需要争抢锁时,我们就需要分布式锁。这把锁不能是某个客户端本地的锁,否则的话,其它客户端是无法访问的。所以分布式锁是需要存储在共享存储系统中的,比如Redis、Zookeeper等,可以被多个客户端共享访问和获取。今天我们就来看一下如何使用Redis来实现分布式锁。

一、前言

在正式开始之前,我们先来了解两个Redis的命令:

SETNX key value

这个命名的含义是,当key存在时,不做任何赋值操作;当key不存在时,就创建key,并赋值成value,即(不存在即设置)。

SET key value [EX seconds | PX milliseconds] NX

SET后加NX选项,就和SETNX命令类似了,也实现不存在即设置的功能。此外,这个命令在执行时,可以通过EX或者PX设置键值对的过期时间。

二、正文

开始之前,我们先引入一个场景:

假设要给某个商品举行秒杀活动,我们事先把库存数据100已经存入到了redis中,我们现在需要来进行库存扣减。

如图所示,我们假设有1000个客户端来进行库存扣减操作,那我们该如何做,才能保证库存扣减顺序一致且不会超扣呢。

Redis如何实现分布式锁

我们首先想到的就是加锁,在进行库存扣减之前,我们先拿到锁,然后进行扣减,最后再释放锁。在redis中我们创建一个key来代表一个锁变量,然后对应的值来表示锁变量的值。我们来看一下如何进行加锁。

假设1000个客户端同时进行加锁请求。因为redis使用单线程来处理请求,所以redis会串行执行他们的请求操作。假设redis先处理客户端2的请求,读取lock_key的值,发现lock_key为0,所以客户端2就把lock_key的value设置成1,表示已经进行了加锁操作。如果此时客户端3被处理,发现lock_key的值已经为1了,所以就返回加锁失败的信息。

Redis如何实现分布式锁

当拿到锁的客户端2处理完共享资源后,就要进行释放锁的操作,释放锁很简单,就是将lock_key重新设置为0。

由于加锁操作包含了三个操作(读取锁变量、判断锁变量的值以及把锁变量的值设置成1),而这三个操作在执行的过程中需要保证原子性。那怎么保证原子性呢?

我们可以使用SETNX命令来实现加锁操作,SETNX命令表示key不存在时就创建,key存在时就不做任何赋值操作,当加锁时候,我们执行

SETNX lock_key 1

对于释放锁操作来说,我们可以使用DEL命令来删除锁变量。比如客户端2进行加锁,执行SETNX lock_key 1,如果lock_key不存在,则会创建lock_key,返回加锁成功,此时客户端2可以进行共享资源的访问。如果这时客户端1来发起请求加锁操作,而此时lock_key已经存在,SETNX lock_key 1不做任何赋值操作操作,返回加锁失败,所以客户端1加锁失败。当客户端2执行完共享资源访问后,执行DEL命令来释放锁。此时当有其它客户端再来访问时,lock_key已经不存在了,就可以进行正常的加锁操作了。所以,我们可以使用SETNX和DEL命令组合来进行加锁和释放锁的操作。

不过这里有两个问题:

1.当某个客户端执行完SETNX命令、加锁后,此时发生了异常,结果一直没有执行DEL操作命令来释放锁。因此,这个客户端一直占用着这个锁,其它客户端无法拿到锁。

解决这个问题,一个有效的方法就是,给锁变量设置一个过期时间。这样一来,即使持有锁的客户端发生了异常,无法主动的释放锁,Redis也会根据锁变量的过期时间把它删除。其它客户端在锁变量过期后,就可以重新进行加锁操作了。

2.如果客户端1执行了SETNX命令加锁后。如果此时客户端2执行DEL命令删除锁,这时,客户端A的锁就被误释放了。这是我们不能接受的。

为了解决这个问题,我们需要能区分来自不同客户端的锁操作。我们该如何做呢?我们可以给每个客户端生成一个唯一值,在进行加锁时,我们把锁变量赋值成这个唯一值。这样在释放锁的时候,客户端需要判断,当前锁变量的值是否和自己的唯一标识相等,在相等的情况下,才能释放锁。

下面来看一下如何在Redis中进行实现。我们可以使用SET加EX/PX和NX选项,来进行加锁操作。

SET lock_key uuid NX PX 100

 其中lock_key是锁变量,uuid表示客户端的唯一标识,PX 100表示100ms过期。由于我们在释放锁时需要对比客户端的标识和锁变量的值是否一致,这包含了多个操作,为了保证原子性,我们需要使用lua脚本,下面是lua脚本的实现。

if redis.call("get",KEYS[1]) == ARGV[1] then  
   return redis.call("del",KEYS[1])
else 
   return 0
end

其中KEY[1]表示lock_key,ARGV[1]表示当前客户端的唯一标识,这两个值是我们在执行lua脚本时作为参数传入的。下面我们来看一下完整的代码实现。

import redis
import traceback
import uuid
import time
 
class Inventory(object):
    def __init__(self):
        pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True)
        client = redis.StrictRedis(connection_pool=pool, max_connections=20)
        self.client=client
        self.uuid=str(uuid.uuid1())
        print(self.uuid)
        self.key="lock_key"
        self.inventory_key="inventory"
    def unlock(self):
        unlock_script="" \
                      "if redis.call(\"get\",KEYS[1]) == ARGV[1] then" \
                      "   return redis.call(\"del\",KEYS[1])" \
                      "else" \
                      "   return 0 " \
                      "end"
        try:
            unlock_cmd=self.client.register_script(unlock_script)
            result=unlock_cmd(keys=[self.key],args=[self.uuid])
            if result==1:
                print("释放成功")
            else:
                print("释放出错")
        except:
            print(traceback.format_exc())
 
    def lock(self):
        try:
          while True:
             result=self.client.set(self.key,self.uuid,px=100,nx=True)
             print(result)
             if result==1:
                 break
 
             print("sleep 1s")
             time.sleep(1)
          print("加锁成功")
          return True
        except:
          print(traceback.format_exc())
    def inventory(self):
        if self.lock():
           print("库存扣减")
           self.client.decr(self.inventory_key)
           print("扣减完成")
           self.unlock()
 
 
inv=Inventory()
inv.inventory()

到此,我们就把Redis实现分布式锁就聊完了。既然都读到了这里,不妨给个「三连」吧,你的三连就是我最大的动力。

到此这篇关于Redis如何实现分布式锁的文章就介绍到这了,更多相关Redis 分布式锁内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
Redis高级数据类型Hyperloglog、Bitmap的使用
May 24 Redis
详解Redis基本命令与使用场景
Jun 01 Redis
解析高可用Redis服务架构分析与搭建方案
Jun 20 Redis
Redis分布式锁Redlock的实现
Aug 07 Redis
redis的list数据类型相关命令介绍及使用
Jan 18 Redis
分布式Redis Cluster集群搭建与Redis基本用法
Feb 24 Redis
Redis集群节点通信过程/原理流程分析
Mar 18 Redis
Redis如何实现验证码发送 以及限制每日发送次数
Apr 18 Redis
Redis keys命令的具体使用
Jun 05 Redis
利用Redis实现点赞功能的示例代码
Jun 28 Redis
Redis配置外网可访问(redis远程连接不上)的方法
Dec 24 Redis
Redisson实现Redis分布式锁的几种方式
Redis分布式锁Redlock的实现
Aug 07 #Redis
关于redisson缓存序列化几枚大坑说明
Aug 04 #Redis
Redis Cluster 集群搭建你会吗
Aug 04 #Redis
解析redis hash应用场景和常用命令
Aug 04 #Redis
redis 存储对象的方法对比分析
Aug 02 #Redis
springboot使用Redis作缓存使用入门教程
Jul 25 #Redis
You might like
PHP 中的面向对象编程:通向大型 PHP 工程的办法
2006/12/03 PHP
使用PHP实现蜘蛛访问日志统计
2013/07/05 PHP
php中base_convert()进制数字转换函数实例
2014/11/20 PHP
PHP实现的一致性哈希算法完整实例
2015/11/14 PHP
php封装的smarty类完整实例
2016/10/19 PHP
php使用curl伪造来源ip和refer的方法示例
2018/05/08 PHP
javascript使用正则表达式实现去掉空格之后的字符
2015/02/15 Javascript
jQuery获取标签文本内容和html内容的方法
2015/03/27 Javascript
js实现带圆角的多级下拉菜单效果
2015/08/28 Javascript
纯javascript响应式树形菜单效果
2015/11/10 Javascript
jQuery中的insertBefore(),insertAfter(),after(),before()区别介绍
2016/09/01 Javascript
jQuery联动日历的实例解析
2016/12/02 Javascript
vuejs 切换导航条高亮(路由菜单高亮)的方法示例
2018/05/29 Javascript
vue 监听屏幕高度的实例
2018/09/05 Javascript
微信小程序的授权实现过程解析
2019/08/02 Javascript
JavaScript 正则应用详解【模式、欲查、反向引用等】
2020/05/13 Javascript
vue3.0生命周期的示例代码
2020/09/24 Javascript
vue的webcamjs集成方式
2020/11/16 Javascript
[02:08]我的刀塔不可能这么可爱 胡晓桃_1
2014/06/20 DOTA
[47:36]Optic vs Newbee 2018国际邀请赛小组赛BO2 第二场 8.17
2018/08/18 DOTA
Python 数据结构之旋转链表
2017/02/25 Python
浅谈python的深浅拷贝以及fromkeys的用法
2019/03/08 Python
利用Python绘制Jazz网络图的例子
2019/11/21 Python
pytorch制作自己的LMDB数据操作示例
2019/12/18 Python
Python matplotlib画曲线例题解析
2020/02/07 Python
python3中布局背景颜色代码分析
2020/12/01 Python
基于css3实现漂亮便签样式
2013/03/18 HTML / CSS
CSS改变网页中鼠标选中文字背景颜色例子
2014/04/23 HTML / CSS
CSS3 linear-gradient线性渐变生成加号和减号的方法
2017/11/21 HTML / CSS
土耳其时尚潮流在线购物网站:Trendyol
2017/10/10 全球购物
什么是用户模式(User Mode)与内核模式(Kernel Mode) ?
2015/09/07 面试题
在职人员函授期间自我评价分享
2013/11/08 职场文书
汽修专业学生自我鉴定
2013/11/16 职场文书
2014年乡镇植树节活动方案
2014/02/28 职场文书
高中军训的心得体会
2014/09/01 职场文书
小学感恩节活动策划方案
2014/10/06 职场文书