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);
}
- 当并发请求时,该写法会导致出现
超卖
的情况。虽然hget
与hincrby
都是原子性的,但两个命令组合起来就不具备原子性了。所有在两个命令之间其他客户端会出现读写脏数据的情况。 - 假设当库存只有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的事务不是原子性的
最后更新于 2022-04-18 15:48:15 并被添加「redis」标签,已有 706 位童鞋阅读过。
本站使用「署名 4.0 国际」创作共享协议,可自由转载、引用,但需署名作者且注明文章出处
此处评论已关闭