前言
这几年一直在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
命令(redis
在2.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
有着固定的大小128
bit位,通常由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
而锁要做的事情就是将一个随机生成的128
位UUID
设置位键的值,防止该锁被其他进程获取
如果程序在尝试获取锁的过程中失败,那么他将不断的进行重试,直到成功的取得锁或超过锁的持有超时时间
- 初始化连接函数
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
结果进行判断验证,符合条件通过delete
在redis
中删除即可,此外当其他用户持有同名锁时,由于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
在其他数据库里面,加锁通常是一个自动执行的基本操作,而Redis
的WATCH
、MULTI
和EXEC
操作只是一个乐观锁;这种锁只会在数据被其他客户端抢先修改的情况下,通知加锁的客户端,让他撤销对于被监控数据的修改,而不会把数据真正的锁住
通过在客户端上面实现一个真正的锁,程序可以位用户带来更好的性能,更熟悉的编程概念、更简单易用的API
于此同时,也要注意,Redis
并不会自动使用我们自制的锁,我们必须自己使用这个锁来代替WATCH
,从而保证数据的正确与一致性