ICode9

精准搜索请尝试: 精确搜索
首页 > 数据库> 文章详细

29 | 无锁的原子操作:Redis如何应对并发访问?

2021-12-20 11:33:50  阅读:162  来源: 互联网

标签:无锁 并发 Redis 29 访问 操作 执行 客户端


Redis核心技术与实战

实践篇

29 | 无锁的原子操作:Redis如何应对并发访问?

为了保证并发访问的正确性,Redis 提供了两种方法,分别是加锁和原子操作。

加锁是一种常用的方法,在读取数据前,客户端需要先获得锁,否则就无法进行操作。当一个客户端获得锁后,就会一直持有这把锁,直到客户端完成数据更新,才释放这把锁。加锁有两个问题:

  • 一个是,如果加锁操作多,会降低系统的并发访问性能;
  • 第二个是,Redis 客户端要加锁时,需要用到分布式锁,而分布式锁实现复杂,需要用额外的存储系统来提供加解锁操作。

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

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

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

并发访问控制对应的操作主要是数据修改操作。当客户端需要修改数据时,基本流程分成两步:

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

这个流程叫做“读取 - 修改 - 写回”操作(Read-Modify-Write,简称为 RMW 操作)。当有多个客户端对同一份数据执行 RMW 操作,需要让 RMW 操作涉及的代码以原子性方式执行。访问同一份数据的 RMW 操作代码,就叫做临界区代码。

如果对临界区代码的执行没有控制机制,就会出现数据更新错误。假设现在有两个客户端 A 和 B,同时执行刚才的临界区代码,就会出现错误,如下图:
在这里插入图片描述
出现图中现象,是因为临界区代码中的客户端读取数据、更新数据、再写回数据涉及三个操作,而这三个操作在执行时并不具有互斥性,多个客户端基于相同的初始值进行修改,而不是基于前一个客户端修改后的值再修改。

Redis 的两种原子操作方法

为了实现并发控制要求的临界区代码互斥执行,Redis 的原子操作采用了两种方法:

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

Redis 使用单线程来串行处理客户端的请求操作命令,所以,当 Redis 执行某个命令操作时,其他命令无法执行,相当于命令操作互斥执行。当然,Redis 的快照生成、AOF 重写这些操作,可以使用后台线程或者是子进程执行,也就是和主线程的操作并行执行。不过,这些操作只是读取数据,不会修改数据,所以,并不需要对它们做并发控制。
虽然 Redis 的单个命令操作可以原子性地执行,但是在实际应用中,数据修改时可能包含多个操作,至少包括读数据、数据增减、写回数据三个操作,非原子操作,无法保证并发安全。

Redis 提供了 INCR/DECR 命令,把这三个操作转变为一个原子操作。INCR/DECR 命令可以对数据进行增值 / 减值操作,而且它们本身就是单个命令操作,Redis 在执行它们时,本身就具有互斥性。如果执行的 RMW 操作是对数据进行增减值的话,Redis 提供的原子操作 INCR 和 DECR 可以直接进行并发控制。

但是,如果要执行的操作不是简单地增减数据,而是有更加复杂的判断逻辑或者是其他操作,Redis 的单命令操作则无法保证多个操作的互斥执行。 所以,这个时候,需要使用Lua 脚本的方法。
Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证 Lua 脚本中操作的原子性。 如果有多个操作要执行,但是又无法用 INCR/DECR 这种命令操作来实现,就可以把这些要执行的操作编写到一个 Lua 脚本中。然后,可以使用 Redis 的 EVAL 命令来执行脚本。这样,这些操作在执行时就具有了互斥性。

示例
当一个业务应用的访问用户增加时,有时需要限制某个客户端在一定时间范围内的访问次数,比如爆款商品的购买限流、社交网络中的每分钟点赞次数限制等。
在这种场景下,客户端限流其实同时包含了对访问次数和时间范围的限制。伪代码如下:

//获取ip对应的访问次数
current = GET(ip)
//如果超过访问次数超过20次,则报错
IF current != NULL AND current > 20 THEN
    ERROR "exceed 20 accesses per second"
ELSE
    //如果访问次数不足20次,增加一次访问计数
    value = INCR(ip)
    //如果是第一次访问,将键值对的过期时间设置为60s后
    IF value == 1 THEN
        EXPIRE(ip,60)
    END
    //执行其他操作
    DO THINGS
END

对于这些操作,需要保证它们的原子性。否则,如果客户端使用多线程访问,访问次数初始值为 0,第一个线程执行了 INCR(ip) 操作后,第二个线程紧接着也执行了 INCR(ip),此时,ip 对应的访问次数就被增加到了 2,就无法再对这个 ip 设置过期时间了。这就会导致,这个 ip 对应的客户端访问次数达到 20 次之后,就无法再进行访问。即使过了 60s,也不能再继续访问,显然不符合业务要求。

这个例子中的操作无法用 Redis 单个命令来实现,此时,可以使用 Lua 脚本来保证并发控制。可以把访问次数加 1、判断访问次数是否为 1,以及设置过期时间这三个操作写入一个 Lua 脚本,如下所示:

# lua.script
local current
current = redis.call("incr",KEYS[1])
if tonumber(current) == 1 then
    redis.call("expire",KEYS[1],60)
end

接着就可以使用 Redis 客户端,带上 eval 选项,来执行该脚本。脚本所需的参数将通过以下命令中的 keys 和 args 进行传递。

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

即使客户端有多个线程同时执行这个脚本,Redis 也会依次串行执行脚本代码,避免了并发操作带来的数据错误(Redis 使用单线程来串行处理客户端的请求操作命令)。

标签:无锁,并发,Redis,29,访问,操作,执行,客户端
来源: https://blog.csdn.net/TQ20160412/article/details/122000202

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有