失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Redis核心技术与实战-学习笔记(二十九):Redis并发控制

Redis核心技术与实战-学习笔记(二十九):Redis并发控制

时间:2023-07-09 14:53:41

相关推荐

Redis核心技术与实战-学习笔记(二十九):Redis并发控制

一.需要并发控制的原因

Redis不可避免的会遇到并发访问问题,比如多用户同时下单,就会对缓存在Redis中的商品库存并发更新,一旦有了并发操作,数据就会被修改,如果我们没有对并发写请求做好控制,就可能导致数据被改错,影响业务正常使用(数据库存数据错误,导致下单异常)

二.解决方案

加锁和原子性

加锁:

在读取数据前,客户端需要先获得锁,否则需要等待。当一个客户端获得锁后,就会一直持有这把锁直到客户端完成数据更新才释放这把锁。

存在问题:

加锁操作过多,会降低系统的并发访问性能;Redis客户端加锁,需要分布式锁实现复杂,需要额外的存储系统来提升加锁操作。

原子操作是另一种提升并发访问控制的方法

三.原子操作

原子操作:执行过程中保持原子性的操作,而原子操作执行时不需要加锁实现了无锁操作。这样既能保证并发控制还能减少对系统开发性能的影响。

四.并发访问中需要对什么进行控制

并发访问控制:对多个客户端访问操作同一份数据的过程进行控制,以保证任何一个客户端发送的操作在Redis实例上执行时具有互斥性。例如:客户端A访问操作在执行时,客户端B操作需要等到A操作结束后才能执行。

并发访问控制对应的操作主要是数据修改操作。

当客户端修改数据时,基本流程分为两步:

客户端先把数据读取到本地,在本地进行修改;客户端修改完数据后,在写回Redis

RMW操作:

读取-修改-写回(Read-Modify-Write)

当有多个客户端对同一份数据进行RMW操作的话,我们就需要让RMW操作涉及的代码以原子性方式执行。访问同一份数据的RMW操作代码,就叫做临界区代码。

五.多客户端更新商品库存

current=GET(id)current--SET(id,current)

客户端首先会根据商品id从Redis中读取商品当前的库存值current(Read)客户端对库存值减一(Modify)再把库存值写回Redis(write)当有多个客户端执行这段代码时,这就是一份临界区代码。

假设现在有两个客户端A和B,同时执行刚才的临界区代码就会出现错误:

客户端A在t1时去库存值10并扣减1t2时,客户端A还没有把扣除后的库存值9写回Redis此时,客户端B读到库存值10也减扣1,B记录的库存值也为9等到t3时,A往Redis写回库存值9t4时,B写回库存值9

错误:

客户端A和B对库存做了一次扣减,库存值应该为8所以这里的库存值明显更新错了。

产生原因

临界区代码中的客户端读取数据,更新数据,写回数据涉及了三个操作,而这三个操作在执行时候不具有互斥性。多个客户端基于相同的初始值进行修改,而不是基于前一个客户端修改后的值进行。

解决方案一:加锁

用锁将并行操作变成串行操作,串行操作具有互斥性,当一个客户端持有锁后,其他客户端只能等待锁释放后,才能拿到锁修改。

LOCK()current=GET(id)current--;SET(id,current)UNLOCK()

加锁会导致系统并发性能降低

后,才能拿到锁修改。

当客户端A加锁执行操作时;客户端B,C就需要等待A释放锁后,假设B拿到锁后,那么C还需要继续等待t1时段内只有A访问共享数据t2时段只有B能访问共享数据系统的并发性能下降

五.原子操作

原子性操作也能实现并发控制,但是原子性操作对系统并发性能的影响较小

Redis的两种原子操作:

把多个操作在Redis中实现成一个操作,也就是单命令操作把多个操作写到一个Lua脚本中,以原子性的方式执行单个lua脚本。

六.Redis本身的单命令操作

Redis是使用单线程来串行处理客户端的请求操作命令;当Redis执行某个命令操作,其他命令是无法执行的,这相当于命令操作时互斥操作。当然,Redis的快照生成,AOF重写这些操作,可以使用后台线程或者子进程执行,也就是和主线程的操作并行执行不过这些数据只是读取数据,不会修改数据,所以我们并需要对它们做并发操作。

存在问题:

虽然Redis单个命令操作可以原子性的执行,但是在实际操作中,数据修改可能包含多个操作,至少包含读数据,数据增减,写回数据三个操作,这显然就不是单个命令操作

Redis提供了INCR/DECR 命令

DECR 命令

如果我们执行RMW操作是对数据进行增减值的,Redis提供的原子操作INCR 和 DECR可以直接帮助我们进行并发控制。

如果我们执行后的操作并不能简单的增减数据,而是更加复杂的判断逻辑或者其他操作,那么Redis单命令操作已经无法保证多个操作的互斥执行

七.Lua脚本

Redis会把Lua脚本作为一个整体执行,在执行过程中不会被其他命令打断,从而保证lua操作的原子性 如果我们有多个操作要执行,又无法用INCR/DECR这种命令操作实现,就可以把这些要执行的操作编写到Lua脚本中使用Redis的eval命令执行脚本这样代码执行时候就具有互斥性

八.Lua的使用

当一个业务应用的访问要不过户增加时,我们有时需要限制某个客户端在一定时间范围内的访问次数比如爆款商品的购买限流,社交网络中每分钟点赞次数限制等

解决方案

我们把客户端IP作为key,把客户端的访问次数作为value。客户端每访问一次我们就用INCR增加访问次数。这种场景下,客户端限流其实同时包含对访问次数和时间范围的限制,例如每分钟的访问次数不能超过20我们在客户端第一次访问时,给对应键值对设置过期时间,丽日设置为60s后过期。同时客户端每次访问时,我们读取客户端当前的访问次数,如果次数超过阈值,就报错,限制客户端再次访问

//获取ip对应的访问次数current=GET(ip)//如果超过访问次数20就报错IF current==null && current >20throw new exceptionelse{//如果访问次数不足20次,增加一次访问计数value=INCR(ip)if(value == 1){//如果是第一次访问,将键值对的过期时间设置为60s后expire (ip,60)}}

对于这些操作,我们同样需要保证他们的原子性。否则如果客户端使用多线程访问,访问次数初始值是0,第一个线程执行了INCR(ip)操作后,第二个线程紧接着也执行了 INCR(ip)此时ip对应的访问次数就被增加到了2,我们就无法再对这个ip设置过期时间导致ip对应的客户端访问次数达到20次后,无法进行访问

我们可以使用Lua脚本保证并发控制

访问次数加一判断访问次数是否为1设置过期时间

local currentcurrent = redis.call("incr",KEYS[1])if tonumber(current) == 1 thenredis.call("expire",KEYS[1],60)end

设置脚本名称为lua.script,我们接着就可以使用Redis客户端,带上eval选项来执行该脚本。

脚本所需的参数通过以下命令中的keys和args进行传递:

Redis -cli --eval lua.script keys,args

访问次数加一,判断访问次数是否为1,设置过期时间这三个操作可以原子性的执行。

在编写Lua脚本时,我们要避免把不需要做并发控制的操作写入脚本中。

九.课后问题:

Redis 在执行 Lua 脚本时,是可以保证原子性的,那么,在我举的 Lua 脚本例子(lua.script)中,你觉得是否需要把读取客户端 ip 的访问次数,也就是 GET(ip),以及判断访问次数是否超过 20 的判断逻辑,也加到 Lua 脚本中吗?

答案:

这2个逻辑都是读操作,不会对资源临界区产生修改,所以不需要做并发控制。减少 lua 脚本中的命令,可以降低Redis执行脚本的时间,避免阻塞 Redis。

使用lua脚本注意点:

lua 脚本尽量只编写通用的逻辑代码,避免直接写死变量。变量通过外部调用方传递进来,这样 lua 脚本的可复用度更高。建议先使用SCRIPT LOAD命令把 lua 脚本加载到 Redis 中,然后得到一个脚本唯一摘要值,再通过EVALSHA命令 + 脚本摘要值来执行脚本,这样可以避免每次发送脚本内容到 Redis,减少网络开销。

如果觉得《Redis核心技术与实战-学习笔记(二十九):Redis并发控制》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。