Redis分布式锁解决并发竞争、超卖等问题

前言

这几年一直在it行业里摸爬滚打,一路走来,不少总结了一些python行业里的高频面试,看到大部分初入行的新鲜血液,还在为各样的面试题答案或收录有各种困难问题

于是乎,我自己开发了一款面试宝典,希望能帮到大家,也希望有更多的Python新人真正加入从事到这个行业里,让python火不只是停留在广告上。

微信小程序搜索:Python面试宝典

或可关注原创个人博客:https://lienze.tech

也可关注微信公众号,不定时发送各类有趣猎奇的技术文章:Python编程学习

分布式锁

一般来说,对数据进行加锁时,程序首先需要通过获取acquire锁来得到对数据操作、排他的权力

在操作完毕之后,还需要通过release进行锁的释放,以供其他程序使用


Redis使用WATCH命令用以代替对数据进行加锁,WATCH只会在数据被其他客户端抢先修改了的情况下通知执行命令的这个客户端(通过WatchError异常),但不会阻止其他客户端对数据的修改,这样的加锁的行为也常称为乐观锁

锁和范围score有关,为了让Redis存储的数据进行排他性访问,客户端需要一个锁,而这样的锁,是可以让所有的客户端都在看得见的范围,这个范围就是Redis本身,因此我们需要把锁构建在Redis里面。另一个方面,虽然有类似的SETNX命令可以实现Redis中的锁的功能,但他锁提供的机制并不完整,也不具备分布式锁的一些高级特性,还是得通过我们手动构建

Watch

回顾一下Multi命令

Multi命令用于标记一个事务块的开始,事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由EXEC命令原子性(atomic)地执行

回顾一下WATCH命令redis2.2之后加入了watch的功能)

WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,在Python中将会抛出WatchError

监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)


当用户购买时,首先开启事务,通过WATCH监听用户库存,判断是否含有库存,如果含有,则库存数量减一,并执行任务

# 首先在redis中设置某商品apple 对应数量value值为1000
import redis
def sale():
    rs = redis.Redis(host=host,port=6379)
    while 1:
        with rs.pipeline() as p: 
            '''
                通过管道方式进行连接
                多条命令执行结束,一次性获取结果
            '''
            try:
                p.watch('apple') # 监听key值为apple的数据数量改变
                count = int(rs.get('apple'))
                print('拿取到了苹果的数量: %d' % count)
                p.multi() # 事务开始
                if count> 0 : # 如果此时还有库存
                    p.set('apple', count - 1)
                    p.execute() # 执行事务
                p.unwatch()
                break # 当库存成功减一或没有库存时跳出执行循环
            except Exception as e: # 当出现watch监听值出现修改时,WatchError异常抛出
                print('[Error]: %s' % e)
                continue # 继续尝试执行
  • 到目前,通过Watch监听,结合事务的MULTI以及EXEC可以实现这样一个版本的锁,随着负载的不断增加,系统完成一次交易的重试次数也将会越来越大,完成一次交易需要等待的时间也将不断增加

可以看到,Redis在尝试完成一个事务的时候,可能会因为事务的失败而重复尝试重新执行,保证商品的库存量正确是一件很重要的事情,但是单纯的使用WATCH这样的机制在压力较大的情况下并不完美,那么接下来,就可以通过上锁来进行库存数量改变

SimpleLock

通过加锁的形式,可以解决以上Watch监控所导致的问题

uuid
  • uuid是什么

它是通过MAC地址、 时间戳、 命名空间、 随机数、 伪随机数来保证生成ID的唯一性

uuid有着固定的大小128bit位,通常由32字节的字符串(十六进制)表示

  • uuid的作用

很多应用场景需要一个id,但是又不要求这个id有具体的意义,仅仅用来标识一个对象

常见的用处有数据库表的id字段

另一个例子是前端的各种UI库,因为它们通常需要动态创建各种UI元素,这些元素需要唯一的id, 这时候就需要使用UUID

例如:一个网站在存储视频、图片等格式的文件时,这些文件的命名方式就可以采用UUID生成的随机标识符,避免重名的出现

python生成uuid数值可以通过以下方式


  • uuid.uuid1([node[, clock_seq]]) :基于时间戳

使用主机ID,序列号,和当前时间来生成UUID,可保证全球范围的唯一性

但由于使用该方法生成的UUID中包含有主机的网络地址,因此可能危及隐私,该函数有两个参数,

如果node参数未指定, 系统将会自动调用getnode()函数来获取主机的硬件地址,如果clock_seq参数未指定系统会使用一个随机产生的14位序列号来代替

  • uuid.uuid3(namespace, name):基于名字的MD5散列值

通过计算命名空间和名字的MD5散列值来生成UUID;可以保证同一命名空间中不同名字的唯一性和不同命名空间的唯一性,但同一命名空间的同一名字生成的UUID相同

  • uuid.uuid4():基于随机数

通过随机数来生成UUID,使用的是伪随机数有一定的重复概率

  • uuid.uuid5(namespace, name):基于名字的SHA-1散列值

通过计算命名空间和名字的SHA-散列值来生成UUID,算法与uuid.uuid3()相同.

RedisLock

使用Redis构建锁非常简单,在Redis中,可以通过使用SETNX命令来实现,这个命令会在键不存在的情况下为键设置值,如果键存在,则设置失败返回0

而锁要做的事情就是将一个随机生成的128UUID设置位键的值,防止该锁被其他进程获取

如果程序在尝试获取锁的过程中失败,那么他将不断的进行重试,直到成功的取得锁超过锁的持有超时时间

  • 初始化连接函数
def get_conn(host,port=6379):
    rs = redis.Redis(host=host, port=port)
  	return rs
  • 加锁函数
def acquire_lock(rs, lock_name, expire_time=10):
    '''
        rs: 连接对象
        lock_name: 锁标识
        acquire_time: 过期超时时间
        return -> False 获锁失败 or True 获锁成功
    '''
    # print('获取锁...')
    identifier = str(uuid.uuid4())
    end = time.time() + expire_time

    while time.time() < end:
        # 当获取锁的行为超过有效时间,则退出循环,本次取锁失败,返回False
        if rs.setnx(lock_name, identifier):  # 尝试取得锁
            # print('锁已设置: %s' % identifier)
            return identifier
        time.sleep(.001)
    return False

加锁函数通过SETNX命令,尝试在锁不存在的i情况下,为键设置一个值,以此来获取锁

在获取锁失败的时候,会尝试在给定的时间内进行重试,一直到重新成功获取到或超过给定的实现

  • 释放锁函数
def release_lock(rs, lockname, identifier):
    '''
        rs: 连接对象
        lockname: 锁标识
        identifier: 锁的value值,用来校验
    '''
    pipe = rs.pipeline(True)
    try:
        pipe.watch(lockname)
        # print('当前获取到的锁:', rs.get(lockname).decode())
        # print('redis中实际锁的值:',identifier)
        # print(rs.get(lockname).decode() == identifier)
        if rs.get(lockname).decode() == identifier:
            pipe.multi()  # 开启事务
            pipe.delete(lockname)
            pipe.execute() # print('锁已释放')
            return True  # 删除锁
        pipe.unwatch()  # 取消事务
    except Exception as e:
        pass
    return False  # 删除失败

锁的删除操作很简单,只需要将对应锁的key值获取到的uuid结果进行判断验证,符合条件通过deleteredis中删除即可,此外当其他用户持有同名锁时,由于uuid的不同,经过验证后不会错误释放掉别人的锁

def sale():
    rs = get_conn(host=host)
    start = time.time() # 程序启动时间
    with rs.pipeline() as p:
        while 1:
            lock = acquire_lock(rs, 'lock')
            if not lock:  # 持锁失败
                continue
            try:
                count = int(rs.get('apple')) # 取量
                p.set('apple', count-1) # 减量
                p.execute() 
                print('当前库存量: %s' % count)
                break
            finally:
                release_lock(rs, 'lock', lock)
    print('[time]: %.2f' % (time.time() - start))

ExpireLock

在之前的锁中,还出现这样的问题,比如某个进程持有锁之后突然程序崩溃,那么会导致锁无法释放而其他进程无法持有锁继续工作,为了解决这样的问题,可以在获取锁的时候加上锁的超时功能

Redis中,可以通过EXPIRE命令为锁设置过期时间,Redis会自动释放超时的锁,以下是超时锁的定义模型

def acquire_expire_lock(rs, lock_name, expire_time=10, locked_time=10):
    '''
        rs: 连接对象
        lock_name: 锁标识
        acquire_time: 过期超时时间
        locked_time: 锁的有效时间
        return -> False 获锁失败 or True 获锁成功
    '''
    # print('获取锁...')
    identifier = str(uuid.uuid4())
    end = time.time() + expire_time

    while time.time() < end:
        # 当获取锁的行为超过有效时间,则退出循环,本次取锁失败,返回False
        if rs.setnx(lock_name, identifier):  # 尝试取得锁
            # print('锁已设置: %s' % identifier)
            rs.expire(lock_name, locked_time)
            return identifier
        time.sleep(.001)
    return False

在其他数据库里面,加锁通常是一个自动执行的基本操作,而RedisWATCHMULTIEXEC操作只是一个乐观锁;这种锁只会在数据被其他客户端抢先修改的情况下,通知加锁的客户端,让他撤销对于被监控数据的修改,而不会把数据真正的锁住

通过在客户端上面实现一个真正的锁,程序可以位用户带来更好的性能,更熟悉的编程概念、更简单易用的API

于此同时,也要注意,Redis并不会自动使用我们自制的锁,我们必须自己使用这个锁来代替WATCH,从而保证数据的正确与一致性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李恩泽的技术博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值