浅谈Redis如何应对并发访问
作者:剑圣无痕
前言
项目中经常会遇到这种场景,我们需要先将Redis数据读取到本地,然后进行修改,修改完成后在将数据写回Redis,这种读取-修改-写回操作,我们称之为RMW操作。当有多个客户端对同一份数据执行RMW操作的话,Redis如何保证RMW操作涉及的代码以原子性方式执行?
原子性操作
Redis的原子性操作是一种无锁操作,即可以保证并发控制,还能减少系统对并发性能的影响,
单命令模式
把Redis多个操作实现成一个操作,即为单命令模式。
Redis提供了INCR/DECR命令,可以对数据进行增值/减值操作,而且它们本身就是单个命令操作,Redis单线程模式,执行命令时具有互斥性。
示例说明
public Long getIncrNumber(String key,long alive) { RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory()); Long incrNum = entityIdCounter.getAndIncrement(); if (null == incrNum || incrNum.longValue()==0) { entityIdCounter.expire(alive,TimeUnit.MILLISECONDS); incrNum = entityIdCounter.getAndIncrement(); } return incrNum; }
说明:采用Reids的INCR命令,如果不存在的key则设置过期时间,如果key存在则进行递增操作返回。所以如果我们执行RMW操作进行相关的递增或者递减操作时,Redis提供的INCR和DECY命令可以保证并发控制。
多命令模式
当我们不是执行简单的加加减减操作,而是更加复杂的逻辑判断或者其他操作时,Redis是无法保证原子性,所以需要将多个操作写到一个Lua脚本中,Redis会把Lua脚本作为一个整体执行,在执行过程中不会被其他命令打断,从而保证了操作的原子性。
lua简介
Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
示例说明
接口进行限流操作,同一用户3秒内不能重复访问,我们可以通过lua脚本来实现。
local key = KEYS[1] local count = tonumber(ARGV[1]) local time = tonumber(ARGV[2]) local current = redis.call('get', key) if current and tonumber(current) > count then return tonumber(current) end current = redis.call('incr', key) if tonumber(current) == 1 then redis.call('expire', key, time) end return tonumber(current)
说明:key、count、time为三个传入参数,分别代表Redis的key、次数和过期时间。通过get获取key对应的值,获取的值为时间内访问接口的次数,如果为第一次访问则返回的为null,此时需要对当前key进行自增1操作,如果返回为数字,则需要判断返回的数字是否已经超过了cout值,如果超过说明已经超过限流了,直接返回。
建议
- 1.编写lua脚本不要进行复杂耗时的计算逻辑,否则执行lua时间过长,会导致Redis主线程阻塞。
- 2.lua脚本尽量简单,不要把所有的操作都放入到lua脚本中执行,这样会导致Redis执行脚本的时间增加,同时也会降低Redis的并发性能。
事务
关于事务保证原子性,采用的watch命令其原理和乐观锁的实现原理类似,详情可以参考juejin.cn/post/712582… 文章,本文就不在具体阐述。
加锁
关于Redis的分布式锁的实现,后续的章节进行详情说明。高并发环境下加锁虽然能够保证正确性,但是也会带来其他的问题:
- 加锁操作过多会降低系统的并发访问性能
- Redis客户端要加锁,需要使用分布式锁,而分布式锁实现复杂,增加复杂度。
总结
本文讲解了针对并发访问Redis如何保证原子性操作,针对不同的业务场景,选择合适的方案,更多相关Redis 并发访问内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!