赞
踩
目录
Redis的快速性能主要归因于其基于内存的数据存储、单线程无锁并发模型、使用C语言实现、精心设计的数据结构、对磁盘I/O的谨慎处理以及高效的网络通信机制。这些特性共同作用,使得Redis能够在处理大量高速数据访问场景时表现出卓越的性能。
Redis使用单线程模型是基于内存操作的高效性、对I/O多路复用技术的充分利用、避免上下文切换成本、简化编程与测试,以及历史沿革与兼容性等方面的综合考量。这一设计使得Redis能够在保证高吞吐量的同时,保持代码简洁、执行确定性强,并适应其作为内存数据库的核心应用场景。
内存操作高效性: Redis是一个基于内存的数据存储系统,它的大部分操作(如读取、写入、哈希计算等)都是在内存中完成的,这些操作本身非常快速,几乎没有明显的CPU瓶颈。因此,相比于通过多线程来并行处理请求以提高CPU利用率,单线程模型足以高效地处理大量的内存操作。
简化数据结构与算法设计: 单线程环境使得Redis可以避免复杂的锁机制,确保了数据结构操作的原子性,无需担心并发写入导致的数据竞争问题。这大大简化了代码实现,降低了编程难度和潜在的bug风险,同时也提高了系统的稳定性和可维护性。
充分利用I/O多路复用技术: 即使是单线程,Redis依然能够高效地服务于大量并发客户端。它采用了I/O多路复用技术(如epoll、kqueue),通过一个线程轮询监听多个文件描述符(即连接),当某个描述符就绪(如可读或可写)时,才进行相应的I/O操作。这种模型使得Redis在单线程中就能同时处理多个客户端的请求,而非阻塞等待某个客户端的响应,从而最大化地利用了网络资源,避免了无谓的线程切换带来的上下文切换开销。
避免上下文切换成本: 在多线程环境中,线程间的上下文切换会消耗一定的系统资源。由于Redis的主要性能瓶颈在于网络延迟和内存访问速度,而非CPU计算能力,因此,避免上下文切换有助于保持高性能。单线程模型保证了所有的操作在一个连续的执行流中完成,避免了线程调度带来的额外开销。
简化编程与测试: 单线程模型使得Redis的编程逻辑相对简单,开发者无需处理多线程环境中的同步问题、死锁等问题,这有利于代码的编写、调试和测试。同时,单线程也使得Redis的执行行为更易于预测,对开发者更加友好。
历史沿革与兼容性: Redis早期设计时选择了单线程模型,并且在实践中证明了其在许多场景下的高效性。随着Redis的发展,尽管在某些版本(如Redis 6)中引入了多线程特性用于特定场景(如网络数据传输),但其核心处理逻辑仍然保持单线程,以保持向后兼容性和对已有部署的平滑升级。
缓存穿透
缓存穿透是指查询的数据既不在缓存中,也不在数据库中。通常由非法或不存在的key引发,这类请求持续不断地直接打到数据库,对数据库造成压力。
解决方案:
缓存击穿
缓存击穿是指针对某个热点key,缓存刚好失效,而此时大量并发请求同时访问该key,所有请求都直接穿透到数据库,对数据库造成巨大压力。
解决方案:
setnx
或lua脚本
):在缓存失效时,第一个请求去数据库加载数据的同时,其他请求等待锁释放,待第一个请求将数据写回缓存后,其他请求再从缓存获取,避免大量请求穿透到数据库。缓存雪崩
缓存雪崩是指在某一时刻,大量缓存在同一时间点失效,导致大量请求直接落至数据库,造成数据库压力激增,严重时可能导致数据库崩溃。
解决方案:
产生脏数据的概率较大(若出现脏数据,则意味着再不更新的情况下,查询得到的数据均为旧的数据)。
比如两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
产生脏数据的概率较小,但是会出现一致性的问题;若更新操作的时候,同时进行查询操作并命中,则查询得到的数据是旧的数据。但是不会影响后面的查询。
比如一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后之前的那个读操作再把老的数据放进去,所以会造成脏数据。
Redis 通过主从加集群架构,实现读写分离,主节点负责写,并将数据同步给其他从节点,从节点负责读,从而实现高并发。
Redis通过原生支持原子操作的命令、事务机制、Lua脚本执行以及各种锁机制,从不同层面确保了数据操作的原子性。这些机制确保了即使在高并发环境下,对Redis数据的访问和更新也能保持一致性和完整性,不会出现数据竞争或中间状态的问题。
原子操作命令: Redis提供了众多原生支持原子操作的命令,如SET
、GET
、HSET
、SADD
、INCR
、DEL
等。这些命令在Redis内部实现为不可分割的操作,从接收到命令到完成数据更新的全过程不会被其他命令打断,保证了单一操作的原子性。即使在高并发环境下,对同一键的原子操作命令也不会相互干扰,始终保持数据的完整性。
事务(Transactions): Redis支持事务机制,通过MULTI
、EXEC
、DISCARD
和WATCH
等命令实现。事务允许将一组命令作为一个整体执行,保证这些命令的原子性:
MULTI
:开始一个事务,后续的命令被放入队列中暂不执行。QUEUED
响应。EXEC
:执行事务中的所有命令。在此期间,Redis会锁住涉及的键,确保事务内的命令序列按预定顺序执行,不受其他客户端干扰。如果事务中任何一个命令执行失败,其余命令都不会被执行,整个事务被视为失败。DISCARD
:取消当前事务,清除已入列的命令。WATCH
:监视一个或多个键,如果在EXEC
前这些键被其他客户端修改,那么当前事务将被打断(EXEC
返回空结果)。通过事务,Redis能保证一个事务内所有命令的原子性,即要么全部执行成功,要么全部不执行,不会出现部分命令生效、部分命令失败的情况。
Lua脚本: Redis支持在服务器端执行Lua脚本,通过EVAL
、EVALSHA
等命令提交。Lua脚本在Redis内部执行时,会被视为一个单一的操作,从开始到结束不会被其他客户端的命令打断,因此脚本内的所有操作具有原子性。使用Lua脚本可以实现更复杂的原子操作逻辑,同时避免了客户端在多个命令间进行协调的复杂性。
锁机制: 对于跨越多个键或者需要在多个客户端间协调的更复杂场景,Redis提供了多种锁实现,如SETEX
、SETNX
、BLPOP
、BRPOP
、LPUSHX
、RPUSHX
等命令可以用于实现特定条件下的原子操作。此外,Redis还支持更高级的分布式锁,如Redlock
算法,通过在多个独立的Redis实例上获取和释放锁来确保在分布式环境下的操作原子性。
Redis凭借其高性能、丰富的数据结构、原子操作和灵活的使用方式,适用于各种需要快速访问、高并发、数据共享和实时处理的场景,尤其在缓存、排行榜、计数器、分布式会话、分布式锁、社交网络功能、消息队列、数据存储、限流器、登录鉴权以及实时分析与监控等方面有着广泛应用。
缓存(Cache):
排行榜(Leaderboards):
计数器(Counters):
INCR
、DECR
等原子命令实现计数器功能,常用于统计页面访问量、用户点击数、商品库存变化等需要快速累加或减量的场景。分布式会话(Distributed Session Management):
分布式锁(Distributed Locking):
SETNX
、EXPIRE
等命令实现分布式锁,确保在分布式系统中对共享资源的互斥访问,如库存扣减、任务分配、数据库事务等需要防止并发冲突的场景。社交网络功能:
消息队列(Message Queue):
数据存储(Data Storage):
限流器(Rate Limiter):
登录鉴权(Authentication):
实时分析与监控:
SETNX
(SET if Not eXists)是Redis提供的一种命令,用于原子性地设置键值对,仅在键不存在时才执行设置操作。如果键已经存在,命令将不做任何改变并返回0
。反之,如果键不存在,命令将成功创建键并设置其值,同时返回1
。SETNX
常用于实现分布式锁、计数器初始化等场景,确保在多客户端并发访问时,只有第一次执行命令的客户端能够成功设置键值。
以下是使用SETNX
命令的一些典型场景和示例:
在分布式系统中,SETNX
可用于实现简单的分布式锁,确保在同一时间内只有一个客户端能持有锁:
- # 客户端A尝试获取锁
- SETNX lock_key unique_identifier_A
-
- # 如果返回1,表示客户端A成功获取锁
- if [ $? -eq 1 ]; then
- # 执行临界区代码...
- # 在代码执行完毕后,释放锁
- DEL lock_key
- else
- # 返回0,表示锁已被其他客户端持有,客户端A等待或重试
- fi
注意,为了防止客户端在持有锁期间崩溃导致锁无法释放,通常还需要结合使用EXPIRE
命令为锁设置一个超时时间。此外,为了实现更健壮的分布式锁,现代实践中往往推荐使用SET
命令配合NX
和PX
选项,或者使用如Redlock算法这样的高级方案。
SETNX
可以确保某个计数器只被初始化一次,避免重复初始化导致的计数错误:
- # 初始化计数器,仅在未初始化时执行
- SETNX counter 0
-
- # 获取计数器当前值并递增
- INCR counter
假设需要确保某个任务仅被执行一次,可以先使用SETNX
创建一个标记键:
- # 检查任务是否已执行
- SETNX task_executed 1
-
- # 如果返回1,表示任务尚未执行
- if [ $? -eq 1 ]; then
- # 执行任务...
- fi
SETNX key value
总结来说,SETNX
命令在Redis中提供了一种简单而有效的原子性设置操作,尤其适用于需要确保某项操作仅被执行一次或某个资源仅被首次请求者占用的场景。在实际使用时,应结合具体业务需求和Redis的其他功能(如键过期、脚本等)来构建更为健壮和高效的解决方案。
Redis 支持的数据结构包括字符串、列表、哈希、集合、有序集合、位图、HyperLogLog 和地理位置。这些数据结构各具特色,适用于不同的应用场景,用户可以根据实际需求选择合适的数据结构来实现高效的数据存储和操作。
Redis 中 String 类型的底层实现采用了名为 Simple Dynamic String(SDS)的数据结构。SDS 是 Redis 团队设计的一种自定义字符串表示方式,它相较于传统的 C 语言字符串(以
\0
结尾的字符数组)具有多项优势,尤其是在性能、安全性、操作便利性等方面。
SDS 在结构中显式记录了字符串的长度(
len
字段),无需像 C 字符串那样通过遍历直到遇到\0
来确定长度,从而实现了 O(1) 时间复杂度获取字符串长度。
SDS 结构定义
- struct sdshdr {
- int len; // 记录字符串的长度(不包括末尾的 '\0')
- int free; // 记录已分配空间中未使用的字节数
- char buf[]; // 字符数组,用于存储字符串内容,末尾会自动添加 '\0'
- };
zset 是 Redis 中一个非常重要的数据结构,其底层是基于跳表(skip list) 实现的。
跳表是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为 O(logN)。简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供 O(logN) 的时间复杂度。
跳表为了避免每次插入或删除带来的额外操作,不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。而且新插入一个节点不会影响其它节点的层数。因此,插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。