Redis 的事务就是将多个命令序列化,按顺序执行,并且它们在执行的过程中,不会被其他客户端发来的命令请求所打断。
一个事务从开始到执行会经历以下三个阶段:
- 开始事务,使用 MULTI命令。
- 命令入队,正常的操作命令。
- 执行事务, EXEC命令。
也可以在 EXEC 之前用 DISCARD 放弃这个事务。
另外需要注意的是,
-
如果入队过程中命令报错了,执行时会直接报错,所有的命令都不会执行。
-
如果入队过程中没有命令报错,而在执行过程中,某个命令执行失败,则不影响其他命令的执行。
下面练习一下 redis 处理秒杀商品的案例。
秒杀商品
代码如下:
def handle_buy_product(user):
"""
抢购商品
"""
product_number = redis_client.get(PRODUCT_NUMBER_KEY)
# 未设置商品数目时,秒杀尚未开始
if product_number is None:
print("秒杀尚未开始")
return False
# 如果用户已经秒杀成功了
if redis_client.sismember(SUCCESS_USER_KEY, user):
print("你已秒杀成功,不能重复购买")
return False
# 如果库存不足,秒杀失败
if int(product_number) <= 0:
print("没有库存了")
return False
# 商品数量减1
redis_client.decr(PRODUCT_NUMBER_KEY)
# 记录秒杀成功的用户
redis_client.sadd(SUCCESS_USER_KEY, user)
return True
使用 locust 模拟高并发场景。
from locust import HttpUser, task
class BuyProductUser(HttpUser):
@task()
def buy(self):
self.client.post("/buy")
if __name__ == "__main__":
import os
os.system("locust -f locust_test.py --host=http://localhost:8000")
可以发现会出现超卖的情况,也就是商品库存被扣到负数。
使用事务解决
我们可以使用 pipeline,watch, multi, execute 来解决。其本质上是使用了乐观锁来处理并发问题。
def handle_buy_product(user):
"""
抢购商品
"""
pipe = redis_client.pipeline()
try:
# 增加监视
pipe.watch(PRODUCT_NUMBER_KEY)
# 获取库存
product_number = pipe.get(PRODUCT_NUMBER_KEY)
# 未设置商品数目时,秒杀尚未开始
if product_number is None:
print("秒杀尚未开始")
return False
# 如果用户已经秒杀成功了
if redis_client.sismember(SUCCESS_USER_KEY, user):
print("你已秒杀成功,不能重复购买")
return False
# 如果库存不足,秒杀失败
if int(product_number) <= 0:
print("没有库存了")
return False
# 事务
pipe.multi()
# 商品数量减1
pipe.decr(PRODUCT_NUMBER_KEY)
# 记录秒杀成功的用户
pipe.sadd(SUCCESS_USER_KEY, user)
# 执行
pipe.execute()
except WatchError:
print("秒杀失败")
return False
finally:
pipe.reset()
return True
实验结果可以发现,不会出现超卖问题了,但是,可以观察到,会出现很多次的秒杀失败,商品数量下降的也比较慢。
使用 lua 锁
我们可以使用 lua 锁(悲观锁)来解决上面的问题。
def handle_buy_product(user):
"""
抢购商品
"""
try:
with redis_client.lock("my-lock-key", blocking_timeout=5) as lock:
# 获取库存
product_number = redis_client.get(PRODUCT_NUMBER_KEY)
# 未设置商品数目时,秒杀尚未开始
if product_number is None:
print("秒杀尚未开始")
return False
# 如果用户已经秒杀成功了
if redis_client.sismember(SUCCESS_USER_KEY, user):
print("你已秒杀成功,不能重复购买")
return False
# 如果库存不足,秒杀失败
if int(product_number) <= 0:
print("没有库存了")
return False
# 商品数量减1
redis_client.decr(PRODUCT_NUMBER_KEY)
# 记录秒杀成功的用户
redis_client.sadd(SUCCESS_USER_KEY, user)
return True
except LockError:
print("秒杀失败")
return False