setnx(redis分布式锁)
- 分布式锁
- 分布式锁本质是占一个坑,当别的进程也要来占坑时发现已经被占,就会放弃或者稍后重试
- 占坑一般使用 setnx(set if not exists)指令,只允许一个客户端占坑
- 先来先占,用完了在调用del指令释放坑
> setnx lock:codehole true
.... do something critical ....
> del lock:codehole
- 但是这样有一个问题,如果逻辑执行到中间出现异常,可能导致del指令没有被调用,这样就会陷入死锁,锁永远无法释放
- 为了解决死锁问题,我们拿到锁时可以加上一个expire过期时间,这样即使出现异常,当到达过期时间也会自动释放锁
> setnx lock:codehole true
> expire lock:codehole 5
.... do something critical ....
> del lock:codehole
- 这样又有一个问题,setnx和expire是两条指令而不是原子指令,如果两条指令之间进程挂掉依然会出现死锁
- 为了治理上面乱象,在redis 2.8中加入了set指令的扩展参数,使setnx和expire指令可以一起执行
> set lock:codehole true ex 5 nx
''' do something '''
> del lock:codehole
redis解决超卖问题
使用reids的 watch + multi 指令实现
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import redis
def sale(rs):
while True:
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 # 继续尝试执行
rs = redis.Redis(host='127.0.0.1', port=6379) # 连接redis
rs.set('apple',1000) # # 首先在redis中设置某商品apple 对应数量value值为1000
sale(rs)
watch+multi解决超卖问题
- 原理
- 当用户购买时,通过 WATCH 监听用户库存,如果库存在watch监听后发生改变,就会捕获异常而放弃对库存减一操作
- 如果库存没有监听到变化并且数量大于1,则库存数量减一,并执行任务
- 弊端
- Redis 在尝试完成一个事务的时候,可能会因为事务的失败而重复尝试重新执行
- 保证商品的库存量正确是一件很重要的事情,但是单纯的使用 WATCH 这样的机制对服务器压力过大
使用reids的 watch + multi + setnx 指令实现
- 为什么要自己构建锁?
- 虽然有类似的 SETNX 命令可以实现 Redis 中的锁的功能,但他锁提供的机制并不完整
- 并且setnx也不具备分布式锁的一些高级特性,还是得通过我们手动构建
- 创建一个redis锁
- 在 Redis 中,可以通过使用 SETNX 命令来构建锁:rs.setnx(lock_name, uuid值)
- 而锁要做的事情就是将一个随机生成的 128 位 UUID 设置位键的值,防止该锁被其他进程获取
- 释放锁
- 锁的删除操作很简单,只需要将对应锁的 key 值获取到的 uuid 结果进行判断验证
- 符合条件(判断uuid值)通过 delete 在 redis 中删除即可,pipe.delete(lockname)
- 此外当其他用户持有同名锁时,由于 uuid 的不同,经过验证后不会错误释放掉别人的锁
- 解决锁无法释放问题
- 在之前的锁中,还出现这样的问题,比如某个进程持有锁之后突然程序崩溃,那么会导致锁无法释放
- 而其他进程无法持有锁继续工作,为了解决这样的问题,可以在获取锁的时候加上锁的超时功能
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import redis
import uuid
import time
# 1.初始化连接函数
def get_conn(host,port=6379):
rs = redis.Redis(host=host, port=port)
return rs
# 2. 构建redis锁
def acquire_lock(rs, lock_name, expire_time=10):
'''
rs: 连接对象
lock_name: 锁标识
acquire_time: 过期超时时间
return -> False 获锁失败 or True 获锁成功
'''
identifier = str(uuid.uuid4())
end = time.time() + expire_time
while time.time() < end:
# 当获取锁的行为超过有效时间,则退出循环,本次取锁失败,返回False
if rs.setnx(lock_name, identifier): # 尝试取得锁
return identifier
time.sleep(.001)
return False
# 3. 释放锁
def release_lock(rs, lockname, identifier):
'''
rs: 连接对象
lockname: 锁标识
identifier: 锁的value值,用来校验
'''
pipe = rs.pipeline(True)
try:
pipe.watch(lockname)
if rs.get(lockname).decode() == identifier: # 防止其他进程同名锁被误删
pipe.multi() # 开启事务
pipe.delete(lockname)
pipe.execute()
return True # 删除锁
pipe.unwatch() # 取消事务
except Exception as e:
pass
return False # 删除失败
'''在业务函数中使用上面的锁'''
def sale(rs):
start = time.time() # 程序启动时间
with rs.pipeline() as p:
'''
通过管道方式进行连接
多条命令执行结束,一次性获取结果
'''
while True:
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))
rs = redis.Redis(host='127.0.0.1', port=6379) # 连接redis
rs.set('apple',1000) # # 首先在redis中设置某商品apple 对应数量value值为1000
sale(rs)
setnx+watch+multi解决超卖问题
上面两个方案 demo 都会出现死锁得问题
解决死锁
def acquire_expire_lock(rs, lock_name, expire_time=10, locked_time=10):
'''
rs: 连接对象
lock_name: 锁标识
acquire_time: 过期超时时间
locked_time: 锁的有效时间
return -> False 获锁失败 or True 获锁成功
'''
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
优化:给分布式锁加超时时间防止死锁