Redis八股记忆笔记和使用示例
Redis为什么快
比较重要的原因有下面 4 点:
1、纯内存操作(主要):它的数据读写操作都发生在内存中,访问速度是纳秒级,而传统数据库读写速度是毫秒级,相差数个数量级。
2、I/O 模型高效:它使用单线程事件循环配合I/O多路复用,让单个线程可同时处理多个网络I/O事件,轻松处理大量并发请求。
3、数据结构有优化:它提供了多种数据类型(如String, Hash, Set等),和高度优化的编码方式,能动态选择最合适的编码,从而平衡性能和空间。
4、通信协议简洁高效:它使用自己设计的RESP协议。实现简单、性能好、客户端和服务端通信的序列化/反序列化开销小,所以整体交互较快。
缓存穿透、击穿、雪崩的区别及解决方案
缓存穿透是查一个不存在的数据,缓存和数据库都没有。解决方案有两个:
第一个是将所有存在的key提前存入布隆过滤器。请求来时用它判断,不存在就直接返回。
第二个是缓存空对象。查询不到的数据也缓存起来,value为null,过期时间设置短一些。
缓存击穿是一个热点 key 刚好过期,大量请求同时打到数据库。解决方案有三个:
第一个是提前预热:比如针对热点数据提前将其存入缓存中,并设置过期时间覆盖活动时间。
第二个是加互斥锁:在缓存失效后,通过设置互斥锁确保只有一个请求去查库并更新缓存。
第三个是让key永不过期。
缓存雪崩是大量 key 同时过期,请求全部落到数据库。解决方案分两种情况:
针对 Redis 服务不可用:
Redis 集群:避免单节点宕机导致整个缓存不可用。
多级缓存:本地缓存 + Redis 缓存,Redis 挂了还能从本地缓存拿数据。
针对大量缓存同时失效:
设置随机失效时间:在固定过期时间上加随机偏移量,避免集体过期。
提前预热:秒杀等场景下,热点数据提前加载到缓存,活动期间不过期。
缓存不过期:一般不推荐,但对于某些变化不频繁的数据,可以考虑使用。
RDB 和 AOF 的区别
RDB 是定期生成的全量快照,文件小,恢复速度快,缺点是两次快照之间如果宕机,这期间的所有数据都会丢失。
AOF 是追加写命令,默认每秒同步一次到磁盘,最多丢失一秒数据,但文件体积大,恢复速度慢。
如果追求性能,但是对数据丢失不敏感,例如缓存场景,可以用 RDB;如果对数据丢失很敏感,例如订单数据,可以用 AOF。默认情况下是RDB,还有一个折中方案是两者都开启,重启恢复优先使用AOF,RDB作为备份对AOF损坏等场景兜底。
Redis 的过期删除策略
Redis有两种过期删除策略:定期删除和惰性删除。
定期删除:每隔100ms随机抽查一批设置了过期时间的key,删除其中过期的。它会根据过期key的比例动态调整执行频率,过期key越多,抽查频率越高。
惰性删除:每次get一个key时,先检查它是否过期,过期就删除,但如果一直没人访问,过期key会占着内存。
两者配合:定期删除主动释放内存,惰性删除分摊CPU开销。如果两者都没删掉,还有内存淘汰策略兜底。
Redis 淘汰策略
Redis 有八种淘汰策略,常用的5个分别是:
allkeys-lru 从所有 key 中淘汰最近最少使用的;
volatile-lru 从设置了过期时间的 key 中淘汰最近最少使用;
allkeys-random 随机淘汰;
volatile-ttl 淘汰即将过期的;
noeviction 不淘汰,内存不足时写操作报错。
Redis 分布式锁如何实现
Redis 分布式锁的核心是 setnx 命令。也就是不存在才set,比如加锁命令:set key value nx ex 30,在java中是调用方法setIfAbsent。要同时要设置合理的过期时间,防止死锁的同时确保任务能执行完毕。解锁时需要防止误删,也就是先判断锁的值是否是自己设置的,再删除,这个操作要用 Lua 脚本保证原子性。如果业务执行时间会超过过期时间,还可以用 Redisson 框架,它已经封装好了看门狗自动续期功能。
为什么设置过期可以防死锁?
分两种情况:第一种是锁无法释放,会导致被锁住的资源无法使用,但是我认为它不是严格意义上的死锁。
第二种是真正的死锁场景,也就是两个线程互相循环等待对方的锁,导致两个线程都无法正常结束,而如果其中一个锁设置了过期时间,那么阻塞一段时间后,锁会自动释放,循环等待就会解除,死锁打开。
项目中 Redis 使用场景示例
用Redis 的SETNX和INCR做接口限流:
private void checkLimit(int limitCount, String key) {
Long count;
redisTemplate.opsForValue().setIfAbsent(key, "0", LIMIT_WINDOW_SECONDS, TimeUnit.SECONDS);
count = redisTemplate.opsForValue().increment(key, 1);
if (count != null && count > limitCount) {
throw new ReturnException("接口访问过于频繁,请稍后重试" + key);
}
}
Redis 存储签名随机串,防止重放攻击
//防重放
Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(RedisConstant.LOGIN_NONCE + nonce, "1", wechatLoginProperties.getNonceExpireSeconds(), TimeUnit.SECONDS);
if (Boolean.FALSE.equals(absent)) {
throw new ReturnException("请求过于频繁,请稍后重试");
}
用Redis 分别用openid为key存token、用token为key存用户信息,实现单点登录;
String oldToken = stringRedisTemplate.opsForValue().get(RedisConstant.LOGIN_OPENID + memberDTO.getOpenid());
if (StringUtils.isNotBlank(oldToken)) {
//删除旧key
stringRedisTemplate.delete(RedisConstant.LOGIN_TOKEN + oldToken);
}
//按照id存token,按照token存用户信息
stringRedisTemplate.opsForValue().set(RedisConstant.LOGIN_OPENID + memberDTO.getOpenid(), token, 2, TimeUnit.HOURS);
stringRedisTemplate.opsForValue().set(RedisConstant.LOGIN_TOKEN + token, objectMapper.writeValueAsString(memberDTO), 2, TimeUnit.HOURS);
用 Redis Set 存储有效优惠券 ID,防止缓存穿透。
if (couponDTO.getId() == null) {
couponMapper.insert(coupon);
} else {
int rows = couponMapper.updateById(coupon);
if (rows == 0) {
throw new ReturnException("优惠券id不存在,更新失败");
}
// 删除redis缓存
redisTemplate.delete(RedisConstant.ALL_COUPON + coupon.getId());
}
// 更新redis优惠券id列表
redisTemplate.opsForSet().add(RedisConstant.COUPON_IDS, String.valueOf(coupon.getId()));
return couponDTO;
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)