redis练习系列-03.事务与商品秒杀练习

462 阅读2分钟

Redis 的事务就是将多个命令序列化,按顺序执行,并且它们在执行的过程中,不会被其他客户端发来的命令请求所打断。

一个事务从开始到执行会经历以下三个阶段:

  • 开始事务,使用 MULTI命令。
  • 命令入队,正常的操作命令。
  • 执行事务, EXEC命令。

也可以在 EXEC 之前用 DISCARD 放弃这个事务。

另外需要注意的是,

  1. 如果入队过程中命令报错了,执行时会直接报错,所有的命令都不会执行。

  2. 如果入队过程中没有命令报错,而在执行过程中,某个命令执行失败,则不影响其他命令的执行。

下面练习一下 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

代码可见:github.com/luxu1220/re…