Redis原子操作与其业务场景

Redis单线程到底指什么?

执行 Redis 命令的核心模块是单线程的,而不是整个 Redis 实例就一个线程,Redis 其他模块还有各自模块的线程的。
Redis 的瓶颈并不在 CPU,而在内存和网络
其实,Redis 4.0 开始就有多线程的概念了,比如 Redis 通过多线程方式在后台删除对象、以及通过 Redis 模块实现的阻塞命令等。

由于redis命令核心是单线程的,所以操作都具有原子性,采用redis的原子性就能避免超卖等问题
数据库事务的情景下,原子性指的是:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。
Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。

在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题

  • 原子性:一个或某几个操作只能在一个线程执行完之后,另一个线程才能开始执行该操作。多线程环境下用“锁”、synchronized保证原子性
  • 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。Java提供了volatile关键字来保证可见性。
  • 有序性:即程序执行的顺序按照代码的先后顺序执行。

错误的代码与正确代码

// 错误的代码
public function sku(int $item_id) {
    // 获取库存
    $num = Yii::$app->redis->hget(self::REDIS_SKU, $item_id);
    // 判断库存为0
    if ($num <= 0) {
        return -1;
    }
    // 库存减少1
    return Yii::$app->redis->hincrby(self::REDIS_SKU, $item_id, -1);
}
  • 当并发请求时,该写法会导致出现超卖的情况。虽然hgethincrby都是原子性的,但两个命令组合起来就不具备原子性了。所有在两个命令之间其他客户端会出现读写脏数据的情况。
  • 假设当库存只有1时,同时来了3个请求,由于hget有原子性,三个请求依次执行hget命令,如果第一个用户在未执行hincrby之前,第三个用户就执行了hget,那么第二和第三个用户都会绕过$num <= 0判断。这三个用户都能抢购成功。而实际上只剩下一件库存可以抢了,库存会出现-2情况。
    // 正确的代码
    public function sku(int $item_id) {
      $rediskey = self::REDIS_SKU . ':' . $item_id;
      if (Yii::$app->redis->lpop($rediskey)) { // 移除并返回列表的第一个元素,无库存时返回false 
          //return Yii::$app->redis->llen($rediskey); // 减少后的库存
          return 1; // 库存减少成功
      }
      return -1; // 无库存
    }
  • 由于lpop操作有原子性,所以高并发时无法绕过if判断,从而不会出现超卖的情况。这里初始化的时候使用了队列,队列的初始化和数字库存的初始化相比稍微麻烦些,那么有没有其他改进方案呢?这里的原理其实还是使用了lpop的原子性,lpop命令有就减少队列,没有就返回null
    // 改进的代码 使用redis+lua
    public function sku(int $item_id) {
      $script = <<<EOT
    if tonumber(redis.call("get",KEYS[1])) > 0 then
      return redis.call("DECRBY",KEYS[1],1)
    else
      return -1
    end
    EOT;
      $rediskey = self::REDIS_SKU . ':' . $item_id;
      return Yii::$app->redis->eval($script, 1, $rediskey);
    }
  • 需要改进这个库存安全,需要满足两个条件:1、一个命令完成库存减1;2、库存为0时不减1,且返回空;目前就队列(list)的lpop与rpop可以实现;
  • Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。

业务场景

扩展知识

Redis事务

在redis中,对于一个存在问题的命令,如果在入队的时候就已经出错,整个事务内的命令将都不会被执行(其后续的命令依然可以入队),如果这个错误命令在入队的时候并没有报错,而是在执行的时候出错了,那么redis默认跳过这个命令执行后续命令。也就是说,redis只实现了部分事务。

总结redis事务的三条性质:

  • 单独的隔离操作:事务中的所有命令会被序列化、按顺序执行,在执行的过程中不会被其他客户端发送来的命令打断
  • 没有隔离级别的概念:队列中的命令在事务没有被提交之前不会被实际执行
  • 不保证原子性:redis中的一个事务中如果存在命令执行失败,那么其他命令依然会被执行,没有回滚机制

Redis锁

参考资料

Redis实现原子操作的两种方式与商品入库出库解决方案
基于redis实现的扣减库存
Redis的事务不是原子性的

此处评论已关闭