当前位置: 首页 > article >正文

Redis的优势/Redis八股01

二、Redis的优势

2.1Redis为什么这么快

2.1.1 单线程架构避免线程竞争和锁的困扰

单线程模型是 Redis 高性能的核心设计之一。具体优势包括:

  • 避免线程切换开销:多线程环境下,线程切换需要保存和恢复上下文,这会带来显著的性能开销。Redis 通过单线程处理所有请求,消除了这种开销,从而提高了执行效率。
  • 消除锁机制:在多线程或多进程模型中,为了保证数据一致性,通常需要使用锁(如互斥锁、读写锁等)。锁的获取和释放不仅增加了额外的操作,还可能导致线程阻塞和上下文切换,降低性能。Redis 的单线程架构天然避免了这些问题,因为所有命令都是按顺序执行的,不存在并发访问同一数据的情况。
  • 简化编程模型:单线程使得编程模型更加简单,开发者无需担心并发问题,如竞态条件和死锁等。这不仅减少了开发复杂度,还降低了出错的可能性。
  • 高效的事件循环:Redis 使用基于事件驱动的非阻塞 I/O 模型(如 epollkqueue 等),能够高效地处理大量并发连接。这种模型在单线程中运行时,能够充分利用 CPU 资源,实现高吞吐量和低延迟。
2.1.2 优秀的数据结构设计

Redis 提供了一系列高度优化的数据结构,每种数据结构都针对特定的使用场景进行了精心设计,以确保操作的高效性:

  • 紧凑的内存表示:Redis 使用多种编码方式(如字符串编码、压缩列表、整数编码等)来存储不同类型的数据。这些优化的存储格式减少了内存占用,提高了数据访问速度。
  • 高效的操作算法:Redis 对其数据结构(如哈希表、跳表、双向链表等)进行了深度优化,确保常见操作(如插入、删除、查找等)在时间复杂度和空间复杂度上都表现优异。例如,有序集合(Sorted Set)使用跳表实现,支持快速的范围查询和排名操作。
  • 内存管理优化:Redis 使用高效的内存分配器(如 jemalloc)来管理内存,减少内存碎片,提高内存分配和回收的效率。此外,Redis 还支持内存压缩和数据淘汰策略,进一步优化内存使用。
  • 轻量级的协议:Redis 的协议(RESP,Redis Serialization Protocol)设计简单且高效,能够快速解析和序列化数据,减少了网络传输和处理的开销。
2.1.3. 内存存储

内存存储是 Redis 高性能的基础,具体体现在以下几个方面:

  • 极低的访问延迟:内存的读写速度远远超过磁盘(包括 SSD)。内存访问的延迟通常在纳秒级,而磁盘访问的延迟在毫秒级。将所有数据存储在内存中,使得 Redis 能够实现超低的响应时间,适用于对速度要求极高的应用场景。
  • 高吞吐量:由于内存带宽的高效利用,Redis 能够在单位时间内处理大量的请求,支持高并发的访问需求。这使得 Redis 成为缓存、会话存储、实时分析等场景的理想选择。
  • 持久化选项:虽然 Redis 主要依赖内存,但它提供了多种持久化机制(如 RDB 快照和 AOF 日志),以确保数据的持久性。这些持久化操作通常是异步进行的,不会阻塞主线程,从而在保持高性能的同时保证数据安全。
  • 数据局部性:内存中的数据具有良好的局部性,CPU 缓存能够更高效地访问数据,进一步提升了数据处理速度。

2.2缓存与数据库一致性

1. 先更新缓存,再更新数据库
描述
  • 操作顺序:先更新缓存,然后再更新数据库。
  • 问题:在高并发情况下,可能会导致缓存和数据库的不一致。
分析
  • 优点:

    • 缓存更新速度快,用户可以立即看到最新数据。
  • 缺点:

    • 如果在更新缓存后,数据库更新失败,会导致缓存和数据库数据不一致。
    • 并发请求可能在缓存和数据库之间产生竞态条件,导致数据不一致。
改进建议
  • 事务管理:尝试将缓存更新和数据库更新放在同一个事务中,但由于Redis和关系数据库通常不在同一个事务管理系统中,实际实现较为复杂。
  • 补偿机制:在更新过程中,如果数据库更新失败,及时回滚缓存的更新。
2. 先写数据库,再写缓存
描述
  • 操作顺序:先更新数据库,然后再更新缓存。
  • 问题:类似于第一种方法,也可能在高并发下导致一致性问题。
分析
  • 优点:

    • 数据库作为权威数据源,先更新数据库可以保证数据的持久性。
  • 缺点:

    • 如果在更新数据库后,更新缓存失败,导致缓存与数据库不一致。
    • 并发请求可能在数据库更新与缓存更新之间引入不一致的窗口期。
改进建议
  • 原子操作:使用分布式事务或两阶段提交协议(2PC)来确保数据库和缓存的一致更新,但这会增加系统复杂性和延迟。
  • 幂等性设计:确保更新操作是幂等的,即使发生多次更新,也不会影响数据一致性。
3. 先删除缓存,再更新数据库
描述
  • 操作顺序:先删除缓存,然后更新数据库。
  • 问题:在并发请求下,可能出现先删除缓存后,一个请求从数据库读取并写回缓存,然后另一个请求继续更新数据库,导致缓存与数据库不一致。
分析
  • 优点:

    • 简化了缓存失效的管理,避免了复杂的缓存更新逻辑。
  • 缺点:

    • 存在“缓存击穿”问题,即在缓存删除和数据库更新之间的窗口期,多个请求可能直接访问数据库,增加数据库压力。
    • 并发请求可能导致缓存与数据库的不一致。
改进建议
  • 互斥锁:在删除缓存后,使用分布式锁来控制只有一个请求可以从数据库读取并更新缓存,其他请求等待或从缓存读取。
  • 请求合并:利用如“缓存击穿保护”机制,确保在缓存失效时,只有一个请求负责更新缓存,其他请求等待更新完成后再读取缓存。
4. 先删除缓存,再更新数据库,后续异步删除缓存内容(使用消息队列)
描述
  • 操作顺序:先删除缓存,再更新数据库,之后通过消息队列异步处理缓存的进一步操作。
分析
  • 优点:

    • 异步处理缓存更新,减轻主流程的压力。
    • 消息队列可以保证消息的可靠性和顺序性。
  • 缺点:

    • 系统复杂性增加,需要管理消息队列和异步处理逻辑。
    • 消息延迟可能导致缓存与数据库的一致性暂时不一致。
改进建议
  • 确保消息顺序:使用有序的消息队列,确保消息按照正确的顺序被处理,避免数据不一致。
  • 重试机制:实现消息处理的重试机制,确保在处理失败时能够重新尝试,防止缓存更新失败。
5. 先写数据库,再删除缓存(常用)
描述
  • 操作顺序:先更新数据库,然后删除缓存。
  • 问题:存在一个小的时间窗口,第一次查询缓存未命中后,从数据库读取并写入缓存,此时数据库已经被修改,导致缓存写入旧数据。
分析
  • 优点:

    • 数据库作为权威数据源,先更新数据库可以保证数据的持久性。
  • 缺点:

    • 存在竞态条件,导致缓存可能写入旧数据。
    • 虽然这种情况较少见,但在高并发环境下仍可能发生。
改进建议
  • 双重删除:在更新数据库后,再次删除缓存,以减少竞态条件发生的可能性。
  • 版本控制:在缓存和数据库中使用数据版本号或时间戳,确保缓存数据的有效性和一致性。
  • 事务日志:记录操作日志,确保在出现不一致时能够快速恢复和修复。
6. 通过Canal监听数据库Binlog并更新缓存
描述
  • 操作顺序:使用Canal监听数据库的Binlog,实时捕获数据变更并更新缓存,确保操作顺序不被查询打断。
  • 问题:需要确保操作的顺序性,防止查询操作插入到变更操作之间,导致数据不一致。
分析
  • 优点:

    • 通过监听数据库变更,能够实时、自动地更新缓存,减少人工干预。
    • 保证了数据变更的可靠性和一致性。
  • 缺点:

    • 系统复杂性增加,需要部署和维护Canal等工具。
    • 处理延迟,尤其是在高并发和大数据量的情况下,可能导致缓存与数据库之间的延迟不一致。
    • 需要处理Binlog解析的兼容性和准确性问题。
改进建议
  • 数据处理流水线优化:优化Canal的处理流水线,减少处理延迟,确保缓存及时更新。
  • 监控与告警:实时监控Canal的运行状态和数据同步情况,及时发现和处理异常。
  • 幂等性设计:确保通过Canal更新缓存的操作是幂等的,避免重复更新导致的数据问题。
7. 强一致性:读读不互相锁,读写和写写互相锁
描述
  • 操作顺序:在强一致性模式下,读操作之间不互相锁定,但读写和写写操作需要互相锁定,以确保数据的一致性。
  • 问题:需要有效的锁机制来管理并发访问,避免死锁和性能瓶颈。
分析
  • 优点:

    • 保证了数据操作的强一致性,避免了数据不一致的问题。
    • 通过锁机制,控制了并发操作,防止了竞态条件。
  • 缺点:

    • 锁机制可能引入较高的延迟,影响系统性能,尤其是在高并发场景下。
    • 需要精细设计锁的粒度和持有时间,避免死锁和资源竞争问题。
    • 分布式锁的实现复杂,可能带来额外的网络开销和管理成本。
改进建议
  • 优化锁粒度:尽量使用细粒度的锁,减少锁的持有时间,降低锁竞争的概率。
  • 使用高效的分布式锁算法:例如Redlock,确保锁的可靠性和性能,但需要注意其局限性和适用场景。
  • 避免锁的滥用:仅在必要的关键操作中使用锁,避免对整个缓存或数据库表进行全局锁定。

2.3Redis中的缓存击穿、缓存穿透、缓存雪崩

  • 互斥锁实现示例(Java)

    public class CacheService {
        private Jedis jedis = new Jedis("localhost");
        private Lock lock = new ReentrantLock();
    
        public String getData(String key) {
            // 尝试从缓存获取数据
            String value = jedis.get(key);
            
            // 如果缓存不存在
            if (value == null) {
                // 加锁以防止并发请求
                lock.lock();
                try {
                    // 再次检查缓存,避免重复查询
                    value = jedis.get(key);
                    if (value == null) {
                        // 查询数据库
                        value = queryDatabase(key);
                        // 将结果放入缓存
                        jedis.set(key, value);
                    }
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            }
            return value;
        }
    }
    

    说明:

    1. 缓存查询:首先尝试从 Redis 中获取数据。
    2. 加锁:如果缓存中没有数据,使用 ReentrantLock 加锁,确保只有一个线程可以查询数据库。
    3. 二次检查:在加锁后再次检查缓存,避免重复查询。
    4. 数据库查询:如果缓存仍然没有数据,查询数据库并将结果存入缓存。
    5. 释放锁:确保锁在查询结束后被释放,以防止死锁。
      这种方式有效地防止了缓存击穿,因为即使在高并发的情况下,只有一个请求会去数据库查询数据,其他请求则会等待铁释放。如果后端是多实例部署般实例数量也不多,即使使用本地锁也行,因为并发也不高。

http://www.kler.cn/a/470389.html

相关文章:

  • Python递归(汉诺塔问题)
  • 旷视科技C++面试题及参考答案
  • Rocketmq 探索MQClientFactoryScheduledThread线程工作
  • 《探秘计算机视觉与深度学习:开启智能视觉新时代》
  • 详解GPT-信息抽取任务 (GPT-3 FAMILY LARGE LANGUAGE MODELS)
  • Wireshark 具体某种协议的分析
  • 出租号平台网站系统源码/单合租用模式 提供用户提现功能
  • leetcode热题100(84. 柱状图中最大的矩形)c++
  • 如何利用Java爬虫按关键字搜索淘宝商品案例指南
  • 机器学习基础-支持向量机SVM
  • 玉米识别数据集,4880张图,正确识别率可达98.6%,支持yolo,coco json,pasical voc xml格式的标注,可识别玉米
  • 工控安全需求分析与安全保护工程
  • java学习 单例模式
  • 11-Gin 中的 Cookie --[Gin 框架入门精讲与实战案例]
  • vue.js 自定义指令-基础语法
  • 大数据安全需求分析与安全保护工程
  • 【PyTorch入门】 PyTorch不同优化器的比较
  • Netty中用了哪些设计模式?
  • 云计算安全需求分析与安全防护工程
  • windows下,golang+vscode+delve 远程调试
  • 安卓漏洞学习(十八):Android加固基本原理
  • PHP零基础入门笔记
  • Vue的后端之一,Django
  • 【大数据】(选修)实验4 安装熟悉HBase数据库并实践
  • 2台ubuntu之间scp
  • QPainter,QPen,QBrush详解