• 如何应对Redis并发访问带来的问题


    前言

    我们在使用Redis的过程中,难免会遇到并发访问及数据更新的问题。但很多场景对数据的并发修改是很敏感的,比如库存数据如果没有做好并发读取和更新的版本控制,就会导致严重的业务问题。今天就来说说应该如何做好并发访问及数据更新问题。

    什么场景需要控制并发访问

    需要控制并发访问,说明这些并发的访问可能会对其他的访问造成影响。比如上面提到的库存问题,若同一时期有多个客户端访问商品A的库存数据,并且可能要更更新库存数据,这时候就需要对并发访问进行控制了。

    说到底,并发访问需要控制的就是对数据的更新动作。 一般来说,客户端要进行数据更新时可分为2个步骤:

    1. 客户端读取Redis数据到本地。
    2. 确认数据后,修改Redis的数据。

    单个访问来看,这个过程并没什么问题。但是并发多了,一分为二的过程就会造成数据错误的问题。这里还是用库存的例子来说:

    • 时间a::客户端1读取到库存=10,我们需要对库存+1=11的操作。
    • 时间b:客户端2读取到的库存也是10,这次要对库存-1=9的操作。
    • 时间c:客户端1将+1后的值11写回到Redis中。
    • 时间d:客户端2将-1后的值9写回到Redis中。

    这样下来,很明显能发现库存数据错了。10+1-1 = 10,正确库存是10,而上述场景最后为9。

    由此可见,这个一分为二的操作不具有原子性,从而产生了错误的结果。类型这种场景很多,因此我们需要对这些并发访问的场景加以控制。

    并发访问的控制方法

    Redis并发访问的控制,总的来说有2种方式。分别是加入锁机制让一系列操作原子化

    一、加入锁机制

    首先第一点,加入锁机制是很常见的解决方案。简单来说就是一个客户端访问数据之前,先要获取锁,等数据操作完之后再解锁。而在这个客户端拥有锁的过程中,其他客户端如果也想访问修改该数据,必须得等锁释放了之后,获取到了锁才行。

    加锁这个方案是可以解决并发访问的数据准确问题,但放在redis这个场景中并不是很好。首先,Redis作为缓存本身并发访问就很多,频繁的加锁解锁,会大大降低redis的访问性能;然后,Redis的客户端在要加锁时,需要用到分布式锁。我们又得用额外的精力去维护这个分布式锁。

    二、操作原子化

    操作原子化,也就是让要执行的一系列动作都保持原子性操作。它的优点就是不需要加入额外的锁机制。并发的数据准确性达到了,对Redis的性能也不会有太大的影响。

    Redis要实现原子操作,总结有2种方式:

    • 单命令操作:也就是Redis中的INCR、HINCRBY等命令,直接将简单的加减操作合成一个命令执行;
    • Lua脚本:借助Lua脚本,让多个操作在Lua脚本上实现原子性操作。

    1.单命令操作

    首先,单命令操作,将数值的加减直接用Redis命令来执行。像string的加减可用INCR、DECR操作,hash列表field的加减可用HINCRBY操作。

    比如下面截图,两个客户端在不同时刻读取的linux_pids a值为4,各自+1、-1后a值为4。结果是正确的。

    由此可见,用Redis的INCR、DECR等命令可以解决数值简单增减的并发场景。但如果我们对数据的更新不仅仅是简单的加减操作时,Redis的这些命令就无能为力了。此时我们可以考虑另一种方案:Lua脚本。

    2.Lua脚本

    Lua语言是由C写的,因此支持多平台和系统。从Redis2.6开始,Redis就内置了Lua解释器,我们能直接用Redis客户端来执行lua脚本。

    我们可以将需要执行的一系列操作用Lua脚本写好,然后用Redis执行它。Lua脚本的方法能保证原子性操作的原因是:Redis会将Lua脚本一次性执行,也就是说执行Lua脚本是0-1的操作,要么成功,要么失败。可以理解成MySQL的事务特性。

    Redis使用lua脚本有2种方式:

    • 客户端中使用:用到script load脚本内容、evalsha等命令执行
    • 直接执行lua脚本。

    我们一般用第二种方式来执行。

    1. 客户端使用方法:

    先用script load加载脚本命令,再用evalsha执行加载得到的sha1值。

    1. 127.0.0.1:6379> script load "return 'hello'"
    2. "1b936e3fe509bcbc9cd0664897bbe8fd0cac101b"
    3. 127.0.0.1:6379> evalsha "1b936e3fe509bcbc9cd0664897bbe8fd0cac101b" 0
    4. "hello"
    5. 复制代码
    1. 再来看看Redis使用Lua脚本的语法:
    1. redis-cli --eval {lua_path} KEYS[1] KEYS[2]... , ARGV[1] ARGV[2]...
    2. --eval: 执行lua脚本的命令
    3. {lua_path}: lua脚本的路径
    4. KEYS[1] KEYS[2]: lua脚本中要操作的redis键,我们可以在lua脚本中用KEYS[1],KEYS[2],KEYS[3]指定多个
    5. ARGV[1] ARGV[2]: 传入到lua脚本的参数,在脚本中用ARGV[1],ARGV[2]...来获取。
    6. 复制代码

    Redis使用lua脚本的场景很多,最经典的案例当属利用lua来控制某个IP的访问频率了。比如说需要防止恶意访问网站的行为,我们规定1分钟内访问次数不能超过30次,实现的方法有很多,比如说漏桶方案、令牌桶方案,但使用最多的还是Redis+lua的分布式限流方案。

    我们用lua脚本(test_lua.script)来简单实现一下上述功能,就是1分钟内若访问次数超过30,直接拦截,否则访问次数+1:

    1. -- 限流的key
    2. local limit_key = KEYS[1]
    3. -- 限流次数
    4. local limit_nums = 30
    5. -- 当前访问次数
    6. local current_num = tonumber(redis.call('get', limit_key) or 0)
    7. -- 超出限流次数
    8. if current_num + 1 > limit_num
    9. then
    10. return '超出访问次数'
    11. -- 没有超出限流数,访问次数+1
    12. else
    13. redis.call("INCRBY", limit_key, "1")
    14. -- 第一次访问,设置过期时间
    15. if current_num == 0 then
    16. redis.call("expire", limit_key, "60")
    17. return current + 1
    18. end
    19. 复制代码

    用Redis执行,命令如下:

    1. redis-cli --eval test_lua.script limit_key
    2. 复制代码

    小结

    本文介绍了Redis并发访问的控制问题,以及如何保证并发操作的原子化。原子化操作可通过单命令操作和Lua脚本的方式实现。

    我们在应对相关问题时,可根据需要选择对应方案解决之。

  • 相关阅读:
    如何查询外文文献?
    MySQL8.0.26—Linux版安装详细教程
    Docker之nacos集群部署(详细教你搭建)
    神奇英语语法系列(四)——非谓语
    在 GNU/Linux 中使用 GNUInstallDirs 优化 cmake 安装路径
    二十、泛型(1)
    Digi重启XBee-Pro S2C生产,有些差别需要注意
    基于javaweb谣言平台系统
    springSecurity登录的全过程
    【强化学习】深度确定性策略梯度(DDPG)算法求解 Pendulum 问题 + Pytorch代码实战
  • 原文地址:https://blog.csdn.net/m0_71777195/article/details/128094212