Redis如何实现分布式锁

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

相信大家对锁已经不陌生了,我们在多线程环境中,如果需要对同一个资源进行操作,为了避免数据不一致,我们需要在操作共享资源之前进行加锁操作。在计算机科学中,锁(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中我们创建一个key来代表一个锁变量,然后对应的值来表示锁变量的值。我们来看一下如何进行加锁。

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

当拿到锁的客户端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如何实现分布式锁