写在前面
本文一起看下Redis的并发访问控制。
1:单线程的Redis为什么会有并发问题
我们知道,Redis是单线程的,为什么还是会有并发问题呢?没错,如果是单命令操作的话肯定没有并发问题,但考虑事务的场景,比如库存量,我们会按照如下的步骤进行修改:
1:读取库存量
2:减少库存量
3:写回库存量
我们把这个流程叫做读取-修改-写回
,Read-Modify-Write,简称RMW操作。以上的3个步骤,对应到Redis其实是3个命令,当存在并发时,就可能导致库存量错误了,如下图,库存量最终应该是8,但由于并发导致库存量最终是9:
2:如何解决并发问题
解决并发问题的核心点就是,让操作以原子性的方式执行,首先单命令Redis是天然支持的,另外还有就是lua脚本,对于lua脚本Redis会以原子性的方式执行,分别来看下。
2.1:单命令
对于RMW操作,正常伪代码可能如下:
这类代码,也有专门的名称,叫做临界代码块
,即同时只能被一个线程执行的代码块,因为这类问题比较常见,所以Redis提供了incr和desc单命令来代替以上代码,如下:
127.0.0.1:6379> set age 99
OK
127.0.0.1:6379> incr age
(integer) 100
127.0.0.1:6379> decr age
(integer) 99
127.0.0.1:6379> decr age
(integer) 98
但是对于具有逻辑判断的场景,就无法简单的使用一个命令来完成了,此时我们就需要使用lua脚本。
2.2:lua脚本
考虑限流场景,比如点赞,每小时来自同一个IP的点赞数不能超过100个,此时代码可能如下:
if (containsKey(ip)) {
int 点赞数 = get(ip);
if (赞点数 > 100) {
sout("点赞数超过100");
} else {
set(ip, 点赞数+1);
}
} else {
// 点赞数设置为1,并设置过期时间为3600秒
set(ip, 1, 3600);
}
此时如果有来自同一IP的大量并发的话,最终的点赞数肯定会大于100,这不是我们想要的结果,此时,我们就可以将这块代码定义在lua脚本中,如rateLimitLua.script
,可能如下:
IF containsKey(ip)) THEN
点赞数 = get(ip);
IF (赞点数 > 100) THEN
sout("点赞数超过100");
ELSE
set(ip, 点赞数+1);
END
ELSE
// 点赞数设置为1,并设置过期时间为3600秒
set(ip, 1, 3600);
END
然后就可以通过eval来执行了redis.cli --eval rateLimitLua.script keys,args
,并且以原子的方式执行。
写在后面
参考文章列表:
Redis 单线程 为何却需要事务处理并发问题 。
Redis事务、MULTI 命令和EXEC 命令 。