1.浏览器缓存过期机制
1.1 最后修改时间 last-modified
浏览器缓存机制是优化网页加载速度和减少服务器负载的重要手段。以下是关于浏览器缓存过期机制、
Last-Modified
和ETag
的详细讲解:一、
Last-Modified
头部
定义:
Last-Modified
表示服务器上资源的最后修改时间。作用:用于资源的条件请求,帮助浏览器判断缓存的资源是否是最新的。
工作流程:
浏览器第一次请求资源时,服务器返回资源内容和
Last-Modified
时间。下次请求同一资源时,浏览器发送
If-Modified-Since
头部,值为之前的Last-Modified
时间。服务器比较资源的当前修改时间与
If-Modified-Since
的值:
- 如果资源未修改,返回
304 Not Modified
,浏览器继续使用缓存。- 如果资源已修改,返回新的资源内容和更新后的
Last-Modified
时间。示例:
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
二、
ETag
头部
定义:
ETag
(Entity Tag)是服务器为资源生成的唯一标识符,通常是资源内容的哈希值或版本号。作用:比
Last-Modified
更加精确,用于验证资源是否变化。工作流程:
浏览器第一次请求资源时,服务器返回资源内容和
ETag
值。下次请求同一资源时,浏览器发送
If-None-Match
头部,值为之前的ETag
。服务器比较当前资源的
ETag
与If-None-Match
的值:
- 如果
ETag
未变化,返回304 Not Modified
,浏览器继续使用缓存。
- 如果
ETag
变化,返回新的资源内容和新的ETag
值。示例:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
五、
Last-Modified
vsETag
- 精确度:
Last-Modified
仅记录最后修改时间,可能无法检测到在同一秒内的多次修改。ETag
通常基于内容的哈希值,能够更精确地检测到任何变化。- 性能:
- 生成
ETag
可能需要更多的计算资源,尤其是在大规模资源或高频请求的情况下。Last-Modified
相对简单,性能开销较小。- 使用场景:
- 对于静态资源,
ETag
更加适用。- 对于动态资源,可以结合
Last-Modified
和其他缓存策略使用。六、最佳实践
- 合理设置缓存策略:
- 对于不经常变化的静态资源,设置较长的
max-age
以充分利用缓存。- 对于经常变化的资源,使用较短的
max-age
或结合验证机制。- 使用
ETag
和Last-Modified
:
- 同时使用两者可以提供更可靠的缓存验证,但需注意服务器的性能开销。
- 如果服务器性能有限,可以选择只使用其中一个。
- 版本化资源:
- 通过在资源URL中包含版本号(如
style.v1.css
),可以在资源更新时强制浏览器下载新版本,避免缓存问题。七、总结
浏览器缓存机制通过多种HTTP头部字段控制资源的缓存和过期,
Last-Modified
和ETag
是其中重要的验证手段。合理配置这些头部字段,可以显著提升网页性能,优化用户体验,同时有效管理服务器资源。
第一次访问
第二次访问
1.2 Etag 标记
但是如果访问的时间一样的,怎么办?If-Modefied精确到的是秒,要知道的是,计算机一秒中可以干好多事的,比如一秒中修改上千次的图片
# 使用 touch 模拟访问时间是一样的(移走1.jpeg,在重新上传一张图片,重命名为1.jpeg)
[root@Rocky9.4 html]#touch -t 202407020931.48 1.jpeg
第一次访问
第一次访问是由于我将时间设置成一样了,但是因为服务器返回的Etag是新的,而浏览器保存的还是旧的,所以Etag不一致,所以返回状态码是200
第二次访问
第二次访问,Etag也统一了,所以返回了状态码304
1.3 过期时间 expires 和 Cache-Control
一、浏览器缓存机制概述
浏览器缓存通过在本地存储网页资源(如HTML、CSS、JavaScript、图片等),避免每次访问网页时都从服务器重新下载这些资源,从而加快页面加载速度,提高用户体验,同时减少服务器带宽的使用。
明白了,你希望更详细地了解浏览器缓存中的
Expires
和Cache-Control
头部,以及它们之间的关系和具体应用。以下是更深入的讲解:二、
Expires
头部1. 定义与作用
Expires
是一个HTTP响应头,用于指定资源的绝对过期时间。它告诉浏览器在指定的时间之前,可以直接从缓存中使用该资源,而无需向服务器重新请求。2. 格式
Expires
的值是一个绝对的HTTP日期和时间,格式为:Wdy, DD Mon YYYY HH:MM:SS GMT
。示例:
Expires: Wed, 21 Oct 2025 07:28:00 GMT
3. 使用场景
- 适用于静态资源,如图片、CSS、JavaScript文件,这些资源不经常变化。
- 适合设置较长的缓存时间,减少浏览器对服务器的请求频率,提升加载速度。
4. 缺点
- 使用绝对时间,可能受客户端和服务器时间不同步的影响。
- 当资源更新时,若不改变
Expires
,可能导致浏览器继续使用过期的缓存,出现内容不一致的问题。三、
Cache-Control
头部1. 定义与作用
Cache-Control
是一个更为灵活和强大的HTTP响应头,用于控制缓存策略。它可以替代或补充Expires
头部,提供更精确的缓存控制。2. 常用指令
max-age=秒数
:指定资源在多少秒内被认为是新鲜的。max-age
的优先级高于Expires
。示例:
Cache-Control: max-age=3600
no-cache
:资源必须在使用前重新验证(即使资源没有过期)。示例:
Cache-Control: no-cache
no-store
:禁止任何形式的缓存,既不存储请求信息,也不存储响应信息。示例:
Cache-Control: no-store
public
:响应可被任何缓存区缓存,包括浏览器和中间缓存(如CDN)。示例:
Cache-Control: public
private
:响应仅为单个用户缓存,不能被共享缓存(如CDN)缓存。示例:
Cache-Control: private
must-revalidate
:一旦资源过期,必须向服务器验证其有效性。示例:
Cache-Control: must-revalidate
proxy-revalidate
:与must-revalidate
类似,但仅适用于共享缓存。示例:
Cache-Control: proxy-revalidate
3. 使用场景
- 动态资源:可以灵活设置缓存策略,如需要频繁更新但又希望利用缓存提升性能的资源。
- 细粒度控制:通过组合多个指令,实现更复杂的缓存策略。
4. 与
Expires
的关系
- 优先级:当同时存在
Cache-Control: max-age
和Expires
时,Cache-Control
优先级更高。- 推荐使用:现代浏览器和服务器更推荐使用
Cache-Control
,因为它更灵活且不依赖绝对时间。四、
Expires
与Cache-Control
的对比
特性 Expires
Cache-Control
类型 绝对时间 相对时间及其他缓存指令 格式 HTTP日期格式 指令列表 优先级 低于 Cache-Control
高于 Expires
灵活性 较低,只有一个绝对过期时间 高,可以组合多种指令控制缓存行为 推荐使用场景 主要用于向后兼容旧浏览器 现代Web应用的首选缓存控制方式 五、实际应用示例
1. 设置长时间缓存(适用于不经常变化的静态资源)
Cache-Control: public, max-age=31536000 Expires: Wed, 21 Oct 2025 07:28:00 GMT
- 解释:资源可以被公共缓存(如CDN)缓存,且在1年内(31536000秒)不需要重新验证。
2. 设置短时间缓存,需重新验证(适用于可能会频繁更新的资源)
Cache-Control: no-cache
- 解释:浏览器每次使用缓存前必须向服务器验证资源是否有更新。
3. 禁止缓存(适用于敏感数据)
Cache-Control: no-store
- 解释:禁止任何形式的缓存,确保每次请求都从服务器获取最新数据。
六、结合
ETag
和Last-Modified
使用缓存验证即使设置了
Cache-Control
或Expires
,浏览器在某些情况下仍可能需要验证缓存资源的有效性。此时,ETag
和Last-Modified
提供了有效的验证机制:
ETag
:提供资源的唯一标识符,确保缓存的资源与服务器上的一致。Last-Modified
:记录资源的最后修改时间,供浏览器进行条件请求。示例:
Cache-Control: max-age=3600, must-revalidate ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
七、最佳实践
- 优先使用
Cache-Control
:
- 由于其灵活性和优先级,现代Web开发中应优先配置
Cache-Control
头部。
- 合理设置
max-age
:
- 根据资源的更新频率,合理设置缓存时间。静态资源可以设置较长时间,动态资源设置较短时间或不缓存。
- 结合使用
ETag
和Last-Modified
:
- 提供双重验证机制,确保缓存的资源始终是最新的。
- 版本化静态资源:
- 通过在资源URL中添加版本号(如
style.v1.css
),确保资源更新时浏览器能够获取到最新版本,避免缓存问题。
- 使用CDN:
- 配合缓存头部,利用内容分发网络(CDN)提升全球范围内的资源加载速度,并有效管理缓存策略。
八、总结
Expires
和Cache-Control
都用于控制资源的缓存和过期,但Cache-Control
提供了更高的灵活性和优先级。ETag
和Last-Modified
是用于缓存验证的强大工具,确保浏览器使用最新的资源。- 最佳实践 是结合使用这些HTTP头部,合理设置缓存策略,提升Web应用的性能和用户体验。
1.4 CDN
CDN(内容分发网络,Content Delivery Network)是一种通过将内容复制并缓存到全球多个地理位置的服务器上,从而加速用户访问速度的技术。它主要的目的是提高网站或应用的性能、稳定性、可扩展性,同时减少服务器负载和带宽消耗。
一、CDN的工作原理
CDN的核心思想是将网站的静态资源(如HTML文件、CSS文件、JavaScript、图片、视频等)缓存到分布在全球的边缘服务器(Edge Servers)上。当用户请求访问某个资源时,CDN会根据用户的地理位置,选择距离用户最近的服务器提供资源,从而减少加载时间和提高访问速度。
1. 资源分发与缓存
- 资源分发:当你将资源上传到CDN服务时,CDN提供商会将这些内容分发到位于世界各地的数据中心。
- 缓存:CDN的服务器会将常用的静态内容缓存到本地存储中,当有新的请求时,如果内容已经存在并且没有过期,则直接返回缓存的内容。
2. 边缘服务器与原始服务器
- 边缘服务器(Edge Server):这些是部署在全球各地的服务器,负责将资源提供给终端用户。用户访问时,通常会被路由到离他们最近的边缘服务器,以减少延迟。
- 原始服务器(Origin Server):原始服务器是网站的源服务器,存储网站的所有内容。如果CDN的边缘服务器没有缓存某个请求的内容,它会从原始服务器获取并返回给用户。
3. 缓存策略
CDN通常会使用一些缓存策略来决定哪些内容需要缓存,以及缓存多久。常见的缓存策略包括:
- 缓存时间(TTL,Time to Live):决定缓存的有效期。例如,静态资源如图片、CSS文件可能会缓存较长时间,而动态内容可能缓存较短时间。
- 缓存控制(Cache-Control):通过设置HTTP头来控制缓存行为(如
max-age
、no-cache
)。- 动态内容缓存:CDN一般针对动态内容(如用户特定数据、实时信息)使用不同的缓存策略,可能会使用“按需缓存”或“低过期时间”的方式进行处理。
4. 智能路由与负载均衡
CDN通常会根据多个因素(如地理位置、网络负载、带宽等)选择最优的边缘服务器来响应用户请求。这一过程称为智能路由或负载均衡。通过此方式,CDN能够确保用户始终通过最快的路径获取到资源。
二、CDN的优势
- 提高加载速度
- 减少延迟:通过将内容分发到全球多个节点,用户总是能够从离自己最近的节点获取资源,从而大幅减少延迟,提高加载速度。
- 更高的可用性:通过分布式缓存,用户能够在多个服务器之间获取资源,即使某个服务器出现故障,也不会影响服务的可用性。
- 减轻原始服务器负载
- CDN缓存了大量静态内容,减少了原始服务器的直接负担,降低了带宽使用和处理请求的压力。
- 提升网站的可扩展性
- CDN帮助网站应对流量激增,能够在不同地区和时段自动调整资源的分配和流量管理,提供更好的扩展性。
- 增强网站的安全性
- DDoS防护:许多CDN提供DDoS攻击防护,能够通过分布式架构分担攻击流量,从而减轻原始服务器的压力。
- SSL加密:CDN服务提供SSL证书支持,帮助加密数据传输,提升安全性。
- 节省带宽成本
- 通过减少从原始服务器到客户端的流量,CDN有助于降低带宽费用,尤其是对于全球性网站。
- 高可用性和容错性
- CDN通过将资源缓存到多个节点,提升了资源的冗余度。在某个节点出现故障时,流量可以被自动引导到其他正常工作的节点,保证网站的高可用性。
三、CDN的类型
- 静态内容CDN
- 主要缓存静态内容,如图片、JavaScript文件、CSS文件等。通过将这些内容缓存到多个位置,能够加速资源加载速度。
- 动态内容CDN
- 动态内容指的是根据用户请求生成的内容,比如数据库查询结果或用户个性化信息。动态内容通常不缓存,但现代CDN提供商提供了对动态内容的优化方案,通过智能缓存策略加速动态内容的加载。
- 直播和视频流CDN
- 专门用于视频流、直播视频内容的传输,优化了大带宽视频数据的分发和传输。常见的技术包括流媒体协议如 HLS(HTTP Live Streaming)和 DASH(Dynamic Adaptive Streaming over HTTP)。
- 边缘计算CDN
- 这种类型的CDN不仅提供缓存功能,还支持在边缘服务器上执行计算任务。它能够在靠近用户的地方处理请求,提高性能和降低延迟。
四、CDN的工作流程
- 资源上传到CDN:
- 将网站的静态资源上传到CDN供应商的服务器。资源可能会分发到多个全球节点进行缓存。
- 用户请求访问资源:
- 用户访问网页时,浏览器向CDN发起请求。CDN会根据用户的地理位置,智能选择离用户最近的服务器响应请求。
- 缓存命中与未命中:
- 如果边缘服务器已缓存该资源(缓存命中),CDN直接返回缓存的内容。
- 如果缓存过期或没有缓存该资源(缓存未命中),CDN会向原始服务器请求资源,并将返回的资源缓存起来供后续用户使用。
- 返回资源给用户:
- 一旦缓存的资源通过CDN的边缘节点返回给用户,用户的浏览器会在本地缓存该资源,下次访问时,直接从浏览器本地获取。
五、CDN的服务提供商
目前,全球有多个主要的CDN服务提供商,最知名的包括:
- Cloudflare
- 提供免费和收费的CDN服务,支持全球分布的边缘节点,提供DDoS防护和Web应用防火墙(WAF)。
- Akamai
- 全球领先的CDN供应商,服务覆盖范围广,适用于大规模企业和高流量网站,提供强大的内容加速和安全功能。
- Amazon CloudFront
- AWS提供的CDN服务,能够与AWS的其他服务(如S3、EC2等)无缝集成,提供高可扩展性和灵活性。
- Fastly
- 以高性能为特点,支持即时缓存清除和高效的动态内容传输,适用于对延迟要求极高的应用。
- KeyCDN
- 提供较为简单和成本效益高的CDN解决方案,适用于中小型网站。
六、CDN的优化策略
- 合理设置缓存过期时间:
- 根据内容的更新频率,合理设置缓存过期时间(TTL),避免缓存过期导致频繁访问原始服务器。
- 使用分布式缓存:
- 利用CDN的全球节点分布,将内容缓存到多个节点,从而提供更好的负载均衡和冗余。
- 压缩和优化内容:
- 对资源进行压缩(如图片、CSS、JavaScript等),减少传输的数据量,提高加载速度。
- 结合HTTPS加密:
- 使用CDN的SSL证书加密功能,为网站提供HTTPS支持,提升数据传输的安全性。
七、总结
CDN是一种通过将网站内容分发到全球多个节点,减少延迟、提高加载速度、减轻服务器负载的技术。它不仅能加速资源的交付,还能提高网站的安全性、可用性和可扩展性。随着互联网应用的增长,CDN已成为优化网站性能和提供全球用户良好体验的重要工具。
1.4.1 用户请求CDN流程
用户请求CDN资源的流程可以分为几个步骤。这个流程涉及到用户如何向CDN发起请求,CDN如何决定从哪个服务器提供资源,以及缓存如何影响响应时间。以下是详细的用户请求CD能资源的流程:
一、请求流程概述
- 用户发起请求:用户的浏览器或应用程序向服务器请求某个资源(如图片、CSS、JavaScript文件等)。
- DNS解析:请求首先通过DNS解析,将资源的域名解析为CDN的IP地址。
- 路由到CDN边缘节点:用户的请求被路由到距离用户最近的CDN边缘节点。
- 边缘节点缓存检查:CDN的边缘节点检查缓存中是否已有该资源。
- 缓存命中或未命中:根据缓存的情况,决定是直接返回缓存的内容,还是从源服务器获取最新的资源。
- 返回资源给用户:资源通过边缘节点传输给用户,用户的浏览器接收并展示。
二、详细步骤
1. 用户发起请求
用户在浏览器中输入网址或点击链接时,浏览器会发起HTTP请求来请求某个资源。这些资源通常是静态文件,如HTML、CSS、JavaScript文件,或者图片、视频等媒体文件。
例如,用户请求资源:
https://www.example.com/images/logo.png
。2. DNS解析
用户请求的域名(如
www.example.com
)会通过DNS解析,转化为一个IP地址。通常,这个域名已经指向CDN提供商的域名解析系统。
- 传统方式:直接访问原始服务器的IP。
- CDN方式:DNS解析返回的是CDN边缘服务器的IP,而不是源服务器的IP。
CDN提供商通常会在多个地理位置部署多个边缘节点(edge node),当请求发起时,DNS会返回离用户最近的CDN边缘节点的IP地址,确保请求被路由到最近的服务器。
3. 请求被路由到CDN边缘节点
DNS解析完成后,浏览器向CDN的边缘节点发送请求。CDN边缘节点是部署在全球各地的服务器,它们缓存了资源内容,能够快速响应用户请求。
CDN边缘节点的选择通常由以下因素决定:
- 地理位置:用户的IP地址与边缘节点的地理位置之间的距离,尽可能选择距离用户最近的节点。
- 网络负载:当前边缘节点的负载情况。如果某个节点过载,CDN会选择其他负载较低的节点。
4. 边缘节点缓存检查
边缘节点收到请求后,会检查缓存中是否已有该资源。这一步称为缓存命中检查。
- 缓存命中:如果边缘节点缓存中已经存在该资源,并且资源没有过期,则直接从缓存中读取并返回给用户。
- 缓存未命中:如果缓存中没有该资源,或者资源已经过期,则会将请求转发给源服务器(origin server)。
5. 缓存命中或未命中
缓存命中:如果资源已经存在并且有效,CDN会直接将缓存的资源返回给用户。这是加速访问的关键步骤,因为用户不需要访问源服务器,节省了时间和带宽。
例如,若用户请求
https://www.example.com/images/logo.png
,CDN的边缘节点可能已经缓存了这个文件,且TTL(过期时间)没有到期,此时CDN直接返回文件。缓存未命中:如果缓存中没有该资源,或者缓存的资源已经过期,CDN会向源服务器发起请求以获取资源。
6. 从源服务器获取资源
当缓存未命中时,CDN边缘节点会向原始服务器(origin server)请求该资源。此时,源服务器会根据请求返回最新的资源,并且将该资源缓存到边缘节点,以供下次请求使用。
- 资源返回后,CDN会缓存到边缘节点并设置适当的缓存过期时间(TTL)。这意味着下一次请求时,边缘节点可以直接返回缓存的内容,而不需要再访问源服务器。
7. 返回资源给用户
无论是缓存命中还是从源服务器获取资源,最终,CDN的边缘节点会把响应数据返回给用户的浏览器。用户的浏览器从CDN边缘节点接收到资源,并进行展示。
8. 浏览器缓存
在资源返回给浏览器后,浏览器也会根据响应头(如
Cache-Control
、Expires
等)进行本地缓存,以便在下一次访问时直接从本地缓存中获取资源,而不再发送请求到CDN或源服务器。三、缓存策略与内容更新
CDN中的缓存策略非常关键,它决定了缓存内容的过期时间、更新方式以及缓存策略的灵活性。
- TTL(Time to Live,生存时间)
- 每个缓存的资源都会设置一个TTL,TTL指定了该资源在CDN边缘节点缓存的有效期。TTL过期后,缓存的内容会被认为是过期的,需要重新向源服务器请求内容。
- 缓存清除
- 主动清除:CDN提供商允许通过管理控制台或API来主动清除缓存中的某些资源。这对于资源更新频繁或紧急更新的情况非常重要。
- 自动清除:当资源的TTL到期时,CDN会自动清除缓存并向源服务器请求新的内容。
- 缓存验证
- 使用
ETag
和Last-Modified
等HTTP头部字段,CDN可以验证缓存是否有效。即使TTL未到期,CDN也可以通过向源服务器发送条件请求(If-None-Match
或If-Modified-Since
)来判断缓存是否需要更新。四、CDN的优势
- 减少延迟:用户总是能从离自己最近的边缘服务器获取资源,减少了传输延迟。
- 提高可用性:即使源服务器宕机,CDN仍可以从其他节点提供缓存的内容,保持服务可用。
- 减轻源服务器负担:通过缓存大量请求,CDN能够减轻源服务器的负载,减少带宽消耗。
- 提高网站性能:加速资源加载,提升用户体验,尤其是对于全球用户。
五、CDN请求流程示意图
用户请求 --> DNS解析 --> CDN边缘节点 --> 缓存检查 --> | | | 缓存命中 缓存未命中 | | | | 返回缓存的资源 从源服务器请求资源 | | | | 返回给用户的资源 缓存资源并返回给用户 |
六、总结
- CDN工作流程:CDN通过将资源分发到多个边缘节点,利用智能路由、缓存和负载均衡技术,将资源快速交付给用户,减少延迟,提高网站性能。
- 缓存命中与未命中:CDN根据缓存策略决定是否直接返回缓存的内容,或者向源服务器请求更新内容。
- 浏览器与CDN缓存:浏览器本地缓存和CDN的缓存共同工作,确保资源加载更快,减少重复请求。
CDN在提高网站性能、增强网站可用性、降低带宽消耗等方面发挥了重要作用,是现代Web应用不可或缺的组成部分。
1.4.2 CDN分层缓存
CDN(Content Delivery Network,内容分发网络)的分层缓存(Layered Caching)是指通过多级缓存架构有效提升内容分发效率的一种策略。在CDN中,请求的内容通常会经过多个层级的缓存节点,以实现更佳的性能和资源利用率。整个流程通常可以分为以下几个层次:
- L1 边缘节点缓存(Edge Cache):
这是离用户最近的一层缓存节点。当用户向CDN请求内容时,边缘节点首先检查本地缓存是否已存有该内容。若存在并未过期,便直接从该节点返回内容给用户,降低传输延迟,提高用户体验;若缓存中无此内容或内容已过期,则向上层的缓存节点或源站请求。- L2 区域或中间层缓存(Mid-Tier/Regional Cache):
当边缘节点未能在本地拿到所需内容时,会将请求向上层的区域缓存节点发出。区域缓存通常位于更靠近源站的核心网络,储存那些在一定时间窗口内被多个边缘节点重复请求的内容。通过在此层进行缓存,CDN减少了向源站多次重复请求同一内容的频率。这一层有助于将热门内容在更广的地理范围内进行共享,降低源站负载,并减少跨区域的回源请求延迟。- 源站(Origin Server):
当所有中间层缓存与边缘缓存均无请求内容时,才会到达最终的源站。源站是内容的原始出处,CDN会从这里获取最新版本的内容,然后将其分发给请求用户,并在适当的层级缓存节点中储存副本,以便满足未来类似请求。分层缓存的工作原理
以下是一个典型的用户请求过程:
- 用户访问网站,请求某个资源(例如一张图片)。
- 用户的DNS解析请求将用户导向离他最近的L1边缘节点。
- L1节点检查自身是否缓存了该资源。
- 如果有,则直接将资源返回给用户,请求结束。这称为“缓存命中”。
- 如果没有,则L1节点向其上层的L2区域节点发起请求。
- L2节点执行相同的检查,查看自身是否缓存了该资源。
- 如果有,则将资源返回给L1节点,L1节点再将其返回给用户。同时,L1节点也会缓存该资源,以便下次相同的请求可以直接命中。
- 如果没有,则L2节点继续向上,向源站发起请求。
- 源站将资源返回给L2节点,L2节点再返回给L1节点,L1节点最终返回给用户。L1和L2节点都会缓存该资源。
分层缓存的优势
- 减轻源站压力: 通过多层缓存,大部分用户请求都可以在L1或L2节点得到满足,大大减少了回源站的请求数量,从而减轻了源站的负载。
- 提高缓存命中率: 分层结构使得更常用的内容可以缓存在更靠近用户的L1节点上,从而提高整体的缓存命中率,减少用户访问延迟。
- 降低网络拥塞: 由于大量请求在CDN内部完成,减少了跨区域和跨运营商的网络传输,有助于缓解网络拥塞。
- 更好的可扩展性: 分层结构使得CDN系统更容易扩展,可以通过增加L1和L2节点来应对不断增长的用户访问量。
分片缓存(Chunked Caching)
在某些情况下,CDN还会使用分片缓存技术,将大文件(例如视频文件)分割成多个小片段(chunks),然后分别缓存这些片段。当用户请求文件时,CDN只需传输用户需要的片段,而不是整个文件。这对于提高大文件传输效率和支持流媒体播放非常有用。
总结
CDN分层缓存是一种有效的提高网站性能和用户体验的技术。通过合理地组织和管理多层缓存节点,CDN可以更好地分配资源,提高缓存命中率,并减轻源站的压力。
2.Redis 安装及连接
Redis简介:
Redis(Remote Dictionary Server)是一个开源的内存数据结构存储系统,可以用作数据库、缓存和消息队列中间件。它以Key-Value形式存储数据,提供多种数据结构和丰富的功能特性。Redis的核心价值在于高速访问、简单的数据操作模型以及丰富的数据结构支持,使其在需要快速读写、实时计算和高并发的场景中表现突出。Redis的主要特性:
内存存储:
Redis将数据存储在内存中,从而达到非常高的访问速度(读写操作通常在微秒级别)。这使其在对实时性要求高的场景(如会话存储、实时排行、实时计数器等)表现优异。多种数据结构支持:
相较于传统的Key-Value存储仅支持字符串,Redis支持多种丰富的数据结构类型,这些数据结构以简单命令即可操作:
- String(字符串):最基础的数据结构,可存储普通字符串、数字、二进制数据。
- Hash(哈希):类似于Key-Value映射的集合,可方便存储对象属性并对属性进行增删改操作。
- List(列表):双端链表实现,支持从头尾插入、弹出元素,可用来实现消息队列、任务列表等功能。
- Set(集合):无序集合结构,支持求交集、并集和差集等集合运算,常用于去重、标签管理。
- Sorted Set(有序集合):每个元素会关联一个分数(score),Redis会根据分数对元素进行排序,可用于排行榜、延时队列等场景。
- Bitmap(位图)、HyperLogLog、Geo(地理位置)等特殊数据类型:满足统计计数、地理位置查询等特殊需求。
持久化能力:
虽然Redis是内存数据库,但它并非易失性存储。Redis提供两种持久化机制,让数据在断电后仍能恢复:
- RDB(Redis Database Backup):定时生成内存快照并持久化到磁盘,恢复速度快,数据略有延迟。
- AOF(Append Only File):将每次写操作以日志的形式追加到文件中,数据恢复更为完整,可根据策略对AOF文件进行定期重写压缩。
可以根据业务需求选择合适的持久化方案,或同时开启RDB和AOF实现数据安全与高效率的折中。
高可用与分布式:
Redis提供主从复制(Master-Slave Replication)实现数据的多份冗余,主节点负责写操作,从节点同步主节点的数据,提供读取分流和故障切换。当主节点出现故障时,可手动或借助Redis Sentinel(哨兵)实现自动故障转移。
对于更大规模的数据集与访问压力,Redis Cluster可以将数据分片至多个节点,提升整体存储能力和吞吐性能。事务支持:
Redis提供简单的事务机制(MULTI/EXEC命令),可以将一组操作打包,保证这些操作的顺序性和原子性。虽然不支持复杂的回滚功能,但事务可以确保一组命令要么都执行要么都不执行。Lua脚本扩展:
Redis内置了Lua解释器,用户可以在Redis内原子执行Lua脚本,对数据进行复杂操作,而无需在客户端与Redis之间多次往返,提高复杂操作的性能和一致性。丰富的使用场景:
凭借高性能和多数据结构支持,Redis可广泛应用于各种场景:
- 缓存热点数据(例如:热门商品信息、用户会话数据、应用程序配置)
- 消息队列与任务调度(利用List或Stream)
- 实时统计(计数器、排行榜、实时分析)
- 分布式锁(利用SetNx命令实现简单的分布式锁机制)
简单易用的命令行与客户端支持:
Redis提供简洁直观的命令行客户端和与主流编程语言(如Java、Python、Go、C#等)兼容的客户端库,降低学习成本与集成难度。
总结:
Redis作为一个内存数据存储系统,具有高性能、丰富的数据类型、灵活的持久化策略以及高可用性架构支持。它在高并发、低延迟与实时处理场景中得到广泛应用,已成为构建现代互联网应用的重要基础组件。
2.1 dnf 安装 Redis
# Rocky 9.4 由系统源提供
[root@redis1.xyy.org ~]#dnf info redis
Name : redis
Version : 6.2.7
Release : 1.el9
Architecture : x86_64
Size : 1.3 M
Source : redis-6.2.7-1.el9.src.rpm
Repository : appstream
Summary : A persistent key-value database
URL : https://redis.io
License : BSD and MIT
Description : Redis is an advanced key-value store. It is often referred to as a data
: structure server since keys can contain strings, hashes, lists, sets and
: sorted sets.
:
: You can run atomic operations on these types, like appending to a string;
: incrementing the value in a hash; pushing to a list; computing set
: intersection, union and difference; or getting the member with highest
: ranking in a sorted set.
:
: In order to achieve its outstanding performance, Redis works with an
: in-memory dataset. Depending on your use case, you can persist it either
: by dumping the dataset to disk every once in a while, or by appending
: each command to a log.
:
: Redis also supports trivial-to-setup master-slave replication, with very
: fast non-blocking first synchronization, auto-reconnection on net split
: and so forth.
:
: Other features include Transactions, Pub/Sub, Lua scripting, Keys with a
: limited time-to-live, and configuration settings to make Redis behave like
: a cache.
:
: You can use Redis from most programming languages also.
[root@Rocky9.4 ~]#
# CentOS 7由 epel 源提供
[root@CentOS7 ~]#yum info redis
Name : redis
Arch : x86_64
Version : 3.2.12
Release : 2.el7
Size : 1.4 M
Repo : installed
From repo : epel
Summary : A persistent key-value database
URL : http://redis.io
License : BSD
[root@redis1.xyy.org ~]#dnf install redis
[root@redis1.xyy.org ~]#systemctl enable --now redis
[root@redis1.xyy.org ~]#pstree -p | grep redis
|-redis-server(4237)-+-{redis-server}(4238)
| |-{redis-server}(4239)
| |-{redis-server}(4240)
| `-{redis-server}(4241)
[root@redis1.xyy.org ~]#
[root@redis1.xyy.org ~]#redis-cl
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> INFO Server
# Server
redis_version:6.2.7
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:ec192bdd77ecd321
redis_mode:standalone
os:Linux 5.14.0-427.13.1.el9_4.x86_64 x86_64
arch_bits:64
monotonic_clock:POSIX clock_gettime
multiplexing_api:epoll
atomicvar_api:c11-builtin
gcc_version:11.3.1
process_id:4237
process_supervised:systemd
run_id:37144e0c3a2930dac6148605d26afae8ee4d38ba
tcp_port:6379
server_time_usec:1734486571682241
uptime_in_seconds:37314
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:6433323
executable:/usr/bin/redis-server
config_file:/etc/redis/redis.conf
io_threads_active:0
127.0.0.1:6379>
2.2 编译安装 Redis
从 Redis 官方下载地址 获取稳定版压缩包,如 redis-7.4.0.tar.gz
。
# 1.创建 Redis 用户(不需要登录权限,只是用于运行 Redis 服务以提高安全性)
useradd -r -s /sbin/nologin redis
# 2.获取源码包
wget https://download.redis.io/releases/redis-7.4.0.tar.gz
# 3.解压并进入源码目录
tar xf redis-7.4.0.tar.gz
cd redis-7.4.0
# 4.开始编译(在某些发行版下可开启 USE_SYSTEMD=yes 选项,以生成可与 systemd 交互的可执行文件。)
make -j $(nproc) USE_SYSTEMD=yes
# 5.安装到指定位置
make PREFIX=/apps/redis install
# 6.建立软链接(方便在命令行中使用redis-server、redis-cli)
ln -s /apps/redis/bin/redis-* /usr/bin/
# 7.创建所需目录
mkdir -p /apps/redis/{etc,log,data,run}
# 8.拷贝源码目录中自带redis.conf,拷贝到配置目录:
cp redis.conf /apps/redis/etc/
# 9.redis.conf:修改关键配置
#bind:改为 0.0.0.0 或保留默认看实际需要;
#requirepass:设置 Redis 密码,如 requirepass 123456;
#dir:RDB/快照文件存放目录,一般设为 /apps/redis/data;
#logfile:日志文件路径,如 /apps/redis/log/redis-6379.log;
#pidfile:pid 文件路径,如 /apps/redis/run/redis-6379.pid;
sed -i -e 's/bind 127.0.0.1/bind 0.0.0.0/' \
-e "/# requirepass/a requirepass 123456" \
-e "/^dir .*/c dir /apps/redis/data/" \
-e "/^logfile .*/c logfile /apps/redis/log/redis-6379.log" \
-e "/^pidfile .*/c pidfile /apps/redis/run/redis-6379.pid" \
/apps/redis/etc/redis.conf
# 10.设置文件权限
chown -R redis:redis /apps/redis
# 11.内核与系统参数优化(不优化会有告警)
# 11.1 调整内核参数
vim /etc/sysctl.conf
net.core.somaxconn = 1024
vm.overcommit_memory = 1
sysctl -p
# 11.2 禁用透明大页(THP)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 可以写入启动脚本(如 /etc/rc.local 或 /etc/rc.d/rc.local)以在重启后继续生效。
# 12.创建Systemd服务并启动
# CentOS/Rocky:/usr/lib/systemd/system/redis.service
# Ubuntu:/lib/systemd/system/redis.service(或 /etc/systemd/system/redis.service)
[Unit]
Description=Redis persistent key-value database
After=network.target
[Service]
ExecStart=/apps/redis/bin/redis-server /apps/redis/etc/redis.conf --supervised systemd
ExecStop=/bin/kill -s QUIT $MAINPID
# 启动notify一定要编译了 USE_SYSTEMD=yes,否则启动服务有问题
Type=notify
User=redis
Group=redis
RuntimeDirectory=redis
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target
# 13.刷新并启动服务
systemctl daemon-reload
systemctl enable --now redis
systemctl status redis
# 14.查看Redis版本或者信息
redis-server -v
# 查看服务信息
redis-cli -a 123456 INFO Server
# 测试插入和查询数据
redis-cli -a 123456 set mykey "Hello World"
redis-cli -a 123456 get mykey
#! /bin/bash
#-----------------------------------------------------
#Author: XingYuyu
#Date: 2024-08-12
#Blog: http://8.141.4.74
#Filename: install_redis.sh
#Description: [Online Install Redis for Rocky Linux ,Ubuntu,CentOS ]
#-----------------------------------------------------
VERSION=redis-7.4.0
PASSWORD=123456
INSTALL_DIR=/apps/redis
os_type() {
awk -F'[ "]' '/^NAME/{print $2}' /etc/os-release
}
color() {
RES_COL=80
MOVE_TO_COL="echo -en \e[${RES_COL}G"
SETCOLOR_SUCCESS="echo -en \e[1;32m"
SETCOLOR_FAILURE="echo -en \e[1;31m"
SETCOLOR_WARNING="echo -en \e[1;33m"
SETCOLOR_NORMAL="echo -en \e[0m"
echo -n "$1" && $MOVE_TO_COL
echo -n "["
if [ $2 = "success" -o $2 = "0" ]; then
${SETCOLOR_SUCCESS}
echo -n $" OK "
elif [ $2 = "failure" -o $2 = "1" ]; then
${SETCOLOR_FAILURE}
echo -n $"FAILED"
else
${SETCOLOR_WARNING}
echo -n $"WARNING"
fi
${SETCOLOR_NORMAL}
echo -n $"]"
echo
}
install_redis() {
wget https://download.redis.io/releases/${VERSION}.tar.gz || {
color "Redis 源码下载失败" 1
exit
}
tar xf ${VERSION}.tar.gz
cd ${VERSION}
CPUS=lscpu | awk '/^CPU\(s\)/{print $2}'
make -j $CPUS USE_SYSTEMD=yes PREFIX=${INSTALL_DIR} install && color "Redis 编译安装完成" 0 || {
color "Redis 编译安装失败" 1
exit
}
ln -s ${INSTALL_DIR}/bin/redis-* /usr/bin/
mkdir -p ${INSTALL_DIR}/{etc,log,data,run}
cp redis.conf ${INSTALL_DIR}/etc/
sed -i -e 's/bind 127.0.0.1/bind 0.0.0.0/' -e "/# requirepass/a requirepass $PASSWORD" -e "/^dir .*/c dir ${INSTALL_DIR}/data/" -e "/^logfile .*/c logfile ${INSTALL_DIR}/log/redis-6379.log" -e "/^pidfile .*/c pidfile ${INSTALL_DIR}/run/redis-6379.pid" ${INSTALL_DIR}/etc/redis.conf
if id redis &>/dev/null; then
color "Redis 用户已经存在,无需创建" 0
else
useradd -r -s /sbin/nologin redis
color "Redis 用户创建成功" 0
fi
chown -R redis.redis ${INSTALL_DIR}
cat >>/etc/sysctl.conf <<EOF
net.core.somaxconn = 1024
vm.overcommit_memory = 1
EOF
sysctl -p
if [ `os_type` == "Ubuntu" ];then
cat >> /lib/systemd/system/rc-local.service <<EOF
[Install]
WantedBy=multi-user.target
EOF
echo '#!/bin/bash' > /etc/rc.local
echo 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' >>/etc/rc.local
chmod +x /etc/rc.local
/etc/rc.local
# Ubuntu 的service文件放在/lib/systemd/system/下或者/etc/systemd/system/下不能放在/usr/lib/下
cat > /lib/systemd/system/redis.service <<EOF
[Unit]
Description=Redis persistent key-value database
After=network.target
[Service]
ExecStart=${INSTALL_DIR}/bin/redis-server ${INSTALL_DIR}/etc/redis.conf --supervised systemd
ExecStop=/bin/kill -s QUIT \$MAINPID
Type=notify
User=redis
Group=redis
RuntimeDirectory=redis
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target
EOF
else
echo 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' >>/etc/rc.d/rc.local
chmod +x /etc/rc.d/rc.local
/etc/rc.d/rc.local
cat > /usr/lib/systemd/system/redis.service <<EOF
[Unit]
Description=Redis persistent key-value database
After=network.target
[Service]
ExecStart=${INSTALL_DIR}/bin/redis-server ${INSTALL_DIR}/etc/redis.conf --supervised systemd
ExecStop=/bin/kill -s QUIT \$MAINPID
Type=notify
User=redis
Group=redis
RuntimeDirectory=redis
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target
EOF
fi
systemctl daemon-reload
systemctl enable --now redis &>/dev/null
systemctl is-active redis &> /dev/null && color "Redis 服务启动成功,Redis信息如下:" 3 || { color "Redis 启动失败" 1 ;exit; }
#sleep 5
redis-cli -a $PASSWORD INFO Server 2>/dev/null
}
install_CentOS7() {
. /etc/init.d/functions
# jemalloc-devel依赖于epel源
yum -y install epel-release && yum -y install gcc jemalloc-devel systemd-devel || {
color "安装软件包失败,请检查网络配置" 1
exit
}
rpm -q wget &>/dev/null || yum -y install wget &>/dev/null
wget https://download.redis.io/releases/${VERSION}.tar.gz || {
action "Redis 源码下载失败" false
exit
}
tar xf ${VERSION}.tar.gz
cd ${VERSION}
CPUS=lscpu | awk '/^CPU\(s\)/{print $2}'
make -j $CPUS USE_SYSTEMD=yes PREFIX=${INSTALL_DIR} install && action "Redis 编译安装完成" || {
action "Redis 编译安装失败" false
exit
}
ln -s ${INSTALL_DIR}/bin/redis-* /usr/bin/
mkdir -p ${INSTALL_DIR}/{etc,log,data,run}
cp redis.conf ${INSTALL_DIR}/etc/
sed -i -e 's/bind 127.0.0.1/bind 0.0.0.0/' -e "/# requirepass/a requirepass $PASSWORD" -e "/^dir .*/c dir ${INSTALL_DIR}/data/" -e "/^logfile .*/c logfile ${INSTALL_DIR}/log/redis-6379.log" -e "/^pidfile .*/c pidfile ${INSTALL_DIR}/run/redis-6379.pid" ${INSTALL_DIR}/etc/redis.conf
if id redis &>/dev/null; then
action "Redis 用户已经存在" false
else
useradd -r -s /sbin/nologin redis
fi
chown -R redis.redis ${INSTALL_DIR}
cat >>/etc/sysctl.conf <<EOF
net.core.somaxconn = 1024
vm.overcommit_memory = 1
EOF
sysctl -p
echo 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' >>/etc/rc.d/rc.local
chmod +x /etc/rc.d/rc.local
/etc/rc.d/rc.local
cat >/usr/lib/systemd/system/redis.service <<EOF
[Unit]
Description=Redis persistent key-value database
After=network.target
[Service]
ExecStart=${INSTALL_DIR}/bin/redis-server ${INSTALL_DIR}/etc/redis.conf --supervised systemd
ExecReload=/bin/kill -s HUP \$MAINPID
ExecStop=/bin/kill -s QUIT \$MAINPID
Type=notify
User=redis
Group=redis
RuntimeDirectory=redis
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now redis &>/dev/null
systemctl is-active redis &> /dev/null && ${COLOR}"Redis 服务启动成功,Redis信息如下:"${END} || { ${COLOR}"Redis 启动失败"${END};exit; }
#sleep 5
redis-cli -a $PASSWORD INFO Server 2>/dev/null
}
install_Ubuntu() {
apt -y install make gcc libjemalloc-dev libsystemd-dev || {
color "安装软件包失败,请检查网络配置" 1
exit
}
install_redis
}
install_Rocky() {
# jemalloc-devel依赖于epel源
yum -y install epel-release && yum -y install gcc jemalloc-devel systemd-devel || {
color "安装软件包失败,请检查网络配置" 1
exit
}
rpm -q wget &>/dev/null || yum -y install wget &>/dev/null
install_redis
}
if [ $(os_type) == 'CentOS' ]; then
install_CentOS7
elif [ $(os_type) == 'Rocky' ]; then
install_Rocky
elif [ $(os_type) == 'Ubuntu' ]; then
install_Ubuntu
else
color "未识别的操作系统" 1
fi
2.3 连接到 Redis
2.3.1 客户端连接到 Redis
1.本机无密码连接
redis-cli
2.跨主机无密码连接
redis-cli -h HOSTNAME/IP -p PORT
3.跨主机密码连接
redis-cli -h HOSTNAME/IP -p PORT -a PASSWORD
2.3.2 程序连接 Redis
redis 支持多种开发语言访问
https://redis.io/docs/latest/develop/clients/
shell 脚本写入数据到 Redis
#!/bin/bash
NUM=100
PASS=123456
for i in `seq $NUM`; do
redis-cli -h 127.0.0.1 -a "$PASS" --no-auth-warning set key${i} value${i}
echo "key${i} value${i} 写入完成"
done
echo "$NUM 个key写入到Redis完成"
3.Redis 的多实例
在生产环境中,为了更好地利用资源、实现多租户隔离或分离不同业务的数据与配置,运维人员往往会在一台服务器上运行多个 Redis 实例。Redis 的多实例部署并非Redis内建的特性,而是通过为每个实例指定独立的配置文件、独立的运行端口与数据目录来实现的。以下是关于Redis多实例的详细讲解:
为什么需要多实例
- 资源隔离与多租户支持:
在某些场景下,不同的业务线或不同的用户需要独立的Redis服务,以免数据和性能相互影响。多实例可以为每个业务运行独立的Redis,保证数据和访问流量的隔离。- 不同的配置要求:
某些业务可能需要不同的持久化策略(RDB或AOF)、内存管理策略或安全设置。多实例部署允许针对每个实例使用单独的配置文件,从而灵活定制每个实例的行为。- 更好地利用硬件资源:
一台物理机/虚拟机的CPU、内存、网络资源较为充裕时,可以在同一台机器上运行多个Redis实例,充分利用硬件资源。尤其在内存较大时,不同实例分别作为缓存、队列、会话存储使用,可以最大化硬件利用率。配置多实例的关键点
独立的配置文件:
每个实例都需要一个独立的配置文件(例如redis-6379.conf
,redis-6380.conf
)。
在配置文件中需要注意如下参数:
port
:每个实例必须使用不同的端口,如6379、6380、6381等。pidfile
:每个实例需要独立的PID文件,如/var/run/redis_6379.pid
、/var/run/redis_6380.pid
。logfile
:为每个实例指定独立的日志文件,如/var/log/redis_6379.log
、/var/log/redis_6380.log
。dir
:为每个实例指定独立的数据目录,如/var/lib/redis/6379/
、/var/lib/redis/6380/
,确保RDB或AOF文件不冲突。daemonize yes
:通常在生产中,多实例都以守护进程方式后台运行。利用 systemd 的进程监督能力,即使用--supervised systemd
参数时,必须将daemonize
设为no
。如果将daemonize
设为yes
,则与 systemd 的监督模式相矛盾,导致 Redis 无法正常通过 systemd 进行管理和监控。独立的启动命令:
启动时为每个实例指定相应的配置文件。常用命令形式:redis-server /path/to/redis-6379.conf redis-server /path/to/redis-6380.conf
确保每个实例正常监听自己的端口并使用自己的配置。
服务管理与守护进程:
为每个实例创建单独的systemd服务文件或init脚本,方便运维管理。如在systemd中创建/etc/systemd/system/redis@6379.service
、redis@6380.service
等文件,然后通过systemctl start redis@6379
启动指定实例。安全与访问控制:
确保为每个实例设置合理的访问控制,如bind
参数、protected-mode
设置、requirepass
或ACL策略。多实例运行时应确保不同实例的数据和访问策略独立,避免安全隐患。监控与报警:
多实例运行时需要对每个实例分别进行监控,收集其内存使用、连接数、QPS、延迟、慢查询等指标,并对异常情况及时报警。举例:多实例文件组织形式
/etc/redis/ ├─ redis-6379.conf ├─ redis-6380.conf └─ redis-6381.conf /var/lib/redis/ ├─ 6379/ │ ├─ dump.rdb │ └─ appendonly.aof ├─ 6380/ │ ├─ dump.rdb │ └─ appendonly.aof └─ 6381/ ├─ dump.rdb └─ appendonly.aof /var/log/ ├─ redis_6379.log ├─ redis_6380.log └─ redis_6381.log
总结
Redis多实例部署是通过为每个实例提供独立的端口、独立的配置文件以及数据和日志目录来实现的。这种方式在同一台服务器上实现了灵活的资源分配和多租户支持。通过精心配置和管理,运维人员能够同时运行多个Redis实例,为不同应用提供高效、独立而又经济实惠的内存数据存储服务。
案例:以编译安装为例实现 Redis 多实例
# 生成的文件列表
[root@Rocky9.4 ~]#ll /apps/redis/
total 0
drwxr-xr-x 2 redis redis 134 Dec 17 23:22 bin
drwxr-xr-x 2 redis redis 22 Dec 18 20:04 data
drwxr-xr-x 2 redis redis 24 Dec 18 20:04 etc
drwxr-xr-x 2 redis redis 28 Dec 17 23:22 log
drwxr-xr-x 2 redis redis 28 Dec 18 20:04 run
[root@Rocky9.4 redis]#tree /apps/redis/
/apps/redis/
├── bin
│ ├── redis-benchmark
│ ├── redis-check-aof -> redis-server
│ ├── redis-check-rdb -> redis-server
│ ├── redis-cli
│ ├── redis-sentinel -> redis-server
│ └── redis-server
├── data
│ ├── dump-6379.rdb
│ ├── dump-6380.rdb
│ └── dump-6381.rdb
├── etc
│ ├── redis_6379.conf
│ ├── redis_6380.conf
│ ├── redis_6381.conf
│ └── redis.conf
├── log
│ ├── redis-6379.log
│ ├── redis-6380.log
│ └── redis-6381.log
└── run
├── redis-6379.pid
├── redis-6380.pid
└── redis-6381.pid
5 directories, 19 files
# 配置文件需要修改的地方
vim /apps/redis/etc/redis_6379.conf
bind 0.0.0.0 -::1
port 6379
daemonize no
pidfile /apps/redis/run/redis-6379.pid
logfile /apps/redis/log/redis-6379.log
# 写入数据的时候,并且满足save才会生产dump-6379.rdb这个文件
dbfilename dump-6379.rdb
dir /apps/redis/data/
# 3600秒,写一次数据 300秒,100次数据,60秒,10000次数据 满足就会备份,为了更快的看到效果可以更改,例如:save 60 1
save 3600 1 300 100 60 10000
appendfilename "appendonly-6379.aof"
vim /apps/redis/etc/redis_6380.conf
bind 0.0.0.0 -::1
port 6380
daemonize no
pidfile /apps/redis/run/redis-6380.pid
logfile /apps/redis/log/redis-6380.log
dbfilename dump-6380.rdb
dir /apps/redis/data/
save 3600 1 300 100 60 10000
appendfilename "appendonly-6380.aof"
vim /apps/redis/etc/redis_6381.conf
bind 0.0.0.0 -::1
port 6381
daemonize no
pidfile /apps/redis/run/redis-6381.pid
logfile /apps/redis/log/redis-6381.log
dbfilename dump-6381.rdb
dir /apps/redis/data/
save 3600 1 300 100 60 10000
appendfilename "appendonly-6381.aof"
# 创建service文件
# 1./usr/lib/systemd/system/redis6379.service
[Unit]
Description=Redis persistent key-value database
After=network.target
[Service]
ExecStart=/apps/redis/bin/redis-server /apps/redis/etc/redis_6379.conf --supervised systemd
ExecStop=/bin/kill -s QUIT $MAINPID
Type=notify
User=redis
Group=redis
RuntimeDirectory=redis
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target
[root@Rocky9.4 ~]#
# 2./usr/lib/systemd/system/redis6380.service
[Unit]
Description=Redis persistent key-value database
After=network.target
[Service]
ExecStart=/apps/redis/bin/redis-server /apps/redis/etc/redis_6380.conf --supervised systemd
ExecStop=/bin/kill -s QUIT $MAINPID
#Type=notify
User=redis
Group=redis
RuntimeDirectory=redis
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target
[root@Rocky9.4 ~]#
# 3./usr/lib/systemd/system/redis6381.service
[Unit]
Description=Redis persistent key-value database
After=network.target
[Service]
ExecStart=/apps/redis/bin/redis-server /apps/redis/etc/redis_6381.conf --supervised systemd
ExecStop=/bin/kill -s QUIT $MAINPID
#Type=notify
User=redis
Group=redis
RuntimeDirectory=redis
RuntimeDirectoryMode=0755
[Install]
WantedBy=multi-user.target
[root@Rocky9.4 ~]#
systemctl daemon-reload
systemctl enable --now redis6379.service redis6380.service redis6381.service
# 这里有个问题,通过二进制安装好的Redis,start的时候用tab键无法补全
4.Redis 持久化
Redis 是一个基于内存的数据结构存储系统,但它提供了多种持久化机制,可以将内存中的数据保存到磁盘中,从而在 Redis 重启或服务器宕机后依然能够恢复数据。Redis 主要提供了两种持久化方式:RDB(Redis Database) 和 AOF(Append Only File)。这两种方式可以单独使用,也可以配合使用,具体选择取决于业务需求(对数据一致性、写入性能、磁盘空间等的不同要求)。
4.1 RDB(Redis Database)
RDB 方式是 Redis 最早的持久化模式,即在某个时间点对内存数据做快照,并保存到一个 .rdb
文件中
4.1.1 RDB 的工作机制
方法1:
SAVE 命令是“阻塞式”保存,Redis 不会创建子进程,而是直接由主进程把内存数据写到 RDB 文件里。
[root@Rocky9.4 redis]#( redis-cli -a 123456 save & );pstree -p | grep redis-server;ls /apps/redis/data/ -lh
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
|-redis-server(28847)-+-{redis-server}(28854)
| |-{redis-server}(28855)
| |-{redis-server}(28856)
| |-{redis-server}(28857)
| `-{redis-server}(28858)
total 180M
-rw-r--r-- 1 redis redis 180M Dec 23 18:48 dump_6379.rdb
-rw-r--r-- 1 redis redis 48K Dec 23 21:45 temp-28847.rdb
使用 python 脚本存入一千万条数据,再进行备份看到下面的现象
# 这个需要使用pip install redis来安装redis包
import redis
pool=redis.ConnectionPool(host="10.0.0.41",port=6379,password="123456")
r=redis.Redis(connection_pool=pool)
for i in range(10000000):
r.set("k%d" % i,"v%d" % i)
data=r.get("k%d" % i)
print(data)
方法2:
BGSAVE 才是“后台”保存,Redis 会 fork 一个子进程来完成 RDB 持久化,主进程继续对外提供服务。
[root@Rocky9.4 data]#redis-cli -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> bgsave
Background saving started
127.0.0.1:6379>
# 生产临时文件,fork 子进程 pid是920,从temp-920.rdb也可以看出是进程920在备份
[root@Rocky9.4 data]#pstree -p | grep redis-server;ls /apps/redis/data/ -lh
|-redis-server(638)-+-redis-server(920)
| |-{redis-server}(666)
| |-{redis-server}(667)
| |-{redis-server}(668)
| |-{redis-server}(669)
| `-{redis-server}(671)
total 128M
-rw-r--r-- 1 redis redis 67M Dec 24 14:43 temp-920.rdb
# 备份结束以后,将文件重命名
[root@Rocky9.4 data]#pstree -p | grep redis-server;ls /apps/redis/data/ -lh
|-redis-server(638)-+-{redis-server}(666)
| |-{redis-server}(667)
| |-{redis-server}(668)
| |-{redis-server}(669)
| `-{redis-server}(671)
total 180M
-rw-r--r-- 1 redis redis 180M Dec 24 14:43 dump_6379.rdb
# 也可以查看日志
[root@Rocky9.4 data]#tail -f ../log/redis-6379.log
# bgsave的日志,会显示出具体的子进程编号
638:M 24 Dec 2024 15:15:14.746 * Background saving started by pid 1037
1037:C 24 Dec 2024 15:15:22.016 * DB saved on disk
1037:C 24 Dec 2024 15:15:22.026 * Fork CoW for RDB: current 0 MB, peak 0 MB, average 0 MB
638:M 24 Dec 2024 15:15:22.095 * Background saving terminated with success
# save的日志
638:M 24 Dec 2024 15:20:09.364 * DB saved on disk
方法3:
Redis 会在配置文件中设置触发 RDB 生成快照的条件,典型配置示例:
save 900 1 # 在900秒内(15分钟)至少有1个键发生变动,则触发保存快照 save 300 10 # 在300秒内(5分钟)至少有10个键发生变动,则触发保存快照 save 60 10000# 在60秒内(1分钟)至少有10000个键发生变动,则触发保存快照 # 上面是之前老版本的写法,写在新版本的写法: save 3600 1 300 100 60 10000
触发快照后,Redis 通过fork 出一个子进程,子进程负责将内存数据写入临时的 RDB 文件;父进程继续处理客户端请求,因此在快照生成的过程中,Redis 仍然能够服务读写请求。
当子进程将数据写完后,会原子性地用临时文件替换原先的 RDB 文件,以确保在替换前的 RDB 文件依然可用。这个和bgsave的模式是一样的.
4.1.2 RDB 的优点
- 性能开销小:由于生成快照时是通过 fork 子进程来执行,主进程只需做少量工作,对性能影响较小。
- 适合做冷备:如果业务允许一定程度的数据丢失(因为 RDB 只能反映生成快照时的数据状态),那么 RDB 非常简洁且容易做冷备份与全量备份。
- 启动速度快:从 RDB 文件进行数据恢复时,因为只是加载一个快照文件,启动速度通常比较快。
4.1.3 RDB 的缺点
- 可能丢失数据:快照通常并不会很频繁地生成(除非你把
save
指令配置得极短,这会带来极大的性能损耗),所以在两次快照之间发生的数据写操作可能会丢失。 - fork 开销:在大数据量时执行 fork 操作需要分配子进程的内存页表,会有一定系统开销,且写入
.rdb
文件时也会消耗 I/O 资源。
4.2 AOF(Append Only File)
从 Redis 7.0.0开始以及之后的版本中,AOF(Append Only File)机制经过优化,引入了基础 RDB 文件和增量 AOF 文件的组合方式。这种设计有助于提高 AOF 的管理效率和数据恢复的速度。().
AOF 是另一种持久化方式,它会将每次写操作以命令的形式追加到一个文件中(默认叫 appendonly.aof
),从而实现数据的保存。
4.2.1 AOF 的工作机制
- 写命令追加:Redis 会把收到的每条写命令,用 Redis 协议格式(Redis Serialization Protocol)记录到 AOF 文件的末尾。
- AOF 刷盘策略:Redis 提供了多种 AOF 同步策略(即何时将命令真正写到磁盘),通过
appendfsync
参数控制:appendfsync always
:每次有写操作时都同步写入磁盘,最安全但最慢。appendfsync everysec
:每秒将缓存中的写命令同步写到磁盘,默认配置,在系统断电时最多丢失1秒的数据。appendfsync no
:由操作系统决定何时同步写到磁盘,性能最高,安全性最低。
- AOF 重写(Rewrite):随着大量写操作的发生,AOF 文件会越来越大,因此需要对 AOF 文件进行“重写压缩”。
- Redis 会 fork 出子进程,把内存中的数据以最精简的命令集合重新写到一个新文件中。
- 重写过程中,主进程持续将新的写操作命令追加到一个缓冲区,待子进程重写完成后,再将这些命令同步到新文件末尾,最后原子地替换旧 AOF 文件。
4.2.2 AOF 的优点
- 数据安全:AOF 可以配置成每次写操作都写入磁盘(
always
),或者至少每秒写一次(everysec
),相比 RDB,数据丢失的风险会小得多。 - 日志记录:AOF 文件是按命令记录的文本文件,人为可读,并且在出现紧急情况时可以对其进行分析或修复(比如手动删除错误指令)。
4.2.3 AOF 的缺点
- 文件体积大:和 RDB 相比,AOF 文件会更大,尤其是在没有做 AOF 重写的情况下。
- 写性能影响:如果采用最安全的
appendfsync always
模式,那么每次写操作都要同步到磁盘,会带来明显的性能损耗。 - 恢复速度:AOF 重放所有写命令来恢复数据,可能比载入一个完整的 RDB 文件更慢。
4.3 如何选择 RDB 和 AOF
- 只用 RDB:
- 对数据一致性要求不高,能容忍几分钟的数据丢失,且更倾向于更好的写性能。
- 能够定期手动备份 RDB 文件,或者通过复制等方式冗余数据。
- 只用 AOF:
- 对数据安全性要求更高,不能容忍太多数据丢失,希望可以在秒级甚至实时上落盘。
- 愿意投入更多的磁盘性能和空间成本,接受 AOF 重放带来的恢复速度影响。
- RDB + AOF 同时使用(较推荐)
- 大多数生产环境下,往往两者结合使用,Redis 启动时优先载入 AOF 文件(更完整),如果 AOF 文件不存在或不可用才载入 RDB 文件。
- 可以在保证数据安全的同时,也能定期生成快照,便于快速恢复或冷备份。
4.4 AOF相关配置
[root@Rocky9.4 etc]#vim /apps/redis/etc/redis_6379.conf
# 启用 AOF 持久化,通过config命令开启,防止数据清空 config set appendonly yes
appendonly yes
# AOF 文件的名称
appendfilename "appendonly-6379.aof"
# 新版本专门为aof增加了一个目录,这个目录是在$dir下创建的
appenddirname "appendonlydir"
# AOF 同步策略
# always: 每个写命令都同步到磁盘
# everysec: 每秒同步一次
# no: 让操作系统决定何时同步
appendfsync everysec
# 数据目录
dir /apps/redis/data/
# AOF 重写的策略
# 例如,当 AOF 文件大小增长到上一个重写后的大小的 100% 时触发重写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 查看备份的目录
[root@Rocky9.4 data]#tree
.
├── appendonlydir
│ ├── appendonly-6379.aof.1.base.rdb
│ ├── appendonly-6379.aof.1.incr.aof
│ └── appendonly-6379.aof.manifest
├── dump-6379.rdb
├── dump-6380.rdb
└── dump-6381.rdb
1 directory, 6 files
[root@Rocky9.4 data]#pwd
/apps/redis/data
[root@Rocky9.4 data]#
根据业务需求选择合适的同步策略:
- always:适用于对数据安全性要求极高的场景,但性能开销较大。
- everysec:默认策略,适用于大多数场景,平衡了性能和数据安全性。
- no:适用于对性能要求极高且可以容忍数据丢失的场景。
在 Redis 7.4 中,AOF(Append Only File)持久化机制引入了更为复杂和高效的文件结构,以提高数据持久性和恢复速度。您在
appendonlydir
目录下看到的以下几个文件:基础 RDB 文件: appendonly-6379.aof.1.base.rdb 180M 增量 AOF 文件: appendonly-6379.aof.1.incr.aof 56 清单文件: appendonly-6379.aof.manifest 98
这些文件分别代表了 Redis 7.4 中 AOF 持久化机制的新特性和结构。下面将详细解释每个文件的含义及其作用。
1.
appendonly-6379.aof.1.base.rdb
(180M)作用:
- 这是一个 基础 RDB 快照 文件。它包含了在某个时间点上 Redis 数据库的完整状态。
- 作为 AOF 持久化的一部分,Redis 7.4 结合了 RDB 和 AOF 的优点,通过基础 RDB 文件和增量 AOF 文件来实现高效的数据持久化和恢复。
优点:
- 快速恢复:通过加载 RDB 快照,Redis 可以快速恢复到某个时间点的状态,而无需重放所有 AOF 命令。
- 减少文件大小:基础 RDB 文件存储了数据的全量快照,后续的增量 AOF 文件只记录自快照以来的变化,避免了 AOF 文件过大的问题。
2.
appendonly-6379.aof.1.incr.aof
(56)作用:
- 这是一个 增量 AOF 文件,记录了自基础 RDB 快照以来的所有写操作命令(如
SET
,HSET
,LPUSH
等)。- 增量 AOF 文件用于补充基础 RDB 快照,确保在恢复时可以通过重放这些命令来达到最新的数据状态。
优点:
- 高效写入:相比传统 AOF 记录所有写操作,增量 AOF 只记录快照之后的变化,减少了磁盘写入量。
- 灵活管理:可以定期生成新的基础 RDB 快照,并清理旧的增量 AOF 文件,优化存储空间。
3.
appendonly-6379.aof.manifest
(98)作用:
- 这是一个 清单文件(Manifest File),用于管理和跟踪基础 RDB 文件与对应的增量 AOF 文件之间的关系。
- 该文件记录了哪些增量 AOF 文件对应于哪个基础 RDB 文件,确保在数据恢复时能够正确地加载和重放命令。
优点:
- 数据一致性:通过清单文件,Redis 可以准确地知道需要加载哪些文件来恢复数据,避免数据不一致的问题。
- 自动管理:清单文件帮助 Redis 自动管理文件的生命周期,如删除过期的增量 AOF 文件,维护持久化目录的整洁。
二、Redis 7.4 AOF 持久化机制的改进
Redis 7.4 引入了 混合持久化(Hybrid Persistence) 机制,将 RDB 和 AOF 结合起来,以充分利用两者的优势:
- 基础 RDB + 增量 AOF:
- 定期生成基础 RDB 快照,作为持久化的基准点。
- 记录基础 RDB 之后的所有写操作到增量 AOF 文件中,确保数据的实时性和持久性。
- 高效恢复:
- 在恢复数据时,Redis 首先加载基础 RDB 文件,快速恢复到某个时间点的状态。
- 然后重放对应的增量 AOF 文件,达到最新的数据状态。
- 优化存储和性能:
- 通过将持久化过程分为全量快照和增量记录,减少了 AOF 文件的大小和重写开销。
- 提高了持久化和恢复的效率,降低了对系统性能的影响。
三、如何管理这些文件
1. 自动管理
Redis 7.4 会自动生成和管理这些文件,包括:
- 生成基础 RDB 文件:根据配置的策略(如 AOF 重写触发条件),定期生成新的基础 RDB 文件。
- 记录增量 AOF:在基础 RDB 文件生成后,开始记录新的写操作到增量 AOF 文件中。
- 更新清单文件:确保清单文件准确反映当前的持久化文件结构。
2. 手动管理
虽然 Redis 会自动管理这些文件,但您仍可以进行一些手动操作以优化或排查问题:
- 触发 AOF 重写:可以使用
BGREWRITEAOF
命令手动触发 AOF 重写,生成新的基础 RDB 文件和增量 AOF 文件。- 备份持久化文件:定期备份
appendonlydir
目录下的所有持久化文件(包括.rdb
,.aof
,.manifest
)以防止数据丢失。- 监控文件大小:监控各类持久化文件的大小,确保磁盘空间充足,并根据需要调整持久化策略。
四、配置示例
在 Redis 配置文件 (
redis.conf
) 中,相关配置可能如下:# 启用 AOF 持久化 appendonly yes # AOF 文件的名称 appendfilename "appendonly.aof" # AOF 同步策略 appendfsync everysec # 混合持久化配置 # 具体配置项可能因 Redis 版本而异,请参考官方文档
注意:Redis 7.4 的混合持久化机制可能引入了新的配置选项,请务必参考 Redis 官方文档 以获取最新和详细的配置说明。
五、总结
Redis 7.4 在 AOF 持久化机制上引入了基础 RDB 文件、增量 AOF 文件和清单文件的结构,通过混合持久化机制,结合了 RDB 和 AOF 的优势,实现了高效、可靠的数据持久化和快速恢复。这些文件的存在确保了 Redis 在高负载和大数据量的场景下,能够保持数据的完整性和系统的高可用性。
理解和正确管理这些持久化文件,对于保障 Redis 数据的安全性和系统的稳定性至关重要。建议定期备份持久化文件,并监控文件的大小和系统性能,以确保 Redis 实例的健康运行。
4.5 AOF rewrite 重写
appendonly-6379.aof.1.base.rdb
:基础 RDB 快照文件。
appendonly-6379.aof.1.incr.aof
:增量 AOF 文件,记录自基础快照以来的所有写命令。
appendonly-6379.aof.manifest
:清单文件,管理基础 RDB 文件与增量 AOF 文件的关系。
4.5.1 基础 RDB 文件与增量 AOF 文件的工作机制
- 基础 RDB 文件 (
appendonly-6379.aof.1.base.rdb
)
- 作用:保存某一时间点的数据库完整状态,相当于一个 RDB 快照。
- 更新条件:当进行 AOF 重写(AOF Rewrite)操作时,Redis 会生成一个新的基础 RDB 文件。此操作可以自动触发,也可以手动执行。
- 增量 AOF 文件 (
appendonly-6379.aof.1.incr.aof
)
- 作用:记录自上一个基础 RDB 快照以来的所有写命令(增量操作)。
- 更新方式:在 Redis 运行过程中,所有写操作都会被追加到当前的增量 AOF 文件中。
- 清单文件 (
appendonly-6379.aof.manifest
)
- 作用:跟踪和管理基础 RDB 文件与增量 AOF 文件之间的关系,确保在数据恢复时能够正确加载基础快照并应用增量命令。
4.5.2 增量备份的工作原理与配置
增量备份主要依赖于基础 RDB 文件和增量 AOF 文件的组合。通过这种方式,你可以在保持高效的同时,实现数据的持续备份。
增量备份的工作流程
- 基础快照生成:
- 当执行 AOF 重写操作时,Redis 会生成一个新的基础 RDB 文件,记录当前数据库的完整状态。
- 记录增量操作:
- 在基础快照生成后,所有新的写操作会被记录到新的增量 AOF 文件中。
- 管理文件关系:
- 清单文件 (
appendonly-6379.aof.manifest
) 记录了当前使用的基础 RDB 文件和对应的增量 AOF 文件,确保数据恢复时能够正确加载。
- 清单文件 (
4.5.3 参数配置
[root@redis.xyy.org ~]#vim /apps/redis/etc/redis.conf
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
aof-timestamp-enabled no
以下是这些参数的详细解释及其在增量备份中的作用:
appendonly yes
:启用 AOF 持久化。
appendfilename "appendonly.aof"
:指定 AOF 文件名。
appendfsync everysec
:每秒执行一次 FSYNC
操作,平衡性能与持久性。
no-appendfsync-on-rewrite no
- 解释:当设置为
yes
时,Redis 在执行 AOF 重写期间会停止执行FSYNC
操作,从而提高性能;设置为no
则不会停止FSYNC
。 - 建议:默认情况下建议保持
no
,确保数据的持久性,尤其在对数据一致性要求较高的场景。
- 解释:当设置为
auto-aof-rewrite-percentage 100
- 解释:定义 AOF 文件增长的比例,达到此比例后触发 AOF 重写。
100
表示当当前 AOF 文件大小是上一次重写后的大小的 2 倍时触发重写(增长了 100%)。 - 建议:根据实际数据写入量和系统性能调整此值。较低的比例会更频繁地进行重写,但可能影响性能;较高的比例则减少重写频率,但可能导致 AOF 文件过大。
- 解释:定义 AOF 文件增长的比例,达到此比例后触发 AOF 重写。
auto-aof-rewrite-min-size 64mb
- 解释:设置 AOF 重写的最小触发文件大小。只有当 AOF 文件大小超过
64MB
且增长比例达到auto-aof-rewrite-percentage
时,才会触发重写。 - 建议:确保设置一个合理的最小值,以避免频繁的小规模重写,影响性能。
- 解释:设置 AOF 重写的最小触发文件大小。只有当 AOF 文件大小超过
aof-load-truncated yes
- 解释:当 AOF 文件不完整或被截断时,是否允许 Redis 加载这些文件。
yes
表示允许加载,并尽可能恢复数据。 - 建议:在生产环境中建议设置为
no
,以避免加载损坏的数据。如果设置为yes
,需要确保有其他数据恢复机制,以防止数据丢失。
- 解释:当 AOF 文件不完整或被截断时,是否允许 Redis 加载这些文件。
aof-use-rdb-preamble yes
- 解释:在 AOF 文件开头包含一个 RDB 快照的前导数据(preamble)。这有助于加快数据加载速度。
- 建议:默认建议保持
yes
,提高数据恢复的效率。
aof-timestamp-enabled no
- 解释:是否在 AOF 文件中记录命令的时间戳。
no
表示不记录,yes
表示记录。 - 建议:通常设置为
no
,除非你有特定需求需要记录时间戳。
- 解释:是否在 AOF 文件中记录命令的时间戳。
# 通过 bgrewriteaof 手动触发重写机制
[root@Rocky9.4 data]#redis-cli -a 123456 bgrewriteaof;pstree -p | grep redis ;ll appendonlydir/ -h
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
Background append only file rewriting started
|-redis-server(22944)-+-redis-server(23325)
| |-{redis-server}(22946)
| |-{redis-server}(22947)
| |-{redis-server}(22948)
| |-{redis-server}(22949)
| `-{redis-server}(22950)
total 180M
-rw-r--r-- 1 redis redis 180M Dec 28 23:25 appendonly.aof.1.base.rdb
-rw-r--r-- 1 redis redis 719K Dec 29 23:04 appendonly.aof.1.incr.aof
-rw-r--r-- 1 redis redis 0 Dec 29 23:08 appendonly.aof.2.incr.aof
-rw-r--r-- 1 redis redis 132 Dec 29 23:08 appendonly.aof.manifest
[root@Rocky9.4 data]#ll appendonlydir/
total 183420
-rw-r--r-- 1 redis redis 187817903 Dec 29 23:08 appendonly.aof.2.base.rdb
-rw-r--r-- 1 redis redis 0 Dec 29 23:08 appendonly.aof.2.incr.aof
-rw-r--r-- 1 redis redis 88 Dec 29 23:08 appendonly.aof.manifest
[root@Rocky9.4 data]#
5.Redis 常用命令
5.1 ACL 控制
user <username> [on|off] [>password] [~pattern] [+permissions] [-permissions]
命令权限(+ / - / ~ / allcommands / nocommands)
+<command>
:允许执行某个命令(如+get
,+set
)-<command>
:禁止执行某个命令allcommands
:允许执行所有命令nocommands
:禁止执行所有命令~<pattern>
:这是匹配命令子令(SUBCOMMANDS),也可以通过+<command>|subcommand
的形式添加特定子命令权限
on
或 off
:启用或禁用用户。
>password
:设置用户的密码,可以有多个密码。
~pattern
:指定用户可以访问的键的模式(可选)。
+permissions
和 -permissions
:授予或撤销用户的权限,可以使用命令类别或具体命令。
# 假设您希望创建一个名为 alice 的用户,设置密码为 123456,并且授予她所有权限但禁用 FLUSHALL 命令。可以按照以下方式配置:
[root@Rocky9.4 etc]#vim redis_6379.conf
user alice on >123456 ~* +@all -FLUSHALL -FLUSHDB
user default on >123456 ~* +@all +get +set -FLUSHALL -FLUSHDB -keys -config
# 重启服务
systemctl restart redis
# 连接到Redis
[root@Rocky9.4 etc]#redis-cli -u redis://alice:123456@127.0.0.1:6379
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> FLUSHALL
(error) NOPERM User alice has no permissions to run the 'flushall' command
127.0.0.1:6379> exit
Key 权限(~ / %)
~<pattern>
:允许对匹配<pattern>
的 key 进行读写操作&<pattern>
:只读访问(7.0+ 里对 key 权限有进一步细分,比如读写分离,会使用%
符号来区分写权限)%<pattern>
:只写访问(这是 Redis 7.0+ 扩展的语法,用于区分只写权限)allkeys
/nokeys
:允许/禁止访问所有 key
解释:
user alice
:定义用户名为alice
。on
:启用该用户。>123456
:设置用户的密码为123456
。~*
:允许用户访问所有键。+@all
:授予用户所有命令权限。-FLUSHALL -FLUSHDB
:撤销FLUSHALL
和FLUSHDB
命令的权限,防止用户执行这些危险命令。
# 默认用户去掉flushall 和 flushdb
user default on >123456 &logs:* ~* +@all -FLUSHALL -FLUSHDB
# 动态管理 ACL
# 1.ACL List(查看当前所有用户配置的详细信息,包括用户名称、密码哈希、权限列表、key patterns 等。)
[root@Rocky9.4 etc]#redis-cli -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> acl list
1) "user alice on sanitize-payload #87b9b7660e9fe77503b14eb6da0277151e031aad3a88a1673b798d8443af242b resetchannels -@all"
2) "user default on sanitize-payload #8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 ~* resetchannels &logs:* +@all -flushall -flushdb"
127.0.0.1:6379>
# 2.ACL GETUSER <username>(获取指定用户的ACL信息)
127.0.0.1:6379> ACL GETUSER alice
1) "flags"
2) 1) "on"
2) "sanitize-payload"
3) "passwords"
4) 1) "87b9b7660e9fe77503b14eb6da0277151e031aad3a88a1673b798d8443af242b"
5) "commands"
6) "-@all"
7) "keys"
8) ""
9) "channels"
10) ""
11) "selectors"
12) (empty array)
127.0.0.1:6379>
# 3.ACL SETUSER <username> [规则 ...](更新(创建或修改)用户的权限规则)
ACL SETUSER bob on >bob_password +get +set -FLUSHALL -FLUSHDB ~bob:*
ACL SETUSER bob on:启用 bob
>bob_password:设置 bob 的密码
+get +set:允许 bob 执行 get、set 命令
~bob:*:只允许 bob 访问前缀为 bob: 的 key
# 4.ACL DELUSER <username> [<username> ...](删除用户及其权限配置)
ACL DELUSER alice
ACL DELUSER bob
# 5.ACL SAVE(将内存中的 ACL 配置写回到 aclfile(如果在配置文件中指定了 aclfile 路径的话)中。默认不写入 redis.conf,如果希望保存到文件,需要先在 redis.conf 中指定:)
注意:在redis.conf中配置aclfile,就不能同时配置user alice...,必须要将配置写入到aclfile,并且还要将这个文件手动创建出来,服务才会重启成功
aclfile /path/to/aclfile.conf
5.2 INFO
作用:查看 Redis 服务器的各种统计信息和状态,例如内存使用情况、复制状态、连接数、持久化信息、keyspace 信息等。
使用示例:
127.0.0.1:6379> INFO keyspace
# Keyspace
db0:keys=10000001,expires=0,avg_ttl=0,subexpiry=0
127.0.0.1:6379>
127.0.0.1:6379> info server
# Server
redis_version:7.4.1
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:a9a1c6875521b0ad
redis_mode:standalone
os:Linux 5.14.0-427.13.1.el9_4.x86_64 x86_64
arch_bits:64
monotonic_clock:POSIX clock_gettime
multiplexing_api:epoll
atomicvar_api:c11-builtin
gcc_version:11.5.0
process_id:9692
process_supervised:no
run_id:83d8b9655623d6edaf809f8a7456e68179e9de91
tcp_port:6379
server_time_usec:1735883510494144
uptime_in_seconds:6113
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:7830262
executable:/apps/redis/bin/redis-server
config_file:/apps/redis/etc/redis_6379.conf
io_threads_active:0
listener0:name=tcp,bind=0.0.0.0,bind=-::1,port=6379
127.0.0.1:6379>
Redis 会返回一个多段文本,包含大量信息,可按模块划分(server、clients、memory、persistence、stats、replication、cpu、cluster、keyspace 等)。
常用操作:
INFO memory
:只查看和内存相关的信息。INFO replication
:只查看主从复制(replication)相关信息。INFO server
: 查看server的相关信息INFO cluster
: 查看集群的信息INFO keyspace
: 查看数据库的信息,有多少键.
5.3 SELECT
作用:切换 Redis 的逻辑数据库(DB)。Redis 默认有 16 个逻辑数据库,编号 0 到 15(可通过 databases
配置修改)。
使用示例:
127.0.0.1:6379> SELECT 1
OK
127.0.0.1:6379[1]> # 命令提示符会显示当前已在 DB1
注意点:
- Redis 集群模式下通常只能使用 DB 0,不支持多 DB 切换。
- 切换数据库仅对当前连接有效,断开后重新连接默认仍进入 DB 0。
5.4 KEYS
作用:列出匹配给定模式(pattern)的所有 Key。常用模式如 KEYS user:*
。
使用示例:
127.0.0.1:6379> KEYS *
1) "foo"
2) "bar"
3) "user:1001"
注意点:
- 不要在生产环境中频繁使用
KEYS
命令,因为它会对整个 keyspace 做遍历,耗时且阻塞服务器,容易导致性能问题,最好使用acl禁用keys*。 - 生产中更常用
SCAN
命令以非阻塞(游标)方式遍历 key。
5.5 BGSAVE
作用:在后台(异步)执行一次 RDB 快照,将当前数据集保存到磁盘(默认文件名 dump.rdb
,可在 redis.conf
中配置)。
使用示例:
127.0.0.1:6379> BGSAVE
Background saving started
执行后会立即返回,Redis 会在后台完成 RDB 持久化。
常用场景:
- 手动触发一次快照,做数据备份。
- 结合
save
配置自动触发也可以,但在现代 Redis 版本中,很多人更倾向于 AOF 或混合持久化。
5.6 DBSIZE
作用:返回当前所选数据库(DB)中 key 的数量。
使用示例:
127.0.0.1:6379> DBSIZE
(integer) 10000001
127.0.0.1:6379>
表示当前 DB 里有 一千万 个 key。
注意点:
DBSIZE
是一个简单计数,不涉及 keys 的遍历,因此非常高效。- 如果想知道所有 DB 的 key 数量,可以逐一
SELECT
0~15 执行DBSIZE
,或者用INFO keyspace
查看。
5.7 FLUSHDB
作用:清空当前数据库(DB)的所有 Key(只影响您当前选择的 DB)。
使用示例:
127.0.0.1:6379> FLUSHDB
OK
如果在 DB0 执行,会清空 DB0;在 DB1 执行会清空 DB1。
风险与注意:
- 不可逆,会删除当前 DB 中的全部数据!
- 可在正式执行前先确认
SELECT
到了正确的 DB,避免误删。
5.8 FLUSHALL
作用:清空 所有 DB 的所有 Key(默认 16 个数据库都会被清空)。
使用示例:
127.0.0.1:6379> FLUSHALL
OK
风险与注意:
- 比
FLUSHDB
更危险,一旦执行,会导致 Redis 整个实例所有 DB 数据被清空。 - 一般不建议在生产环境使用,务必谨慎操作!
5.9 SHUTDOWN
作用:关闭 Redis 服务器。执行后,会尝试做一次持久化(若配置了 RDB/AOF),然后退出进程。
使用示例:
127.0.0.1:6379> SHUTDOWN
连接会立即断开,Redis 服务停止。
注意点:
- 如果想避免保存数据(即不再进行 RDB/AOF 落盘),可加选项
SHUTDOWN NOSAVE
。 - 如果只想保存数据而不关闭,可以执行
SAVE
或BGSAVE
。
5.10 SCAN
在 Redis 中,SCAN
命令是一种基于游标(cursor)**的迭代查询方式,能够**分批且非阻塞地遍历数据库中的 Key(或集合、哈希、ZSet 等),避免像 KEYS *
这样一次性扫描全部 Key 导致大规模阻塞的问题。下面介绍一下 SCAN
的核心概念、使用方法以及与 KEYS
的区别。
一、为什么要用 SCAN?
- KEYS 的缺点:
KEYS pattern
命令会一次性遍历所有 Key,并返回所有匹配的结果。- 在 Key 数量很大的情况下(几百万上千万),一次扫描会造成主线程阻塞,期间无法处理其他请求,导致服务卡顿甚至超时。
- 因为 Redis 是单线程架构,这种大规模阻塞会严重影响线上业务。
- SCAN 的优点:
- 将大范围扫描拆分成多次小范围扫描,每次只返回一部分数据。
- 采用“游标(Cursor)+ 增量遍历”的模式,每扫描一部分,Redis 就返回一个新的 Cursor,并在响应中包含本次扫描到的部分数据。
- 用户可以根据返回的 Cursor 继续下一次扫描,直到 Cursor 回到 0 表示扫描结束。
- 相比
KEYS
,SCAN
对服务器的阻塞时间更短,也更可控。
二、SCAN 的基本用法
2.1 命令格式
SCAN cursor [MATCH pattern] [COUNT count]
- cursor:游标,初次调用时通常传
0
,表示从头开始扫描。 - MATCH pattern:可选,用于过滤匹配的 Key 模式(如
MATCH user:*
)。如果不指定MATCH
,则返回的 Key 不做任何模式过滤(会返回所有 Key 的子集)。 - COUNT count:可选,用于指定每次扫描希望返回的 Key 数量。并非严格保证返回固定数量,而是“期望值”,Redis 可能实际返回多于或少于这个数的 key。
初次调用:
127.0.0.1:6379> SCAN 0 COUNT 10
1) "13107200"
2) 1) "k4533779"
2) "k252114"
3) "k933235"
4) "k3676789"
5) "k2576537"
6) "k7573677"
7) "k5285770"
8) "k2267950"
9) "k2473601"
10) "k4433328"
- 返回结果中的第一个元素
"13107200"
是新的游标值,下次扫描时要用它。 - 第二个数组是本次扫描到的一批 Key(例如 5~10 个)。
后续调用:
127.0.0.1:6379> SCAN 13107200 COUNT 10
1) "4456448"
2) 1) "k1450912"
2) "k9102989"
3) "k6829708"
4) "k3410677"
5) "k2513869"
6) "k9564207"
7) "k7683296"
8) "k2951179"
9) "k6113726"
10) "k8041825"
127.0.0.1:6379>
- 继续用上一次返回的游标 13107200 作为本次调用的游标;
- Redis 返回游标 “4456448” 和新的 Key 列表。
三、SCAN 与其他相关命令
SCAN:遍历数据库中的 Key。
SSCAN:遍历 Set 中的元素。
HSCAN:遍历 Hash 中的 field-value 对。
ZSCAN:遍历 Sorted Set 中的 member-score 对。
它们用法相似,都是 SCAN cursor [MATCH pattern] [COUNT count]
的形式,只是操作的数据结构不同。例如:
HSCAN myhash 0 MATCH field:* COUNT 10
四、SCAN vs. KEYS
- 性能:
KEYS
命令会阻塞 Redis 直到扫描完所有 Key;SCAN
采用增量扫描,每次只处理一部分,能把阻塞时间分散到多个小的时间片,对线上性能影响更小。
- 用法:
KEYS
适合在测试或小规模场景下调试时使用,方便一次性获取所有匹配 key;- 生产环境中强烈推荐使用
SCAN
,能避免大规模阻塞。
- 一致性:
KEYS
在那一刻会返回快照式的所有 Key;SCAN
可能会出现漏扫或重复,尤其当 Key 动态变化时。但大多数情况下,这种不完全一致性是能接受的(可额外在应用层做去重处理)。
6.Redis 数据类型
6.1 字符串(string)类
6.1.1 String 的存储特点
- 存储内容:可以是文本(如
"Hello"
)、数字(如"123"
)、二进制文件(如图片、音频等),只要单个值不超过 512 MB 即可。 - 内存开销:小字符串采用
SDS(Simple Dynamic String)
结构存储,Redis 会根据实际值大小自动选择合适的底层结构,避免频繁的内存分配。 - 常用场景:
- 缓存网页内容、配置、令牌、session 信息、计数器等;
- 存储对象的序列化结果,比如 JSON、Protobuf 等;
- 计数统计,利用
INCR
/DECR
等快速自增自减
6.1.2 常见的操作命令
6.1.2.1 设置与获取
6.1.2.1.1 SET
-
作用:设置 key 的值。如果 key 已存在,会被覆盖;如果 key 不存在,则创建一个新的 key。
-
基本语法:
SET <key> <value>
-
示例:
SET user:1001 "Alice" GET user:1001 # 返回 "Alice"
-
扩展参数:
-
EX seconds
:设置过期时间(秒)。 -
PX milliseconds
:设置过期时间(毫秒)。 -
NX
:只有当 key 不存在时才执行设置。 -
XX
:只有当 key 存在时才执行设置。 -
例如:
SET mykey "Hello" EX 10 NX
表示仅当
mykey
不存在时,才设置值为"Hello"
并自动在 10 秒后过期。
-
6.2.2.1.2 GET
-
作用:获取 key 的字符串值。
-
示例:
GET mykey
若 key 存在,则返回对应的值;若 key 不存在,返回
nil
6.2.2.1.3 MSET / MGET
-
MSET:同时设置多个 key-value 对。
MSET k1 "v1" k2 "v2" k3 "v3"
一次性写入多对数据,减少多次网络往返。
-
MGET:批量获取多个 key 的值。
MGET k1 k2 k3
返回一个数组,如
[v1, v2, v3]
,不存在的 key 会以nil
对应。
6.2.2.1.4 SETNX / SETXX(或结合 SET 命令的 NX / XX 参数)
SETNX
:Set if Not eXists,只在 key 不存在时设置成功。等价于SET key value NX
。SETXX
:Set if eXists,只在 key 存在时设置成功。等价于SET key value XX
。
6.1.2.2 数值操作
Redis 支持对字符串值进行数字自增自减操作(前提是该字符串能被解析为整数或浮点数)。
6.1.2.2.1 INCR / DECR
-
INCR :将 key 的值自增 1。如果 key 不存在,则先初始化为 0 再自增。
-
DECR :将 key 的值自减 1。
-
示例:
SET counter 10 INCR counter # counter = 11 DECR counter # counter = 10
-
注意:若 value 不是一个整数字符串,例如
"Hello"
或1.5
,执行INCR
/DECR
会报错 (ERR value is not an integer)。
6.1.2.2.2 INCRBY / DECRBY
-
作用:一次性加/减指定数值。
INCRBY <key> <increment> DECRBY <key> <decrement>
-
示例:
SET counter 100 INCRBY counter 50 # counter = 150 DECRBY counter 20 # counter = 130
6.1.2.2.3 INCRBYFLOAT
-
作用:对浮点数进行加法操作。
SET price 12.5 INCRBYFLOAT price 0.7 # price = 13.2
-
使用场景:计量或需要小数的场景,如金额、温度等。
6.1.2.3 部分字符串操作
6.1.2.3.1 APPEND
-
作用:向指定 key 的现有值 追加 一段字符串。如果 key 不存在,就相当于
SET
。 -
示例:
SET greeting "Hello" APPEND greeting ", Redis!" GET greeting # "Hello, Redis!"
6.1.2.3.2 GETRANGE (旧命令:SUBSTR)
-
作用:获取字符串中指定区间(基于下标)的子串,区间含左右边界,支持负索引(-1 表示最后一个字符)。
-
语法:
GETRANGE <key> <start> <end>
-
示例:
SET greeting "Hello, Redis!" GETRANGE greeting 0 4 # 返回 "Hello" GETRANGE greeting -6 -2 # 返回 "Redis" 中的一部分, 要根据实际字符串判断
注意字符串
"Hello, Redis!"
的长度和下标分布,从 0 到 12。
6.1.2.3.3 SETRANGE
-
作用:在指定偏移量开始处,用给定值覆盖或插入到现有字符串里。若偏移量超过当前字符串长度,中间会填充空字节(
\x00
)。 -
示例:
SET mykey "Hello World" SETRANGE mykey 6 "Redis" # 原字符串第 6 位开始覆盖,"Hello Redis" GET mykey # "Hello Redis"
6.1.2.3.4 STRLEN
-
作用:返回字符串值的长度(字节数)。
-
示例:
SET name "Alice" STRLEN name # 返回 5
6.1.2.4 过期与生命周期管理
尽管不是 String 专有命令,但实际中常和 String 配合使用:
- EXPIRE :为 key 设置过期时间(单位秒)。到期后 key 会被自动删除。
- TTL :查看 key 剩余存活时间。
- PERSIST :移除 key 的过期时间,使 key 变为永久存在。
也可在 SET
命令时直接附带 EX
或 PX
参数来设置过期时间。
6.1.2.5 高级应用:Bit 操作
Redis 还提供了对 String 值执行位操作(bitwise)的命令,如 SETBIT
, GETBIT
, BITCOUNT
, BITOP
等,能在每个位上进行读写、计数或逻辑运算。
- SETBIT :将字符串第 offset 位设置为 value (0 或 1)。
- GETBIT :获取 offset 位置的位值。
- BITCOUNT [start end]:统计字符串指定区间内值为 1 的位数。
- BITOP AND/OR/XOR [… ]:对多个 key 对应的位进行逻辑运算,并将结果存储到
destkey
。
这些命令常用于**实现位图(bitmap)**功能,比如用户签到、活跃状态等布尔属性的存储。
6.1.2.6 小结 & Best Practices
- 最常用的操作:
SET
/GET
:基础读写;MSET
/MGET
:批量读写;INCR
/DECR
/INCRBYFLOAT
:实现计数器或数值操作;APPEND
/GETRANGE
/SETRANGE
:在字符串上做增改或截取;EXPIRE
/SET ... EX
:配合过期管理。
- 注意字符串与数值:
- 只有当字符串能被解析为整数或浮点数时,
INCR
/DECR
/INCRBYFLOAT
才能工作,否则会报错; - 若您频繁进行数值操作,不要在中间手动
GET
并转回数字,以免造成竞态。直接用原子操作(INCR…)更可靠。
- 只有当字符串能被解析为整数或浮点数时,
- 内存与大小限制:
- Redis 默认可存储最大 512MB 的值,但太大的值会带来内存和网络传输负担,不建议把超大文件直接放 Redis。
- 建议拆分或使用外部文件存储(OSS、S3、NFS),Redis 中只存元数据或指针。
- 使用过期策略:
- 若用 Redis 做缓存,通常会为 key 设置过期时间,避免数据无限堆积或过期失效。
- Bit 操作:
- 高级玩法,能有效利用 Redis 做位图统计,如用户行为打点、日活统计等,但要注意数据结构设计和偏移量计算。
# 设置值,并设置有效期(s)--10秒过期,也可以通过SETEX来直接设置
set title study ex 10
# 存在title,不变,不存在设置成ceo
127.0.0.1:6379> SETNX title ceo
(integer) 1
# 批量设置--mset/mget
127.0.0.1:6379> mset name xingyuyu age 18 gender femal title cto
OK
127.0.0.1:6379> mget name age
1) "xingyuyu"
2) "18"
127.0.0.1:6379>
6.2 哈希(Hash)
在 Redis 中,Hash(哈希) 是一种将多个字段(field)映射到同一个 key 下的数据结构。换句话说,Redis 的 Hash 类似一个小型的 key-value 表,表里的 “行” 只有一条,但这条“行”有许多字段(field)。它非常适合用来存储和读取类似对象、用户资料、配置项等场景。下面将按照前面类似的步骤,讲解 Hash 的基本概念、常用操作以及使用场景与注意事项。
6.2.1 Redis Hash 的存储与特点
- 内部结构
- 小规模的哈希对象(字段数量较少时),Redis 会采用一种紧凑的数据结构(ziplist 或 listpack)来节省内存。
- 当字段数量或字段长度变大时,会转换成真正的哈希表(Hashtable)。
- 这对我们是透明的,Redis 会自动处理。但它解释了为什么 Hash 往往在小规模场景下特别高效。
- 数据特征
- Key:在 Redis 层面标识一个哈希对象的名称(如
user:1001
)。 - Fields 和 Values:在哈希对象内部,每个 field 都拥有一个独立的 value。不同 field 之间互不干扰,但都属于同一个 key 的管理之下。
- 字段唯一:同一个哈希键下的 field 名称是不能重复的,若重复则会更新原有值。
- Key:在 Redis 层面标识一个哈希对象的名称(如
- 常用场景
- 存储用户信息(字段:username、email、age 等);
- 缓存一条记录或配置,这些记录可有多个属性;
- 减少 key 数量(相比将每个属性单独存为一个 String key,这时可以把多个属性存在一个哈希 key 中)。
6.2.2 Hash 的常见操作命令
6.2.2.1 基础增删改查
6.2.2.1.1 HSET / HGET
-
HSET
-
作用:向 hash 里设置(或更新)一个字段的值。如果字段不存在,就创建;若存在,就覆盖。
-
语法:
HSET <key> <field> <value> [<field2> <value2> ...]
-
示例:
HSET user:1001 name "Alice" age "20"
若
user:1001
不存在,Redis 会创建一个新的哈希;并设置字段name="Alice"
、age="20"
-
-
HGET
-
作用:获取 hash 表中某个字段的值。
-
语法:
HGET <key> <field>
-
示例:
HGET user:1001 name # 返回 "Alice"
-
6.2.2.1.2 HMSET / HMGET
-
HMSET(在新版本 Redis 中
HSET <key> <field> <value> ...
已经可以替代,HMSET 旧命令仍兼容)-
作用:同时设置多个字段值,和
HSET
的多字段形式一致。 -
示例:
HMSET user:1001 city "Beijing" gender "female"
-
-
HMGET
-
作用:一次性获取多个字段的值。
-
示例:
HMGET user:1001 name age city # 返回一个数组 ["Alice", "20", "Beijing"]
-
6.2.2.1.3 HGETALL / HKEYS / HVALS
-
HGETALL
-
作用:获取 hash 中所有字段和对应值,以 field-value 对数组返回。
-
示例:
HGETALL user:1001 # 可能返回 ["name","Alice","age","20","city","Beijing","gender","female"]
-
-
HKEYS
- 作用:只返回所有字段(field)的列表。
-
HVALS
- 作用:只返回所有字段的值列表。
-
这三种命令在字段很多时,一次性返回也会占用较多资源,生产环境可考虑
HSCAN
(分批扫描)以避免阻塞。
6.2.2.1.4 HDEL
-
作用:删除哈希表中的一个或多个字段。
-
语法:
HDEL <key> <field1> [field2 ...]
-
示例:
HDEL user:1001 age # 移除 "age" 字段
6.2.2.1.5 HEXISTS
-
作用:检查哈希表中指定字段是否存在。
-
示例:
HEXISTS user:1001 name # 若存在则返回 1,否则返回 0
6.2.2.2 数值操作(原子自增自减)
如果哈希字段的值可以解析为数字(整数或浮点数),Redis 提供了原子增量命令:
6.2.2.2.1 HINCRBY
-
作用:对 hash 某个字段的值执行加法(整数)。
-
语法:
HINCRBY <key> <field> <increment>
-
示例:
HINCRBY user:1001 login_count 1 # 将 user:1001 哈希表里 login_count 字段值 +1
-
注意:若字段不存在,会先初始化为 0,再进行加法。如果原来是非数字值,会报错。
6.2.2.2.2 HINCRBYFLOAT
-
作用:对 hash 字段的值执行浮点加法。
-
语法:
HINCRBYFLOAT <key> <field> <increment>
-
示例:
HINCRBYFLOAT product:2001 price 9.99 # 如果 price="100.0",则更新后 price="109.99"
6.2.2.3 Hash 扫描(HSCAN)
-
HSCAN
-
与
SCAN
类似,用于分批遍历一个哈希表,避免HGETALL
在字段太多时造成阻塞。 -
用法:
HSCAN <key> <cursor> [MATCH pattern] [COUNT count]
-
示例(伪代码):
cursor = 0 do { response = HSCAN user:1001 cursor MATCH "f*" cursor = response[0] # 新游标 fields = response[1] # 字段值对 # ...处理 fields ... } while (cursor != 0)
-
这样可以一口气分多次小范围返回,不会阻塞 Redis 主线程太久。
-
6.2.3 Hash 的使用场景
- 用户配置、信息
user:1001
存放 name、email、age、login_count 等多个字段;- 避免把每个字段都做一个单独的 String key。
- 数据对象缓存
- 例如商品信息
product:2001
,其中字段 price、title、stock 等; - 更新时只需改对应字段,不必覆盖整个对象;
- 还能减小内存,因为小哈希在 Redis 内部可用紧凑结构存储。
- 例如商品信息
- 记录统计量
- 可以在 Hash 中存放各种计数器字段(点击量、点赞量、回复数等),通过
HINCRBY
实现原子自增。 - 一个大对象的多种统计值都集中在同一个 hash key 下,管理更方便。
- 可以在 Hash 中存放各种计数器字段(点击量、点赞量、回复数等),通过
- 减少 key 的数量
- 在某些业务场景中,为了避免 key 过多带来的管理压力,把多个相关字段汇总到同一个 Hash key 下会简化命名空间;
- 但要注意单个哈希不宜过大,否则一次获取全部字段仍可能带来性能开销。
6.2.4 常见注意事项 & Best Practices
- 单个 Hash 不宜过度膨胀
- 虽然 redis.conf 里会控制 ziplist/listpack 的一些大小阈值,但如果字段规模过大,一次性
HGETALL
会占用大量内存带宽; - 对于超大哈希(上百万字段),谨慎使用或考虑拆分、使用
HSCAN
分批遍历。
- 虽然 redis.conf 里会控制 ziplist/listpack 的一些大小阈值,但如果字段规模过大,一次性
- 字段值只能是字符串
- Redis Hash 的 value 最终以字符串形式存储(即使是数字,也保存为字符串);
- 若要存储复杂对象,可以序列化后放进字段值,如 JSON 或 MsgPack,更新局部字段可能就需要再解析 JSON。
- 过期和生命周期
- Redis 只能对整个 Hash key 设置过期时间(如
EXPIRE user:1001 3600
),无法对 Hash 里某个字段单独设置过期。 - 若只想让特定字段有失效时间,需要业务层逻辑配合。
- Redis 只能对整个 Hash key 设置过期时间(如
- Hash vs. String
- 如果只有一个字段,使用 String 即可;
- 如果对象字段较多或需要一起管理,Hash 更方便,也可能更省内存(对小字段场景)。
- Hash vs. Set、ZSet
- Hash 专注于“字段-值”映射的结构,字段之间无顺序,主要用于描述对象属性;
- Set/ZSet 更侧重“集合”或“排序”语义;
- 业务逻辑决定选择。
- HXxx 命令的性能
- 在绝大多数情况下都为 O(1) 或 O(N)(N 为字段数)的操作,和内部哈希表实现相关。
- 大量字段更新时,也要留意操作分散性,别在高并发下对同一个大哈希做高频更新——可能会增大锁竞争(Redis 单线程)。
6.2.5 小结
Redis Hash 提供了紧凑且灵活的 “字段-值” 结构,适合存储类似对象、配置、多字段记录等。它的核心命令可总结为:
- 增改
HSET
/HMSET
:设置或更新字段HINCRBY
/HINCRBYFLOAT
:对数值字段原子自增
- 查询
HGET
/HMGET
:获取单个或多个字段值HGETALL
:获取全部字段和值HKEYS
/HVALS
:只获取字段 / 值列表HEXISTS
:判断字段是否存在HLEN
:获取字段总数
- 删除
HDEL
:删除一个或多个字段
- 扫描
HSCAN
:分批遍历所有字段
- 其他
EXPIRE
/TTL
等只能针对整个 key,不支持单字段到期
合理使用 Hash,可以有效减少 key 数量,提升空间利用率,且让数据结构更加接近业务对象形态。结合其他 Redis 数据类型(如 List、Set、ZSet)能够构建出更丰富的业务逻辑与功能。
- HSET
<key> <field> <value>
:设置 hash 表中的 field 字段值。 - HGET
<key> <field>
:获取 hash 表中 field 字段值。 - HMGET / HMSET:批量操作多个字段。
- HGETALL
<key>
:获取 hash 的所有字段和值。 - HDEL
<key> <field>
:删除指定字段。
6.3 列表(List)
在 Redis 中,List(列表) 是一个非常常用的数据类型,用来存储一个按插入顺序排序的字符串列表。它本质上可被视作一个双端队列,允许在队列头(left)或队列尾(right)进行插入、弹出等操作。
6.3.1 List 的存储与特点
- 存储结构:在内部,Redis 为 List 使用了一种类似双端链表或QuickList(在较新的版本中)来维护元素。
- 数据特征:
- 有序:按插入顺序保存元素,可在头/尾插入或弹出。
- 元素类型:每个元素都是一个字符串,可以是文本、JSON、二进制等,长度最大可达 512MB(与普通 String 的限制相同)。
- 常用场景:
- 实现消息队列 / 任务队列(如生产者-消费者模型)。
- 记录最新的操作日志或信息(用 LPUSH + LTRIM 维护一个固定长度)。
- 需要按顺序遍历、插入、或弹出的场景。
6.3.2 List 的常见操作命令
Redis 提供了多种与 List 交互的命令,下面分门别类介绍。
6.3.2.1 插入与弹出
6.3.2.1.1 LPUSH / RPUSH
- LPUSH [element …]:在列表头部插入一个或多个元素。新的元素会排在最前面。
- RPUSH [element …]:在列表尾部插入一个或多个元素。新的元素会排在最后面。
示例:
LPUSH mylist "world"
LPUSH mylist "hello"
# mylist = ["hello", "world"]
RPUSH mylist "!"
# mylist = ["hello", "world", "!"]
当 mylist
不存在时,会先创建一个空列表再插入。
6.3.2.1.2 LPOP / RPOP
- LPOP :移除并返回列表头部的第一个元素。
- RPOP :移除并返回列表尾部的最后一个元素。
示例:
LPOP mylist # 返回 "hello",mylist 变为 ["world", "!"]
RPOP mylist # 返回 "!", mylist 变为 ["world"]
6.3.2.1.3 LINSERT
- LINSERT BEFORE|AFTER
- 作用:在列表中的某个“现有元素”前或后,插入一个新元素。
- 如果
pivot
不存在,命令返回 -1;如果 key 不存在或列表为空,返回 0。
示例:
# mylist = ["hello", "world"]
LINSERT mylist BEFORE "world" "there"
# mylist = ["hello", "there", "world"]
6.3.2.2 获取与查看列表内容
6.3.2.2.1 LRANGE
-
LRANGE :返回列表在指定区间内的所有元素,包含左右边界。
-
下标可以是正数,0 表示第一个元素,1 表示正数第二个,以此类推。
-
下标可以为负数,-1 表示最后一个元素,-2 表示倒数第二个,以此类推。
-
示例:
# mylist = ["hello", "world", "!"] LRANGE mylist 0 -1 # 返回所有元素 ["hello","world","!"] LRANGE mylist 0 1 # 返回 ["hello","world"]
6.3.2.2.2 LINDEX
-
LINDEX :根据索引获取列表中的单个元素。
-
index 可为正数(从头开始 0 表示第一个)或负数(-1 表示最后一个)。
-
示例:
# mylist = ["hello", "world", "!"] LINDEX mylist 0 # 返回 "hello" LINDEX mylist -1 # 返回 "!"
6.3.2.2.3 LLEN
-
LLEN :返回列表的长度(元素个数)。
-
若 key 不存在,则返回 0;若 key 的类型不是 list,则报错。
-
示例:
# mylist = ["hello","world","!"] LLEN mylist # 返回 3
6.3.2.3 修改与截取
6.3.2.3.1 LSET
-
LSET :将列表中下标为
index
的元素设置为新的element
。 -
若索引越界或列表不存在,会返回错误。
-
示例:
# mylist = ["hello","world","!"] LSET mylist 1 "Redis" # mylist 变为 ["hello", "Redis", "!"]
6.3.2.3.2 LTRIM
-
LTRIM :对列表进行修剪,只保留指定区间的元素,区间外的全部删除。
-
注意截取的是保留区间,而非删除区间。
-
示例:
# mylist = ["hello","Redis","!"] LTRIM mylist 0 1 # 只保留下标 0 到 1 的元素,mylist = ["hello","Redis"]
6.3.2.3.3 LREM
-
LREM :按 count 的值移除指定元素:
- count > 0:从头到尾,删除最多 count 个等于 element 的元素;
- count < 0:从尾到头,删除最多 |count| 个等于 element 的元素;
- count = 0:删除所有等于 element 的元素。
-
示例:
# mylist = ["hello","world","hello","Redis"] LREM mylist 1 "hello" # 只从头到尾删除 1 个 "hello" -> mylist = ["world","hello","Redis"] LREM mylist 0 "hello" # 再把剩余的 "hello" 都删掉 -> mylist = ["world","Redis"]
6.3.2.4 队列 & 阻塞操作
6.3.2.4.1 RPOPLPUSH
-
RPOPLPUSH :
- 从
source
列表的尾部弹出一个元素,并将该元素插入到destination
列表的头部; - 返回弹出的元素值。
- 从
-
可把它当作右弹左推的队列操作,适用于在多个队列之间传递消息。
-
示例:
# list1 = ["job1","job2","job3"] # list2 = ["x","y"] RPOPLPUSH list1 list2 # -> 返回 "job3" # list1 = ["job1","job2"] # list2 = ["job3","x","y"]
6.3.2.4.2 BLPOP / BRPOP
-
BLPOP [key2 …] :
- 从左侧弹出第一个非空列表的头元素;如果所有列表都为空,则阻塞等待,直到有新元素插入或达到超时。
- 如果超时,返回
nil
。
-
BRPOP 类似,但从右侧弹出。
-
常用场景:消息队列或任务队列中,消费者阻塞等待任务的到来。
-
示例:
BLPOP list1 list2 30 # 优先检查 list1 是否有元素,如果空,再检查 list2 # 如果都为空,则最多阻塞 30 秒,期间如果有新元素插入便立即返回
LPOP/RPOP:如果列表为空,会立即返回 nil,不会阻塞。
BLPOP/BRPOP:如果列表为空,会阻塞等待(直到超时或有新数据),适合任务队列或实时消费场景。
6.3.3 List 的使用场景
- 消息/任务队列
- 生产者使用
LPUSH
/RPUSH
将任务入队; - 消费者使用
BLPOP
/BRPOP
阻塞式弹出任务进行处理; - 实现简单可靠的异步队列机制。
- 生产者使用
- 最新消息/日志列表
- 使用
LPUSH
不断往头部插入最新的消息; - 配合
LTRIM mylist 0 99
等截取操作,保留最近 100 条; - 方便快速获取最新信息,同时限制长度防止无限膨胀。
- 使用
- 数据分页或流式读取
- 可以用 List 存储一批数据,使用
LRANGE
分段获取; - 但要注意大规模数据可能导致内存占用多,可分而治之或考虑其他结构。
- 可以用 List 存储一批数据,使用
- Rotating Queue
- 使用
RPOPLPUSH
在多个队列之间转移数据,实现轮转或多队列消费等策略。
- 使用
6.3.4 常见注意事项 & Best Practices
- 避免存放超大量元素
- 虽然 List 可存数百万个元素,但一次
LRANGE
全量拉取也会占用大量内存和带宽。 - 如果只需要最新部分数据,建议使用
LTRIM
保留一定区间,或按业务逻辑拆分存储。
- 虽然 List 可存数百万个元素,但一次
- 使用阻塞命令需注意
BLPOP
/BRPOP
在等待时会占用一个连接。如果连接数有限或网络环境复杂,需要注意不要同时有太多阻塞请求。
- List 并非随机访问结构
- 只能通过索引下标进行有限的随机访问(LINDEX),或是依赖
LRANGE
全面遍历。 - 若需要频繁随机读写中间元素或多字段操作,或许
Hash
/Set
等其他结构更合适。
- 只能通过索引下标进行有限的随机访问(LINDEX),或是依赖
- 使用场景决定操作端
- 对于队列场景,一般约定一端入队,另一端出队,避免混用增删端,以形成一致的 FIFO(或 LIFO)顺序。
- 对于日志场景,通常是
LPUSH
+LTRIM
,永远从头部插入最新消息。
- Watch out for
LRANGE
large ranges- 尽管 Redis 的 List 操作性能较优,但一次性获取几百万条也会造成阻塞和内存压力。
- 生产环境中可以
LRANGE key start end
分段获取,或设计合理的数据拆分。
6.3.5 小结
List 在 Redis 中是一个强大且灵活的数据结构,可满足多种顺序场景需求,包括消息队列、最新列表、双端队列等。其核心操作可以概括为:
- 头/尾插入:
LPUSH
、RPUSH
- 头/尾弹出:
LPOP
、RPOP
- 读取区间:
LRANGE
、查看长度:LLEN
- 阻塞弹出:
BLPOP
、BRPOP
(常用于队列) - 部分修改:
LSET
、LTRIM
、LREM
- 多队列转移:
RPOPLPUSH
在实际业务中,通过合理地选用这些操作并结合过期策略或其他结构(例如存储 ID 在 List 中、实际数据放在 Hash 或 String 里),可以构建高效的队列模型、实时日志系统或其他顺序相关的功能。
6.4 集合(Set)
在 Redis 中,Set(集合) 类型用于存储一组不重复的字符串元素,和我们熟悉的数学集合很类似。Redis 提供了丰富的命令来操作集合,包括添加、删除、判断成员以及集合之间的交并差运算等。下面将按照类似前面介绍的风格,详细讲解 Set 的基本概念、常用命令以及使用场景和最佳实践。
6.4.1 Redis Set 的存储与特点
- 内部结构
- Redis 使用类似哈希表(Hash Table)来存储 Set 中的元素,每个元素都是一个独立的字符串,并且不允许重复。
- 如果尝试将相同的元素多次添加到同一个 Set,只有第一次会生效,后续重复添加不会改变集合状态,也不会报错。
- 数据特征
- 元素无序:Redis 不保证插入顺序,也不能通过索引下标来获取或遍历。
- 元素唯一:同一个元素在集合中只会出现一次。
- 高效查找:Set 中的元素查找、添加、删除通常是 O(1) 平均复杂度(哈希表机制)。
- 常用场景
- 存储好友列表、标签列表、不重复的元素集合;
- 取交集、并集、差集,做共同好友、共同兴趣、共同标签分析等;
- 去重功能(快速判断某元素是否已经存在);
- 随机获取、抽奖(
SRANDMEMBER
、SPOP
)等场景。
6.4.2 Set 的常见操作命令
6.4.2.1 添加、删除、判断成员
6.4.2.1.1 SADD
-
作用:向集合添加一个或多个元素,如果元素已存在则跳过,不影响集合。
-
语法:
SADD <key> <member1> [member2 ...]
-
返回值:实际被添加进集合的新元素数量。
-
示例:
SADD myset "apple" SADD myset "banana" "orange" # 如果 myset 之前不存在,会创建并插入元素。 # 如果 "apple" 已存在,再次插入不会重复。
6.4.2.1.2 SREM
-
作用:删除集合中指定的一个或多个元素。
-
语法:
SREM <key> <member1> [member2 ...]
-
返回值:被成功移除的元素数量。
-
示例:
SREM myset "banana" # 从 myset 移除 "banana"
6.4.2.1.3 SISMEMBER
-
作用:判断某个元素是否为集合成员。
-
语法:
SISMEMBER <key> <member>
-
返回值:1 表示是集合成员,0 表示不是或不存在。
-
示例:
SISMEMBER myset "apple" # 若 myset 包含 "apple" 则返回 1,否则返回 0
6.4.2.2 获取、查看和随机操作
6.4.2.2.1 SMEMBERS
-
作用:返回集合中的所有元素。
-
示例:
SMEMBERS myset # 比如返回 ["apple","orange","grape"]
-
注意:
- 如果集合元素非常多,一次性返回也会占用较多内存和带宽;
- 生产环境中若集合规模庞大,可以考虑使用
SSCAN
命令分批次扫描(类似于SCAN
)。
6.4.2.2.2 SCARD
-
作用:返回集合中的元素数量。
-
示例:
SCARD myset # 返回例如 3
6.4.2.2.3 SPOP
-
作用:随机移除并返回集合中的一个或多个元素(默认 1 个)。
-
语法:
SPOP <key> [count]
-
示例:
SPOP myset # 随机弹出一个元素,并将其从 myset 中移除。 SPOP myset 2 # 随机弹出 2 个元素,返回数组,并同时从集合删除它们。
-
应用场景:抽奖、随机发放奖品、随机推荐等。
6.4.2.2.4 SRANDMEMBER
-
作用:随机获取集合中的一个或多个元素,但不会将其从集合中删除(和
SPOP
区别在于是否删除)。 -
语法:
SRANDMEMBER <key> [count]
-
示例:
SRANDMEMBER myset # 随机返回一个元素,不会删除 SRANDMEMBER myset 2 # 随机返回 2 个元素(可能包含重复,Redis < 3.2 不同版本实现略有区别)
在 Redis 3.2+,如果 count 为正,则返回不重复的元素;如果 count 为负,则可能返回重复的元素。可查阅对应版本文档以确认。
6.4.2.3 集合运算(交集、并集、差集)
Redis 提供了三个重要的集合运算命令:SINTER、SUNION、SDIFF。配合这些命令,能快速做聚合分析,如“共同好友”、“共同兴趣标签”等场景。
6.4.2.3.1 SINTER / SINTERSTORE
-
SINTER:求一个或多个集合的交集。
SINTER <key1> [key2 ... keyN]
返回所有在这些集合中都存在的元素。
-
SINTERSTORE:将交集结果直接存储到另一个集合。
SINTERSTORE <destination> <key1> [key2 ... keyN]
-
示例:
# set1 = {"apple","banana","orange"} # set2 = {"banana","orange","grape"} SINTER set1 set2 # 返回 {"banana","orange"} SINTERSTORE set3 set1 set2 # set3 将变成 {"banana","orange"}
6.4.2.3.2 SUNION / SUNIONSTORE
-
SUNION:求并集,返回任意集合里出现过的所有元素(去重)。
SUNION <key1> [key2 ... keyN]
-
SUNIONSTORE:把并集结果存储到
destination
。 -
示例:
SUNION set1 set2 # 返回 {"apple","banana","orange","grape"} SUNIONSTORE set4 set1 set2 # set4 = {"apple","banana","orange","grape"}
6.4.2.3.3 SDIFF / SDIFFSTORE
-
SDIFF:求差集,返回在第一个集合里而不在其他集合中的元素。
SDIFF <key1> [key2 ... keyN]
-
SDIFFSTORE:将差集结果存储到
destination
。 -
示例:
SDIFF set1 set2 # 返回 {"apple"} (set1 - set2) SDIFFSTORE set5 set1 set2 # set5 = {"apple"}
6.4.2.4 其他常用或补充命令
- MOVE (Redis 6.2 以前版本仅对 key 生效,对 Set 结构可以使用 SPOP + SADD 或 SRANDMEMBER + SADD 的方式来“移动”元素到另一个集合)。
- RENAME (对整个 key 进行重命名,不仅是 Set 类型才有,不过仅限 key 级别操作,不是移动元素)。
- SSCAN (类似
SCAN
,可分批扫描大集合,语法:SSCAN key cursor [MATCH pattern] [COUNT count]
),在集合规模很大的情况下比一次性SMEMBERS
友好,避免阻塞。
6.4.3 使用场景
- 去重功能
- 将数据插入 Set 并检查返回值或
SISMEMBER
判断重复。 - 例如:用户签到(只需保存用户 ID,保证每个用户只签到一次)。
- 将数据插入 Set 并检查返回值或
- 社交场景
- 用户关注列表、好友列表都可以用 Set 表示;
- 求共同好友:
SINTER userA:friends userB:friends
。 - 计算关注量:
SCARD user:followers
,判断是否互相关注:SISMEMBER user:followers otherUserID
等。
- 标签或权限管理
- 每个用户的标签是一个 Set 或每个标签对应一个 Set。可通过集合运算来灵活地求交集或并集。
- 随机抽取、抽奖
SRANDMEMBER
随机选取;如需“抽完就移除”,可用SPOP
。
- 去除重复消息 / 数据过滤
- 在大量数据处理中,可把已处理过的标记存进 Set,若再次出现,表明是重复,跳过即可。
6.4.4 常见注意事项 & Best Practices
- Set 元素数量过多
SMEMBERS
会把所有元素一次性返回,可能导致内存和带宽的压力;- 建议使用
SSCAN
分批遍历;或在设计上拆分成多个小集合,避免单个 Set 过大。
- 大小限制
- 单个 Set 理论上可存放多达数亿个元素(只要内存足够);但操作如交集并集也会消耗相应资源,需评估性能和内存。
- 不要滥用 Set 做有序需求
- Set 是无序的,若需要按照分数、排名等有序排序,应该考虑使用 Sorted Set (ZSet)。
- 若需要按插入顺序遍历或常做 FIFO/LIFO,List 或 Stream 更适合。
- 集合运算开销
SINTER
,SUNION
,SDIFF
等运算在集合很大时也可能比较耗时,尤其当参与运算的集合规模都非常庞大时。- 若是在线查询,需要注意别在关键路径上做大规模集合运算,以免阻塞 Redis 主线程。可以在业务层缓存结果,或在后端服务分层处理。
- 过期策略
- Redis 不支持对 Set 中的单个元素设置过期,但可以对整个 key 设置过期时间(
EXPIRE key seconds
)。 - 若需要对其中的部分元素做时间控制,可能需要别的结构或自行管理。
- Redis 不支持对 Set 中的单个元素设置过期,但可以对整个 key 设置过期时间(
6.4.5 小结
Set 在 Redis 中是一个非常基础且强大的数据结构,最显著的特点是:
- 元素唯一、无序、快速判重;
- 提供交集、并集、差集等集合运算;
- 适合去重、好友列表、标签管理、抽奖等常见场景。
其核心命令可以简要归纳为:
- 添加 / 删除:
SADD
/SREM
- 查询:
SMEMBERS
(全量),SISMEMBER
(单一判断),SCARD
(数量)SSCAN
(分批扫描)
- 随机:
SPOP
(随机弹出并删除),SRANDMEMBER
(随机获取不删除)
- 集合运算:
SINTER
,SUNION
,SDIFF
以及对应的STORE
版本
- 其他:
SMOVE
(已弃用命令,Redis 版本里没有 SMOVE 可以用 SPOP+SADD 替代),LTRIM
不适用于 Set,SSCAN
用于分批遍历等。
只要掌握这些命令和组合应用,就能应对大部分业务中需要“去重、随机、集合运算”的需求。在实际开发中,还要结合 Redis 的内存管理、过期策略及安全控制,来达成高性能又可靠的 Set 使用方案。
6.5 有序集合(ZSet)
在 Redis 中,有序集合(Sorted Set)是一种在集合基础上,为每个成员额外关联一个分数(score)的数据结构,常被缩写为 ZSet。它的特点是:元素不允许重复,但可以按照分数进行排序。Redis 提供了一系列针对有序集合的操作命令,方便我们在排行榜、排名查询、区间筛选、限时排序等场景中使用。
6.5.1 ZSet 的存储与特点
- 内部结构
- Redis 使用一种结合了 跳表(Skiplist) + 哈希表(Hashtable) 的结构来存储 ZSet。
- 跳表可以让我们在 O(log N) 的复杂度下进行有序操作(如范围查找、排序等),哈希表负责快速定位元素。
- 数据特征
- 元素唯一:和 Set 一样,每个 ZSet 里元素(member)是唯一的。
- 分数(score)可以重复:不同的 member 可以拥有相同的分数,但是 member 自身不能重复。
- 按分数排序:Redis 可以根据 score 值对元素进行从小到大的排序,也可以支持倒序操作。
- 常用场景
- 排行榜:比如按积分、热度、时间戳等排序;
- 时间序列:score 代表时间戳,按时间顺序进行查询或截取;
- 带权重的列表:以 score 作为优先级或权重值,随时能快速获取指定排名或分数区间的元素。
6.5.2 有序集合 ZSet 的常见命令
6.5.2.1 添加、更新、删除元素
6.5.2.1.1 ZADD
-
作用:向有序集合中添加一个或多个
member
,并设置它们的分数score
。若 member 已存在则更新其 score。 -
语法:
ZADD <key> [NX|XX] [CH] [INCR] <score1> <member1> [<score2> <member2> ...]
-
常用选项:
NX
:仅当 member 不存在时才添加;XX
:仅当 member 已存在时才更新;CH
:返回被修改的成员数量(包括 score 发生变动的情况);INCR
:对给定 member 的分数执行加法操作,而不是直接设置分数。
-
示例:
ZADD leaderboard 100 "playerA" ZADD leaderboard 200 "playerB" 150 "playerC" # 如果 playerA 已存在,则其 score 会被更新为 100
6.5.2.1.2 ZREM
-
作用:从有序集合中删除指定元素,支持删除多个。
-
语法:
ZREM <key> <member1> [member2 ...]
-
示例:
ZREM leaderboard "playerC" # 删除 "playerC" 这个 member
6.5.2.1.3 ZINCRBY
-
作用:对指定 member 的分数进行增量操作(加上给定的数值)。
-
语法:
ZINCRBY <key> <increment> <member>
-
示例:
ZINCRBY leaderboard 50 "playerA" # playerA 的分数在原来的基础上 +50
-
注意:如果 member 不存在,会先以 score=0 创建该 member,再加上 increment。
6.5.2.2 按序查询、获取排名
6.5.2.2.1 ZRANGE / ZREVRANGE
-
作用:按 score 升序(
ZRANGE
)或降序(ZREVRANGE
)获取指定索引区间内的元素。 -
语法:
ZRANGE <key> <start> <stop> [WITHSCORES] ZREVRANGE <key> <start> <stop> [WITHSCORES]
-
示例:
ZRANGE leaderboard 0 -1 # 返回所有成员,按分数升序 ZRANGE leaderboard 0 2 WITHSCORES # 返回前三名,同时显示分数 ZREVRANGE leaderboard 0 2 WITHSCORES # 返回分数最高的三名
-
索引区间:
0
表示第一个元素(最小或最大的视升序/降序而定),-1
表示最后一个元素;- 依次类推,
start
、stop
可以是正负索引。
6.5.2.2.2 ZRANGEBYSCORE / ZREVRANGEBYSCORE
-
作用:按 score 值进行范围查询,返回分数在
[min, max]
(或[max, min]
)区间内的 member 列表。 -
语法:
ZRANGEBYSCORE <key> <min> <max> [WITHSCORES] [LIMIT offset count] ZREVRANGEBYSCORE <key> <max> <min> [WITHSCORES] [LIMIT offset count]
-
说明:
ZRANGEBYSCORE leaderboard 100 200
:获取 score 处于 100~200 范围内的所有成员,按升序。ZREVRANGEBYSCORE leaderboard 200 100
:同样的分数区间,但按分数从高到低排序。LIMIT offset count
:可对结果做分页或截取。+inf
和-inf
:可用于表示无穷大和无穷小;(100
表示大于 100 而不是大于等于(带括号表示排除)。
6.5.2.2.3 ZRANGEBYLEX / ZRANGEBYDICT(Redis 6.2+ 支持),与按 Member 字典序相关
- 在某些场景下可以按 Member 的字典序,而非 score 进行范围查询;这是比较少用的操作,了解即可。
6.5.2.2.4 ZRANK / ZREVRANK
-
作用:获取某个 member 在有序集合中的排名(下标)。
-
语法:
ZRANK <key> <member> ZREVRANK <key> <member>
-
返回值:
- 若 member 存在,返回其以 0 开始的排名;
- 若不存在,返回
nil
。
-
举例:
ZRANK leaderboard "playerA" # 返回 playerA 在升序下的名次(0 表示 score 最小的那个,数值越大表示分数更高的排名) ZREVRANK leaderboard "playerA" # 返回 playerA 在降序下的名次(0 表示分数最高的)
6.5.2.2.5 ZCARD / ZCOUNT
-
ZCARD:返回有序集合的基数(元素总数)。
ZCARD <key>
-
ZCOUNT:统计指定 score 区间内的元素数量。
ZCOUNT <key> <min> <max>
例如:
ZCOUNT leaderboard 100 200
返回分数在 [100,200] 的元素数。
6.5.2.3 删除和范围操作
6.5.2.3.1 ZREM、ZREM_RANGE_BY_SCORE、ZREM_RANGE_BY_RANK
-
ZREM:前面提过,按 member 删除指定元素。
-
ZREMRANGEBYSCORE:删除score在给定区间内的所有元素。
ZREMRANGEBYSCORE <key> <min> <max> # 如 ZREMRANGEBYSCORE leaderboard 0 100 -> 删除 score <= 100 的所有元素
-
ZREMRANGEBYRANK:按排名下标区间删除元素。
ZREMRANGEBYRANK <key> <start> <stop> # 如 ZREMRANGEBYRANK leaderboard 0 9 -> 删除排在前 10 名的元素
6.5.2.3.2 ZINTERSTORE / ZUNIONSTORE / ZDIFFSTORE (Redis 6.2+)
和普通 Set 类似,Redis 也提供了对 ZSet 的交集、并集、差集运算,但是会对 score 做相应的合并或计算。常见的是 ZINTERSTORE
和 ZUNIONSTORE
。
- ZINTERSTORE:计算给定的一个或多个有序集合的交集,将结果存储到新集合中,并根据每个元素在各集合中的 score 值执行相应的加权或求和。
- ZUNIONSTORE:并集操作,score 同样可以加权或求和。
示例:
ZADD zset1 1 "a" 2 "b"
ZADD zset2 2 "a" 3 "c"
ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3
# 这里 2 表示有 2 个集合,WEIGHTS 2 3 分别表示 zset1 的 score 乘以2, zset2 的 score 乘以3
# "a" 在 zset1 score=1, zset2 score=2
# 计算交集则 "a" 存在于两个集合中,score = 1*2 + 2*3 = 2 + 6 = 8
# "b" 只在 zset1, "c" 只在 zset2,故它们不在交集里
# out = {"a":8}
注意:Redis 7.0+ 也引入了
ZDIFFSTORE
(差集),不过在实际中用得相对少一些。
6.5.3 常见使用场景
- 排行榜
- 例如游戏积分榜、电商热卖榜、点赞数排行等;
- 可以通过
ZADD
不断更新用户的分数,然后使用ZREVRANGE
或ZREVRANK
获取某人排行或前 N 名; ZINCRBY
用于积分累加,ZRANK
/ZREVRANK
用于查询当前名次。
- 实时排序
- 每次用户操作(点赞、查看、消费等)都可以更新其相关分数;
- 通过按分数的正序或倒序可实现某些实时刷新排名的功能,比如直播间送礼排行榜、产品热度排名等。
- 延时队列 / 时间调度
- 把 future task 存成
(score=执行时间, member=任务ID)
,利用ZRANGEBYSCORE
查询到期任务,进行处理后删除; - 可以替代一些简单的定时任务逻辑。
- 把 future task 存成
- 时间序列
- 以时间戳作为 score,member 是事件或数据 ID;
- 使用
ZRANGEBYSCORE
或ZREVRANGEBYSCORE
在一定时间范围内获取数据。
6.5.4 注意事项 & Best Practices
- score 的精度
- Redis score 本质是一个双精度浮点数(double),尽量避免过于依赖小数精度(有时可能出现浮点误差)。
- 如果分数是整数,推荐直接用整数(如 123456)或用一个长整型来表达时间戳/分值以减小误差。
- ZADD 频繁更新
- 更新分数只需再次执行
ZADD <key> <score> <member>
,若 member 存在则覆盖;或ZINCRBY
增量更新。 - 频繁更新大规模 ZSet 时要注意是否会带来内存或操作耗时,毕竟跳表需要维护顺序。
- 更新分数只需再次执行
- 排行榜前 N vs. 全量遍历
ZRANGE key 0 9
、ZREVRANGE key 0 9
:获取前 10 名或后 10 名非常高效;- 如果要获取所有成员(百万级)并排序,返回时就会产生大流量,占用 Redis 线程,对性能影响较大。
- 可以考虑分页或分段查询:
ZREVRANGE key start end
,或者用ZRANGEBYSCORE + LIMIT
进行分页,不过也要注意性能。
- 删除操作
ZREM
只针对指定 member;- 对于区间删除(score 范围或排名范围)可使用
ZREMRANGEBYSCORE
或ZREMRANGEBYRANK
。 - 这些批量删除操作会在一定程度上阻塞 Redis 主线程,规模很大时也需注意分拆或异步管理。
- 内存占用
- ZSet 同时维持哈希表 + 跳表结构,会比普通 Set 消耗更多内存;
- 大量 ZSet 时需考虑内存大小、持久化和主从复制对性能的影响。
- 过期策略
- Redis 不支持对 ZSet 的单个 member设置过期,只能对整个 key 设置过期;
- 如果需要对某些分数或元素做定期清理,可以定时执行
ZREMRANGEBYSCORE
或业务逻辑判断后删除。
6.5.5 小结
有序集合(ZSet)是 Redis 中一个非常重要的高级数据结构,结合了“集合去重”与“分数排序”的优势,能在很多排行榜、调度、排序场景下大显身手。
其核心命令可简要概括为:
- 增、删、改、查
ZADD
:添加/更新元素ZREM
:删除指定元素ZINCRBY
:分数自增ZRANK
/ZREVRANK
:查看排名(索引位置)ZCARD
:ZSet 大小
- 获取数据
ZRANGE
/ZREVRANGE
:按索引区间获取排序结果ZRANGEBYSCORE
/ZREVRANGEBYSCORE
:按分数区间获取ZCOUNT
:分数区间计数
- 范围删除
ZREMRANGEBYSCORE
、ZREMRANGEBYRANK
- 交并集操作
ZINTERSTORE
/ZUNIONSTORE
:对多个 ZSet 做交集 / 并集,score 可以加权合并
掌握了这些命令及应用思路,就能灵活地在Redis中构建实时排行榜、延时队列、时间序列、计分系统等功能。配合其他类型(String、Hash、Set 等)的混合使用,可以构建出更丰富、更高效的 Redis 数据模型来满足不同业务需求。
6.7 时间复杂度
在 Redis 中(以及更多计算机科学领域),我们常用 Big-O 表示法(如 O(1)、O(log N)、O(N) 等)来衡量算法或操作的时间复杂度,它代表了输入规模增大时,操作耗时的增长趋势。下面我们分几个层面来理解:
6.7.1 什么是 O(1) 和 O(log N)?
- O(1)(常数时间):
- 表示无论数据规模 N N N 多大,操作所需的时间都近似于一个常数,与 N N N 无关。
- 例如,用哈希表(Hash Table)查询一个元素的平均时间复杂度是 O(1):查找过程可在常数时间内完成。
- 当然这是平均情况。哈希表在最坏情况下(冲突严重)可能退化到 O(N);但在良好散列和低冲突的场景下可视为 O(1)。
- O(log N)(对数时间):
- 表示操作所需时间随数据规模 N N N 的增加按照对数规律增长。对数函数增长速度比线性函数慢得多,所以当 N N N 很大时,O(log N) 算法仍然比较高效。
- 例如,用平衡二叉搜索树、跳表(Skiplist)、B+树等结构进行插入、查找、删除,通常都能在 O(log N) 时间内完成。
- Redis 的**有序集合(ZSet)**内部默认用“跳表 + 哈希表”来存储:
- 跳表部分提供按分数排序、区间查询等有序操作的能力(O(log N));
- 哈希表部分用来快速定位元素是否存在(O(1) 平均情况)。
6.7.2 为何跳表的有序操作是 O(log N)?
跳表(Skiplist) 是一种随机化的数据结构,结构上类似多层链表,每一层都跳过一些节点,以加速查找、插入和删除。
- 在理想状态下,每向上走一层,节点数量就减少一半;
- 因此要找到某个元素或一个分数区间时,只需从高层往低层“逐渐逼近”,访问的节点数大约是 log 2 N \log_2{N} log2N量级;
- 整体上插入、删除、查找等操作都能保持在 O(log N) 水平。
举个形象的例子:
- 在一个排好序的很长的链表中,如果只能一格一格往后找,那么查找就是 O(N)。
- 但如果你先在“顶层链表”里,隔几个节点就有一个索引,可以快速跳过若干节点;再往下一层,更密集的索引帮助你再定位……如此层层逼近目标,就能极大缩短“跳”的次数,这就是为什么能够得到 O(log N) 的时间复杂度。
6.7.3 为何哈希表的操作是 O(1)(平均情况)?
哈希表(Hash Table) 通过哈希函数把键(Key)映射到一个桶(bucket)或槽(slot)里,理论上可以直接通过哈希函数一次定位到存储位置。
- 在冲突不多、散列均匀的情况下,查找、插入、删除操作只需要常数步(O(1))就能完成;
- 这是因为无论有多少数据(N 很大),哈希函数的计算和一次到位的寻址,都不太依赖 N 的规模;
- 不过,如果哈希冲突非常多或散列不均,最坏情况可能要在某个桶里的链表挨个遍历(O(N)),但平均来说,好的哈希设计能让大部分操作维持在 O(1)。
6.7.4 Redis 中具体体现
- ZSet 的跳表部分:
- 当我们需要“根据分数范围取某些元素”或“得到某个分数在有序集合中的排名”时,就需要依赖有序结构(跳表)来进行区间遍历或排名查找,复杂度约为 O(log N)(有序查找、插入、删除)或 O(log N + M)(找出 M 个结果)。
- 例如命令
ZRANGEBYSCORE
、ZREVRANGEBYSCORE
就是利用跳表快速定位到指定分数区间的起始位置,然后顺序扫描出结果。
- ZSet 的哈希表部分:
- 当我们执行 “给某个元素设置分数” 或 “查一个元素在不在这个 ZSet 里” 时,可先通过哈希表在 O(1) 平均时间内找到该元素;如果更新分数,则跳到跳表对应位置更新节点;
- 这样就兼顾了快速定位(哈希表)和有序操作(跳表)。
- Hash(哈希)类型、Set、HashMap 等其他结构:
- 也都基于哈希表,插入、查询、删除的平均复杂度是 O(1)。
- List 或 Stream:
- 大部分情况下是 O(1) 地在头尾插入或弹出(LPUSH、LPOP、RPUSH、RPOP),但随机访问或索引查找时就需要 O(N)(链表或类似结构需遍历)。
6.7.5 如何理解这些复杂度?
- Big-O 并非绝对耗时:它只描述增长趋势,并不意味着一旦是 O(log N) 就一定比 O(1) 慢,或者 O(N) 就一定比 O(log N) 慢;在小规模数据时,常数和常数因子也会很重要。
- 平均 vs. 最坏:哈希表的 O(1) 通常指平均复杂度;跳表/树型结构的 O(log N) 指大多数情况下都会如此,但在极端情况下也可能退化。
- 选择合适的数据结构:如果你需要有序查询、范围操作、排名等功能,就需要跳表/树这样支持排序的结构(ZSet)。如果只是key -> value 快速查询(比如 Set/Hash),哈希表就足够好,简单粗暴且平均 O(1)。
6.7.6 小结
- O(1):操作耗时不随着数据规模变化而显著增长,是平均情况下哈希表操作最有吸引力的特征。
- O(log N):跳表、平衡树等结构能实现高效的有序操作。随着 NN 增大,耗时按照 级别增长,比 O(N) 好很多,但比 O(1) 大。
- Redis ZSet 内部“跳表 + 哈希表”的组合可以理解为:
- 哈希表提供快速定位(O(1) 平均)
- 跳表提供有序操作(O(log N))
- 二者互补,使得 ZSet 在“既需要有序,又要能快速查某元素”的场景中表现优异。
这些就是常见的时间复杂度概念和在 Redis 内部的具体落地。当在应用层设计数据结构或分析 Redis 性能时,理解这些复杂度有助于我们选择合适的操作并提前预估可能的开销。
6.7 消息队列
在消息队列或者消息传递领域,一般会提到 两种常见的消息模式:
- 生产者-消费者(Producer-Consumer)
- 发布-订阅(Publisher-Subscriber,Pub/Sub)
6.7.1 生产者-消费者(Producer-Consumer)
6.7.1.1 模式概念
- 生产者(Producer):负责发送消息或任务到队列(Queue)中。
- 消费者(Consumer):从队列中取出消息或任务并进行处理。
常见的形态是“消息队列”:生产者把消息投递到队列,消费者从队列里取出并消费,最终队列中存储的是待处理的消息。
6.7.1.2 工作流程
- 生产者将消息发送到消息队列;
- 消息队列负责临时存储这些消息,充当缓冲区;
- 消费者从队列中轮询或阻塞获取消息(通常是 FIFO,先进先出 first in,first out),然后执行处理;
- 处理完毕后,消息即被标记为消费完成或从队列移除。
6.7.1.3 主要特点
- 解耦:生产者和消费者可以异步运行,生产者发送后就可以继续处理其他任务,不用等待消费者处理完成。
- 缓冲:消息队列在生产者和消费者之间提供了一个缓冲池,能应对突发的高并发或流量不均衡,避免直接把压力传递给消费者。
- 可靠性(可选):大多数消息队列中间件(RabbitMQ、Kafka、RocketMQ 等)会提供持久化、确认(ACK)机制来保证消息不丢失。
- 竞争消费:通常一个队列可被多个消费者并行消费,每条消息只会被其中一个消费者处理一次,适合“任务分发”或“分布式工作池”模式。
6.7.1.4 典型应用场景
- 异步任务处理:如发送短信、邮件、图像处理,把耗时任务扔进队列中,再由工作进程去处理。
- 削峰填谷:电商大促时,订单处理可先进入队列以防止瞬时并发过高;消费者慢慢消化队列里积压的订单。
- 分布式系统间解耦:生产者和消费者可以各自升级或故障重启,而不影响对方的逻辑。
6.7.2 发布-订阅(Pub/Sub, Publisher-Subscriber)
6.7.2.1 模式概念
- 发布者(Publisher):向某个主题或频道(Topic/Channel)发布消息。
- 订阅者(Subscriber):对指定主题或频道进行订阅,一旦有新的消息发布,就会被推送或接收到。
可以把它理解为“广播”式:所有订阅了某个频道的订阅者,在发布者推送消息后,都会各自收到一份消息。
6.7.2.2 工作流程
- 订阅者先声明对某个主题/频道感兴趣,并订阅之;
- 发布者向该主题/频道发布消息;
- 消息中间件负责将消息同时分发给所有已订阅该主题/频道的消费者;
- 每个订阅者都能够各自收到该条消息,并进行自己的处理。
6.7.2.3 主要特点
- 一对多:一个发布者发布的一条消息可以同时被多个订阅者接收,这与“生产者-消费者”中单条消息只被一个消费者处理截然不同。
- 实时推送:当发布者有新消息时,订阅者能立即收到;
- 不保证存储(取决于实现):如果某个订阅者离线或没有及时消费,消息可能就错过了(除非使用带持久化/主题回溯的消息系统,例如 Kafka、Redis Streams 也有一定存储能力,但传统“Redis Pub/Sub”则不存储历史消息)。
- 松耦合:发布者不需要知道订阅者是谁,订阅者也不需要知道具体是哪一个发布者发的消息。它们只通过主题来“对接”。
6.7.2.4 典型应用场景
- 广播通知:系统中某个事件需要同时通知多个服务或模块。
- 实时推送:聊天房间、直播间弹幕、股票行情等,需要向所有在线订阅者推送最新数据。
- 异步事件总线:在微服务架构中,一些服务可能只需要监听特定事件来触发自己的逻辑,发布者只管发布事件,多个订阅者各自执行不同流程。
6.7.3 对比与总结
维度 | 生产者-消费者(消息队列) | 发布-订阅(Pub/Sub) |
---|---|---|
消息传递关系 | 一对一:队列中的一条消息只会被一个消费者处理 | 一对多:一个消息可同时被多个订阅者接收 |
消息存储 | 通常会存储在队列里,若消费者未及时消费,消息也不会丢 | 取决于系统实现,传统 Pub/Sub 通常不存储历史消息 |
消费模式 | 消费者竞争消费同一个队列;可实现分布式工作池、并行处理 | 所有订阅该主题的订阅者都能各自收到一份消息,并行处理 |
适用场景 | 异步处理、队列缓冲、削峰填谷、解耦分布式任务 | 实时广播通知、事件订阅、多客户端同时接收相同消息 |
消息确认 & 重试 | 通常具备ACK/重试机制,确保消息被正确处理 | 传统简单 Pub/Sub 一般无重试/确认,消息分发后即丢 |
核心关注点 | 可靠消费,队列实现排队、缓冲;保证单条消息只被处理一次 | 消息广播,多消费者同时收到,不保证是否被落地存储 |
6.7.4 延伸与选择
- 生产者-消费者主要关注:
- 消息排队、可靠处理、可控的并发消费、处理完消息后就出队。
- 适用于工作队列、任务处理、订单处理、分布式流水线等。
- 发布-订阅主要关注:
- 分发:一次消息,多个订阅者同时收到;
- 广播或多对多模式;
- 适用于实时性推送、广播通知、事件总线等。
- 组合使用:
- 有时一个系统中既需要排队处理,又需要广播通知,则会在不同子模块选择不同模式,或者使用支持多种模式的消息中间件(如 Kafka 既支持“订阅分区”,也能做分布式队列;RabbitMQ 可用交换器+队列配合实现多播等)。
- 不同系统实现
- 如 RabbitMQ、ActiveMQ、RocketMQ 更偏向队列模型(生产者-消费者),但也能通过不同的 exchange 类型(主题交换、fanout 等)来实现类似发布-订阅;
- Kafka 虽然更多用于订阅(消费者组对 Topic 的订阅),但本质也能做队列(同一个分区内仍是单消费者消费)。
- Redis 的 Pub/Sub 属于较简单的纯广播,不存储历史消息,而 Redis Streams 则提供了可做队列或订阅的能力,也支持 Consumer Group。
6.7.5 小结
- 生产者-消费者(消息队列模型):
- 核心诉求:每条消息由唯一消费者处理,注重可靠消费、解耦、延迟容忍,可以做异步处理、削峰填谷。
- 发布-订阅(Pub/Sub 模型):
- 核心诉求:一次消息可同时被多个订阅者接收,注重实时广播、事件分发;不一定有存储与确认机制。
两种模式各有适用场景,在实际项目中往往结合使用,或者在同一个消息系统内使用不同的交换策略实现不同效果。选择哪种模式取决于消息要给多少人接收、是否需要队列缓冲、是否要落盘与确认、是否需要多对多的实时通信等业务需求。
6.7.6 Redis 实现消息队列
在许多场景下,Redis 经常被用作轻量级的消息队列(Message Queue),实现简单的生产者-消费者模型或发布订阅功能。根据 Redis 版本和需求的不同,常见的实现方式主要有:
- 使用 List(列表)+ 阻塞弹出:最传统的方式,利用
LPUSH
/RPUSH
与BLPOP
/BRPOP
组合,构建简单队列。 - 使用 Redis Pub/Sub:更适合实时广播式的消息分发,但消息不能持久化。
- 使用 Redis Streams(5.0+):官方推荐的现代化消息流结构,支持持久化、有序存储、消费组、ACK 等功能,更适合复杂的消息队列场景。
下面依次介绍各方案的特点与适用场景,并总结常见注意事项。
6.7.6.1 使用 List 构建简单队列
6.7.6.1.1 基本原理
- 生产者:在列表尾部或头部插入消息
- 常用
LPUSH queue "message"
或RPUSH queue "message"
- 常用
- 消费者:使用 阻塞命令
BLPOP
或BRPOP
来弹出消息- 当队列为空时,消费者阻塞等待,直到超时(若指定)或有新消息入队
这样就构成了一个FIFO(先进先出)或类似队列的模型。例如:
# 生产者往右端插入
RPUSH task_queue "task1"
RPUSH task_queue "task2"
# 消费者阻塞等待
BLPOP task_queue 0
# 如果为空,则阻塞等待;有数据就立刻返回
- 如果需要严格的 FIFO,可约定同一端入队,另一端出队,比如:
- 生产者使用
RPUSH
- 消费者使用
BLPOP
- 数据会从右端进,左端出,实现最常见的队列顺序。
- 生产者使用
6.7.6.1.2 特点与适用场景
- 优点:
- 实现简单,命令直观;
- 无需额外插件或复杂配置,适合小规模队列或临时需求;
BLPOP
/BRPOP
是阻塞模式,避免频繁轮询,实时性好。
- 缺点:
- 无内置消息确认(ACK)、重试、消费组等机制;如果消费者在处理后崩溃,消息就丢失了(除非自行实现“处理后写回”流程)。
- 不便于多消费者并行处理同一个队列里的同一条消息,需要自己设计逻辑避免重复消费。
- 没有持久化机制时,Redis 重启后现有队列内容是否能够完整恢复,需结合 AOF/RDB 机制进行持久化;但如果严格消息可靠性要求很高,Redis 并不是专业的“持久化消息队列”解决方案(可考虑 RabbitMQ、Kafka)。
这种方式非常适合简单任务队列或小规模异步处理:
- 例如把用户的某些操作异步处理掉,不要求强一致或复杂消息确认;
- 如果对消息丢失容忍度高或可以接受自己做补偿机制。
6.7.6.2 使用 Redis Pub/Sub
6.7.6.2.1 基本原理
- Redis 的 Publish/Subscribe功能实现了发布和订阅模型:
- 发布者通过
PUBLISH channel message
向指定频道发送消息; - 订阅者使用
SUBSCRIBE channel
监听该频道并实时接收消息;
- 发布者通过
- 这是一种广播式或一对多的消息分发:只要多个订阅者都订阅了同一个 channel,它们都会收到发布者的消息副本。
6.7.6.2.2 特点与应用场景
- 优点:
- 实时性好,消息一发送,所有订阅者都能立即收到;
- 适合推送或通知类场景,比如聊天室、服务器实时事件广播等。
- 缺点:
- 无持久化:消息不是存储在 Redis 中,而是一经发布即向所有订阅者推送,如果当时订阅者掉线或来不及处理,就会丢失。
- 无法回放历史消息,也没有内置的队列特性(排队、确认、重试)等。
因此,Redis Pub/Sub 不大适合“传统队列”需求,更像实时广播,用来做直播聊天室、在线游戏通知、事件总线等。在需要可靠消费的场景中则不合适。
6.7.6.3 使用 Redis Streams(5.0+)
6.7.6.3.1 Streams 简介
- Redis Streams 是 Redis 5.0 引入的新数据结构,专为消息流(log stream)设计。
- 它既可以看作有序存储的消息队列,也可像 Kafka 一样保留历史消息,客户端可以从指定位置开始读取、重复读取并标记消费进度。
6.7.6.3.2 关键概念
- Stream:按时间或插入顺序将消息(Entry)排成有序队列,每条消息带有一个 ID(类似时间戳+序列号)。
- XADD:生产者向 Stream 中添加消息(
XADD mystream * field1 value1 field2 value2 ...
)。 - XREAD / XREADGROUP:消费者读取消息,可指定从哪个 ID 开始读,实现增量消费。
- Consumer Group(消费者组):允许多个消费者分组消费同一个 Stream 中的数据;Redis 会自动做分配,并支持消息ACK和未确认消息管理,适合多消费者并行且保证消息不会重复消费或丢失太多。
- XACK / XPENDING:通过ACK机制,只有消费者成功处理后才算消息被确认,从而减少消息丢失。未确认的消息可以查看和重新分配。
6.7.6.3.3 Streams 使用示例(简化)
# 生产者
XADD mystream * sensor-id 1 temperature 30.2
# 消费者(若非消费者组)
XREAD COUNT 10 STREAMS mystream 0
# 读取 mystream 中的全部消息(从 id=0 开始)
# 创建消费者组
XGROUP CREATE mystream mygroup $ MKSTREAM
# 消费者在该组内读取
XREADGROUP GROUP mygroup consumer1 COUNT 10 STREAMS mystream >
# 读取从上次读取位置以后的新消息
# 消费后,手动确认
XACK mystream mygroup <message-id1> <message-id2> ...
# 标记这些消息已成功处理
6.7.6.3.4 优势与特点
- 可靠性:通过消费者组和 ACK 机制,避免消费者崩溃导致消息永久丢失。
- 多消费者并发:同一个消费者组可以有多个消费者竞争消费消息,Redis 会自动分配未被其它消费者领取的消息给它们。
- 消息回溯:默认消息不会自动丢弃,可以根据需要保留一定长度或时间周期,以便后续读重或做故障回放。也可通过
MAXLEN
配置限制流长度。
6.7.6.3.5 适用场景
- 分布式系统中需要可靠消息队列:既要并发消费,又要防止某台消费者挂了导致消息丢失;
- 需要历史消息回溯(至少在一定范围内),比如日志、事件存档等;
- 相对大规模、严谨的异步消息处理,比 List 方案更“专业”,且支持高级特性。
6.7.6.4 选择哪种方式?
- 最简单:List + 阻塞弹出
- 适合轻量级的单一队列,不需要复杂确认或重复消费管理;
- 代码简单易上手,如果消息丢了问题不大(或能用自定义补偿),就行。
- Pub/Sub
- 适合实时推送或一对多通知场景,不做持久化,消息即时投递后即丢弃;
- 不适合需要存储与可靠消费的排队场景。
- Redis Streams
- 适合对消息的可靠性、持久性、多消费者管理有一定要求的场景,提供近似 MQ 的能力;
- 结构更复杂,需要掌握
XREADGROUP
,XACK
,XPENDING
,XCLAIM
等命令; - 如果需要专业、超大规模的队列并发(数十万~百万 QPS 级别)或高级分布式事务,可考虑专业消息系统(Kafka、RabbitMQ、RocketMQ 等);但 Redis Streams 对于中等规模的可靠队列、实时处理依然非常合适。
6.7.6.5 常见注意事项
- Redis 单线程特性
- 所有命令都在单线程内执行,若消息量巨大、消费者或生产者并发极高,可能造成阻塞。
- Redis 6.0 引入多 I/O 线程,但命令执行依然是单线程,需根据实际场景评估性能。
- 持久化
- 如果你希望消息在 Redis 重启后保留,需要启用 RDB / AOF 持久化,但也得考虑性能开销与数据一致性。
- 如果需要100%不丢消息,需要确保 Redis 持久化策略和硬件环境都可靠。
- 内存限制
- Redis 主要在内存中操作,如果消息积压量非常大,就可能导致内存压力,或大量写入也影响性能。可开启 eviction 策略,但那会导致可能的消息丢失。
- 消费端处理失败重试
- 简单 List 模型下,若处理失败,需要手动将消息再 push 回队列,或记录日志后再做补偿。
- 使用 Streams + 消费者组可以重新分配未确认的消息(
XPENDING
,XCLAIM
),从而实现一定程度的重试、死信队列等机制。
- 吞吐 vs. 功能
- Redis 消息队列方式更倾向于“轻量、高速、短期缓存/队列”场景;不提供像专业 MQ 那样完善的路由、扩展、跨节点分布功能。
- 如果需求简单、无需过度扩展,Redis 够用且非常快;若是大规模企业级 MQ 场景或需要严格事务和更灵活的路由策略,可考虑 RabbitMQ、Kafka、RocketMQ 等。
6.7.6.6 小结
Redis 作为内存数据库,本身并不是专业的消息中间件,但它速度快、命令简单,对很多轻量级消息队列或实时通知场景来说很实用:
- List +
BLPOP
/BRPOP
:最经典的简易队列方案。适合单队列、简单生产-消费、对可靠性无特殊要求的场景。 - Pub/Sub:适合实时广播的一对多订阅,不保留历史消息,也无确认机制。
- Redis Streams:Redis 5.0+ 官方提供的“日志流”数据结构,支持有序存储、消费者组、ACK 确认、回溯消息等,能满足中等规模、较高可靠性要求的消息队列场景。
根据业务需求,选择合适的模式和实现方式,才能在性能与可靠性之间找到平衡。对简单场景,List 队列最易上手;若需要相对专业的队列功能又想依赖 Redis生态,则 Streams 是更好的选择;若需要高可靠性和大规模分布式集群,可进一步研究专业MQ或结合 Redis 与其他系统。