Redis

Redis不管是使用还是学习,都已经很多很多遍了,但是对一些术语记得不牢,决定再学习一遍。

一、缓存穿透、缓存击穿、缓存雪崩

特性 缓存穿透 缓存击穿 缓存雪崩
核心问题 查询不存在的数据 热点key失效瞬间被高并发访问 大量key同时失效缓存服务宕机
缓存状态 缓存和数据库都没有数据 缓存刚好过期 缓存大面积失效不可用
影响范围 单个不存在的key 单个热点key 大量key或整个缓存服务
恶意攻击 通常由恶意攻击引起 通常由正常的业务高峰引起 通常由设计不当或故障引起
解决方案 1. 缓存空对象 2. 布隆过滤器 1. 热点数据永不过期 2. 互斥锁 1. 差异化过期时间 2. 高可用集群哨兵模式、集群模式 3. 降级熔断nginx或spring cloud gateway
后果 大量无效查询直接落到数据库,给数据库造成巨大压力,甚至可能拖垮数据库。 缓存失去了保护数据库的作用。 数据库在短时间内承受巨大的并发请求,可能引起性能骤降甚至服务不可用。 导致所有请求都直接落到数据库,数据库因承受不了巨大压力而崩溃,进而导致整个系统瘫痪

简单来说:

  • 防穿透,重在拦截非法请求(布隆过滤器)。

  • 防击穿,重在避免热点数据被集体访问(加锁或永不过期)。

  • 防雪崩,重在避免缓存集体失效(随机过期时间 + 高可用)。

缓存穿透-布隆过滤器

在Spring Boot中实现布隆过滤器,通常有几种方式:

1.使用Guava库提供的布隆过滤器(单机版)

java 复制代码
import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;

@Component
public class BloomFilterService {

    // 预期插入的数据量
    private static final int EXPECTED_INSERTIONS = 1000000;
    // 误判率
    private static final double FPP = 0.01;
    // 布隆过滤器
    private BloomFilter<String> bloomFilter;

    /**
     * 初始化布隆过滤器
     * 在应用启动时,将已有的数据(例如从数据库加载的所有有效键)放入布隆过滤器
     */
    @PostConstruct
    public void init() {
        // 初始化布隆过滤器
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), EXPECTED_INSERTIONS, FPP);
        // 假设这里从数据库加载所有有效的键,这里用空列表代替,实际应用需要从数据库查询
        List<String> allValidKeys = loadAllValidKeysFromDatabase();
        for (String key : allValidKeys) {
            bloomFilter.put(key);
        }
    }

    /**
     * 模拟从数据库加载所有有效键
     * @return 有效键列表
     */
    private List<String> loadAllValidKeysFromDatabase() {
        // 这里应该从数据库查询,例如:select key from table where valid = true
        // 返回所有需要加入布隆过滤器的键
        // 示例返回空列表,实际应用需要替换
        return List.of();
    }

    /**
     * 判断键是否可能存在
     * @param key 待检查的键
     * @return 如果布隆过滤器认为键存在,则返回true(可能误判);如果返回false,则键一定不存在
     */
    public boolean mightContain(String key) {
        return bloomFilter.mightContain(key);
    }

    /**
     * 手动添加键到布隆过滤器(例如,当新增数据时,除了写入数据库,也加入布隆过滤器)
     * @param key 新增的键
     */
    public void put(String key) {
        bloomFilter.put(key);
    }
}

2.使用Redis提供的布隆过滤器模块Redisson(分布式)

java 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.4</version> <!-- 请使用最新版本 -->
</dependency>
java 复制代码
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;

@Component
public class RedisBloomFilterService {

    @Autowired
    private RedissonClient redissonClient;

    private RBloomFilter<String> bloomFilter;

    // 布隆过滤器的key
    private static final String BLOOM_FILTER_KEY = "myBloomFilter";

    /**
     * 初始化布隆过滤器
     */
    @PostConstruct
    public void init() {
        // 获取或创建布隆过滤器
        bloomFilter = redissonClient.getBloomFilter(BLOOM_FILTER_KEY);
        // 初始化布隆过滤器,预计元素数量为1000000,误判率为0.01
        bloomFilter.tryInit(1000000L, 0.01);
        // 加载已有的数据
        List<String> allValidKeys = loadAllValidKeysFromDatabase();
        for (String key : allValidKeys) {
            bloomFilter.add(key);
        }
    }

    private List<String> loadAllValidKeysFromDatabase() {
        // 从数据库加载所有有效键,同上例,省略
        return List.of();
    }

    /**
     * 判断键是否可能存在
     */
    public boolean mightContain(String key) {
        return bloomFilter.contains(key);
    }

    /**
     * 添加键到布隆过滤器
     */
    public void add(String key) {
        bloomFilter.add(key);
    }
}

缓存击穿-互斥锁

互斥锁 :这是最常见的解决方案

  • 流程 :当发现缓存失效时,不是所有线程都去查询数据库,而是让一个线程(如使用Redis的SETNX命令获取分布式锁) 去查询数据库并重建缓存。其他线程等待锁释放后,重新从缓存中读取数据。

  • 优点:能有效防止数据库被瞬间击垮。

  • 缺点:性能有一定损耗,可能出现线程等待。实现相对复杂。

java 复制代码
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

@Service
@Slf4j
@RequiredArgsConstructor
public class ProductServiceWithDoubleCheck {
    
    private final RedisTemplate<String, Object> redisTemplate;
    private final ProductRepository productRepository;
    
    // 本地锁,用于减少Redis访问(适合单机多线程场景)
    private static final ConcurrentHashMap<String, ReentrantLock> localLocks = 
        new ConcurrentHashMap<>();
    
    private static final String CACHE_PREFIX = "product:";
    private static final String LOCK_PREFIX = "product_lock:";
    private static final String NULL_CACHE_PREFIX = "null_product:";
    
    /**
     * 双重检查锁 + 空值缓存
     */
    public Product getProductWithDoubleCheck(String productId) {
        String cacheKey = CACHE_PREFIX + productId;
        String nullCacheKey = NULL_CACHE_PREFIX + productId;
        
        // 1. 先检查空值缓存
        if (Boolean.TRUE.equals(redisTemplate.hasKey(nullCacheKey))) {
            log.debug("命中空值缓存,productId: {}", productId);
            return null;
        }
        
        // 2. 检查正常缓存
        Product product = getFromCache(cacheKey);
        if (product != null) {
            return product;
        }
        
        // 3. 获取本地锁(减少Redis压力)
        ReentrantLock localLock = localLocks.computeIfAbsent(
            productId, k -> new ReentrantLock());
        
        localLock.lock();
        try {
            // 4. 再次检查缓存(第一重检查)
            product = getFromCache(cacheKey);
            if (product != null) {
                return product;
            }
            
            // 5. 获取分布式锁
            String lockKey = LOCK_PREFIX + productId;
            String requestId = UUID.randomUUID().toString();
            boolean distributedLocked = false;
            
            try {
                // 尝试获取分布式锁
                distributedLocked = tryRedisLock(lockKey, requestId, 30, 1000);
                
                if (distributedLocked) {
                    // 6. 第三重检查
                    product = getFromCache(cacheKey);
                    if (product != null) {
                        return product;
                    }
                    
                    // 7. 查询数据库
                    product = productRepository.findById(productId).orElse(null);
                    
                    if (product == null) {
                        // 缓存空值,防止缓存穿透
                        redisTemplate.opsForValue().set(
                            nullCacheKey, 
                            "null", 
                            60, // 空值缓存时间短一些
                            TimeUnit.SECONDS
                        );
                        log.debug("缓存空值,productId: {}", productId);
                        return null;
                    }
                    
                    // 8. 写入缓存
                    setToCache(cacheKey, product);
                    
                    return product;
                } else {
                    // 9. 获取分布式锁失败,等待并重试
                    return waitForCache(cacheKey, productId);
                }
            } finally {
                // 释放分布式锁
                if (distributedLocked) {
                    releaseRedisLock(lockKey, requestId);
                }
            }
        } finally {
            localLock.unlock();
            // 清理本地锁(可选)
            localLocks.remove(productId);
        }
    }
    
    /**
     * 等待缓存
     */
    private Product waitForCache(String cacheKey, String productId) {
        int maxWaitTime = 2000; // 最大等待2秒
        long startTime = System.currentTimeMillis();
        
        while (System.currentTimeMillis() - startTime < maxWaitTime) {
            try {
                Product product = getFromCache(cacheKey);
                if (product != null) {
                    log.debug("等待后获取到缓存,productId: {}", productId);
                    return product;
                }
                Thread.sleep(50); // 短暂等待
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        
        log.warn("等待缓存超时,直接查询数据库(可能有压力),productId: {}", productId);
        // 兜底:直接查询数据库
        return productRepository.findById(productId).orElse(null);
    }
    
    /**
     * 尝试获取Redis锁
     */
    private boolean tryRedisLock(String lockKey, String requestId, 
                                long expireSeconds, long waitMillis) {
        long endTime = System.currentTimeMillis() + waitMillis;
        
        while (System.currentTimeMillis() < endTime) {
            Boolean success = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS);
            
            if (Boolean.TRUE.equals(success)) {
                return true;
            }
            
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        
        return false;
    }
    
    /**
     * 释放Redis锁
     */
    private void releaseRedisLock(String lockKey, String requestId) {
        String value = (String) redisTemplate.opsForValue().get(lockKey);
        if (requestId.equals(value)) {
            redisTemplate.delete(lockKey);
        }
    }
    
    // ... getFromCache, setToCache 方法同上 ...
}

二、先删除缓存,还是先修改数据库

双写一致性指的是同时更新缓存和数据库时,如何保证两者的数据一致。

1.双写一致性的核心挑战

挑战1:原子性无法保证

java 复制代码
// 这不是原子操作!
redis.set(key, value);  // 步骤1
db.update(data);        // 步骤2 - 可能失败

挑战2:并发时序问题

java 复制代码
线程A:set cache=20 → update db=20
线程B:set cache=30 → update db=30
结果取决于执行顺序

挑战3:失败处理

  • 缓存成功,数据库失败:如何回滚?

  • 数据库成功,缓存失败:如何补偿?

2. 先删除缓存,再更新数据库

可以立即清除旧数据,迫使后续读请求直接访问数据库,获取最新数据

1.问题:并发下的数据不一致

java 复制代码
时间线:
1. 线程A删除缓存
2. 线程B读取缓存(未命中)→ 读取数据库旧值 → 写入缓存(旧值)
3. 线程A更新数据库(新值)
结果:缓存中是旧数据,数据库是新数据,不一致

2.解决方案:延迟双删

java 复制代码
线程A(写操作)                线程B(读操作)               线程C(读操作)
---------                     ---------                   ---------
1. 删除缓存                   
                             2. 缓存未命中
                             3. 查询从库(旧数据)
                             4. 写入缓存(旧数据)
5. 更新主库
6. 主从同步开始...
7. 等待延迟(主从同步完成)
8. 第二次删除缓存
                                                9. 缓存未命中
                                                10. 查询从库(新数据)
                                                11. 写入缓存(新数据)

3.适用场景:

  • 缓存极其重要,读性能要求极高

  • 数据库更新可重试,且有补偿机制

  • 数据允许短暂不一致

3. 先更新数据库,再删除缓存(推荐)

在修改数据库后、删除缓存前,其他线程可能读到旧的缓存数据

问题:

  1. 缓存穿透风险:删除后,大量请求直接打到数据库

  2. 短暂不一致:删除后、更新前,有请求会读到旧数据

优点:

  1. 数据一致性更好:即使第二步(删除缓存)失败,最多是缓存脏数据,数据库始终是正确的

  2. 并发问题概率低:缓存未命中时,会从数据库读取最新数据

大多数情况下,选择"先更新数据库,再删除缓存",因为:

  1. 数据库是唯一可信源,保证数据库正确最重要

  2. 缓存可以重建,缓存不一致的影响相对较小

  3. 配合异步删除+重试机制,可以保证最终一致性

  4. 通过设置合理的缓存过期时间,可以兜底解决脏数据

  5. 不同业务场景的选择

|--------------------|-------------------|------------------|----------------------|------------------|
| 场景 | 策略 | 补充 | 原因 | 场景示例 |
| 读多写少 | 先更新DB,再删除缓存 | 设置缓存过期时间 | 写操作少,删除缓存影响小 | 商品详情 |
| 写多读少 | 直接更新DB,缓存等待过期 | 使用本地缓存 + 批量写入 | 频繁更新缓存代价高 | 点击计数 |
| 强一致性要求 | 分布式锁+ 先DB后缓存 | 使用数据库行锁或乐观锁 | 不允许超卖 | 库存扣减 |
| 数据量小但访问频繁 | 先更新DB,再更新缓存(带版本号) | 使用广播通知所有节点更新本地缓存 | 配置需要立即生效 | 配置信息 |
| 写操作频繁,缓存删除需要解耦 | 消息MQ,保证最终一致性 | 重试机制 | 保证最终一致性,但可接受短暂延迟 | 跨服务/跨系统的缓存更新 |

如果系统对一致性要求极高,可以考虑:

  • 使用分布式锁(性能有影响)

  • 使用数据库事务+缓存更新(复杂性高)

  • 使用CDC工具监听数据库变更

在分布式系统中,追求最终一致性往往比强一致性更实际

三、Redis持久化

在Redis中提供了两种数据持久化的方式:1.RDB 2.AOF

1.RDB

RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。

  • 定期生成内存数据的快照

  • 生成 .rdb 文件(二进制格式)

优点

  • 性能好,fork子进程处理,不影响主进程

  • 文件紧凑,适合备份和灾难恢复

  • 恢复大数据集比AOF快

缺点

  • 可能丢失最后一次快照后的数据

  • fork过程可能阻塞(内存大时)

工作原理

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。fork采用的是copy-on-write技术:

  • 当主进程执行读操作时,访问共享内存;
  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作。

数据究竟丢失多少?

Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:

比如配置save 900 1(900秒内,如果至少有一个key被修改,则执行bgsave)

  • 如果在 900秒(15分钟) 内,至少有一个键被修改

  • 那么 在第900秒时 触发一次RDB快照保存

  • 从上次快照到本次快照之间的数据修改可能丢失

最坏的情况:

bash 复制代码
时间线:
0秒:    上一次RDB快照完成(包含此刻的所有数据)
1秒:    修改了一个键(key1) ← 开始计时
... (继续正常使用)
900秒:  触发BGSAVE,开始创建新快照
901秒:  BGSAVE完成,新快照保存了key1的修改

如果在 899秒 时Redis崩溃

那么从 第1秒到第899秒 的所有修改都会丢失

最多可能丢失899秒(约15分钟)的数据

但是!!!单靠 save 900 1 风险很高,还会配置几个save的,

多个save条件的关系

|----------------------------------------------------------------------------------|
| * Redis会同时检查所有save条件 * 满足任意一个条件就会触发保存 * save 60 10000 可能在1分钟就触发,覆盖save 900 1 |

如果遇到内存不够等问题

  • BGSAVE可能因为内存问题失败

  • 如果BGSAVE失败,直到下次成功保存前,数据都面临丢失风险

2.AOF

AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。

AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:

AOF文件一般存储在redis服务器的工作目录下,文件名为 appendonly.aof。

  • 记录每个写操作到日志文件

  • 重启时重新执行所有命令恢复数据

优点

  • 数据安全性高(可配置)

  • 可读的日志文件,便于分析

  • 最多丢失1秒数据(everysec配置)

缺点

  • 文件通常比RDB大

  • 恢复速度较慢

  • 写入性能略低于RDB

同步策略

AOF的命令记录的频率也可以通过redis.conf文件来配:

解决AOF文件大的问题

**AOF重写,**创建当前数据的最小命令集。

bash 复制代码
# 手动触发
redis-cli BGREWRITEAOF

# 自动触发配置
auto-aof-rewrite-percentage 100   # 比上次重写后体积增加100%
auto-aof-rewrite-min-size 64mb    # AOF文件至少64MB

3.混合持久化(Redis 4.0+)

结合两者优势:

bash 复制代码
aof-use-rdb-preamble yes
  • 重写时:将当前数据以RDB格式写入AOF开头

  • 追加写:后续命令继续以AOF格式追加

  • 恢复时:先加载RDB部分,再执行AOF命令

4.选择建议

场景 推荐方案
可容忍分钟级数据丢失 RDB
需要更高数据安全性 AOF
兼顾性能与安全 RDB + AOF
Redis 4.0+ 混合持久化

5.持久化注意事项

(1)监控持久化状态

bash 复制代码
redis-cli info persistence

(2)备份策略

  • 定期备份RDB/AOF文件到其他服务器
  • 测试恢复流程

(3)性能调优

  • 避免在持久化时执行大量写操作( 最好用户使用少的时候,进行持久化**)**
  • 监控fork耗时:latest_fork_usec指标

(4)内存规划

  • 持久化时需要额外内存(尤其是fork时,RDF持久)
  • 建议预留50%内存

6.恢复优先级

RDB因为是二进制文件 ,在保存的时候体积也是比较小的,它恢复的比较快 ,但是它有可能会丢数据;我们在项目中通常也会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令

  1. 如果开启AOF,优先使用AOF恢复

  2. 否则使用RDB恢复

  3. 两者都关闭则数据完全在内存

7.推荐生产环境配置

bash 复制代码
# 生产环境推荐配置(平衡安全与性能)
save 900 1      # 15分钟至少1个修改
save 300 10     # 5分钟至少10个修改  ← 减少数据丢失窗口
save 60 10000   # 1分钟至少10000个修改 ← 应对突发高写入

# 如果数据非常重要,必须加AOF
appendonly yes
appendfsync everysec  # 最多丢失1秒数据

# 监控和报警
监控:rdb_last_save_time, aof_last_bgrewrite_status

四、Redis数据过期策略

假如redis的key过期之后,会立即删除吗?

Redis对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉。可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)。 惰性删除、定期删除

1.惰性删除

惰性删除:

  • 原理:只在访问键时检查是否过期,过期则删除

  • 触发时机:任何读取/写入键的命令执行前

  • 优点:对 CPU 友好,只在实际需要时才会进行过期检查,过期则删除。对于很多用不到的key不用浪费时间进行过期检查

  • 缺点:内存不友好,可能积累大量过期键未删除。也就是,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放

bash 复制代码
客户端执行 GET key
    ↓
Redis检查 key 是否过期
    ↓
如果过期 → 删除key → 返回nil
如果未过期 → 返回值

2 定期删除

定期删除:每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。

  • 原理:Redis 定期(默认每100ms)随机抽取部分键检查删除

  • 触发时机:由 Redis 主线程周期性执行

  • 配置:不可配置,但行为可调优

  • 优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。

  • 缺点:难以确定删除操作执行的时长和频率。

定期清理有两种模式:

  • SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的hz 选项来调整这个次数
  • FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

3.会带来的问题:

陷阱1 :设置了过期时间但从不访问

后果:依赖定期删除,可能堆积

解决:定期扫描并访问,或使用volatile-ttl策略

陷阱2 :大量键同时过期

后果:内存瞬间释放,可能引发阻塞

解决:给过期时间加随机抖动

SET key value EX $((60 + RANDOM % 60)) # 60-120秒随机

陷阱3 :内存淘汰频繁触发

后果:性能下降

解决:增加内存或调整淘汰策略

Redis的过期删除策略:惰性删除 + 定期删除 两种策略进行配合使用

五、Redis数据淘汰策略

假如缓存过多,内存是有限的,内存被占满了怎么办? ------ 其实就是想问redis的数据淘汰策略是什么?

数据的淘汰策略:当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

1.八种数据淘汰策略

Redis支持8种不同策略来选择要删除的key:

  • noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略
  • volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
  • allkeys-random:对全体key ,随机进行淘汰
  • volatile-random:对设置了TTL的key ,随机进行淘汰
  • allkeys-lru: 对全体key,基于LRU算法进行淘汰
  • volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰
  • allkeys-lfu: 对全体key,基于LFU算法进行淘汰
  • volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰
策略 说明 适用场景
noeviction (默认) 不淘汰,新写入报错 数据绝不能丢失
allkeys-lru 从所有键中淘汰最少使用的 无明确访问模式
volatile-lru 从设了过期时间的键中淘汰最少使用的 部分数据可丢失
allkeys-random 从所有键中随机淘汰 所有键被访问概率相同
volatile-random 从设了过期时间的键中随机淘汰 部分数据随机淘汰
volatile-ttl 淘汰剩余生存时间最短的 清理即将过期的数据

Redis 4.0+ 新增LFU策略

策略 说明 特点
allkeys-lfu 淘汰最不经常使用的键 基于访问频率
volatile-lfu 从过期键中淘汰最不经常使用的 结合频率和时间

LRU的意思就是最少最近使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。

LFU的意思是最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高

2.数据淘汰策略------使用建议

  1. 优先使用allkeys-lru策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。
  2. 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random,随机选择淘汰。
  3. 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
  4. 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略。

六、Redis 数据结构及应用场景对比表

数据结构 内部实现 最大存储 特点 适合存储的数据类型 典型应用场景 注意事项
String SDS (简单动态字符串) 512MB 二进制安全,可存储文本/数字/序列化对象 1. 字符串/文本 2. 整数/浮点数 3. 序列化对象(JSON/protobuf) 4. 二进制数据 1. 缓存 2. 计数器 3. 分布式锁 4. 会话存储 大字符串影响性能,建议分片存储
List 双向链表/压缩列表(ziplist) 约40亿元素 有序,可重复,支持双向操作 1. 消息队列 2. 最新列表 3. 任务队列 4. 历史记录 1. 消息队列 2. 时间线 3. 最新N条记录 4. 分页查询 长列表影响性能,考虑分拆
Hash 字典(dict)/压缩列表(ziplist) 约40亿键值对 适合存储对象,可部分更新 1. 用户对象 2. 商品属性 3. 配置信息 4. 对象字段聚合 1. 用户信息存储 2. 购物车 3. 对象缓存 4. 聚合统计 单个Hash不宜过大,建议不超过1000字段
Set 字典(dict)/整数集合(intset) 约40亿元素 无序,不重复,支持集合运算 1. 标签系统 2. 好友关系 3. 抽奖用户池 4. 黑白名单 1. 共同好友/兴趣 2. 去重 3. 随机推荐 4. 数据排重 SMEMBERS返回全部元素,大集合谨慎使用
Sorted Set 跳表(skiplist)+字典 约40亿元素 有序,不重复,按分数排序 1. 排行榜 2. 带权重的队列 3. 时间轴 4. 范围查询数据 1. 实时排行榜 2. 延时队列 3. 范围查询 4. 优先级队列 分数可重复,成员唯一;分数支持浮点数
Bitmap String的位操作 512MB(2^32位) 位操作,极度节省空间 1. 用户在线状态 2. 签到记录 3. 布隆过滤器 4. 特征标记 1. 日活跃用户统计 2. 用户标签系统 3. 签到打卡 4. 布隆过滤器 适合大规模布尔值存储,节省内存
HyperLogLog 特殊字符串结构 约12KB 基数估算,误差率0.81% 1. 独立访客统计(UV) 2. 搜索词去重 3. 大规模集合基数估算 1. 网站UV统计 2. 大规模数据去重统计 3. 不需要精确结果的计数 只能估算基数,不能获取具体元素
Geospatial Sorted Set扩展 基于Sorted Set限制 地理位置存储与计算 1. 地理位置坐标 2. 附近的人/地点 3. 距离计算 4. 地理围栏 1. 附近商家推荐 2. 共享单车位置 3. 出行距离计算 4. 地理围栏监控 底层是Sorted Set,使用经纬度转分数

七、面试问题:

  1. Redis常见数据结构有哪些?Hash适合存什么样的数据?
  2. 什么是缓存穿透,缓存击穿,缓存雪崩?以及解决方案
  3. 先删除缓存,还是先修改数据库?
  4. Redis的RDB和AOF持久化方式,优缺点及适用场景是什么?持久化怎么恢复的?
  5. Redis RDF如果出现丢失数据,会丢失多少,比如save 60 10000
  6. Redis的数据过期策略有哪些?
  7. Redis淘汰策略
  8. 数据库有1000万数据,Redis只能缓存20w数据,如何保证Redis中的数据都是热点数据 ?
  9. Redis的内存用完了会发生什么?
  10. 主从复制主要解决什么问题?哨兵模式比主从复制增加了哪些能力?

部分答案:

8.数据库有1000万数据,Redis只能缓存20w数据,如何保证Redis中的数据都是热点数据 ?

使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据

9.Redis的内存用完了会发生什么?

主要看数据淘汰策略是什么?如果是默认的配置( noeviction ),会直接报错。

10.主从复制主要解决什么问题?哨兵模式比主从复制增加了哪些能力?

主从复制主要解决了数据备份、读写分离和负载均衡 的问题,它通过从节点同步主节点数据来提供冗余副本,并将读请求分流到从节点以提升读性能。而哨兵模式在主从复制的基础上,显著增强了高可用性 ,它通过独立的哨兵集群持续监控所有节点,能在主节点故障时自动完成故障检测、选举新主节点并更新所有客户端配置,从而实现了无需人工干预的自动故障转移,为Redis服务提供了真正的生产级高可用保障。

本文有部分引用:https://cloud.tencent.com/developer/article/2436822

相关推荐
l1t9 小时前
DeepSeek辅助编写的利用位掩码填充唯一候选数方法求解数独SQL
数据库·sql·算法·postgresql
墨月白9 小时前
[QT] QT中的折线图和散点图
数据库·qt
龙潜月七9 小时前
做一个背单词的脚本
数据库·windows·c#·aigc·程序那些事
我科绝伦(Huanhuan Zhou)9 小时前
DM数据库物理存储结构深度解析与理论实践
数据库·oracle
霖霖总总9 小时前
[小技巧23]全面理解 MySQL 的 WAL 机制:原理、影响与可观测性
数据库·mysql
列御寇9 小时前
MongoDB分片集概述
数据库·mongodb
又是忙碌的一天10 小时前
SpringMVC响应
java·服务器·数据库
梭七y10 小时前
【力扣hot100题】(133)LRU缓存
leetcode·缓存·哈希算法
W001hhh10 小时前
260110
java·数据库