基于键值对(key-value)
的数据库。为了满足不同的业务场景,Redis 中的 value 支持多种不同的数据结构,比如 String、Hash、List、Set、SortedSet、Bitmap、HyperLogLog、GEO 等数据结构。单线程
执行命令,每个命令串行执行,一个命令在执行,其他命令不会中途插进来,线程是安全的。基于内存的
,速度快。因为 Redis 会将数据都存在内存里,不像 MySQL 那样将数据都往磁盘里写。内存的读写速度相对于磁盘快很多。Redis 支持数据持久化
,定期将数据从内存持久化到磁盘,从而确保数据的安全性。Redis 支持主从集群
(主节点负责写,从节点负责读,读写分离,提高查询效率)和 分片集群
(将数据拆分,比如有 1TB 的数据拆成 n 份存到不同的节点上去,用很多台机器一起来存,存储的上限就提高了,实现水平的扩展。)Redis 速度快的原因主要有几点:
I/O 多路复用是指一个进程或者线程可以同时处理多个 IO 请求,是一种非阻塞的 IO 模型。
举个例子
假设我是奶茶店的店员,顾客们排队买奶茶。
阻塞 IO 模型:按顺序逐个点餐,先给 A 点,然后是 B、C、D … 这中间如果有一个人卡住,后面排队的人都会被耽误。
非阻塞的 IO 模型:谁先想好要点什么谁先说。这时 C、D 先说,表示他们想好要点什么了,然后我依次给 C、D 点单,然后继续等别的顾客。此时 E、A 又说要点什么,然后我去给 E、A 点单。
我觉得主要原因有 3 点:
Redis 持久化分为 RDB 和 AOF。
RDB 持久化在四种情况下会执行:
900秒内,如果至少有1个 key 被修改,则执行 bgsave , 如果是 save “” 则表示禁用RDB
save 900 1
save 300 10
save 60 10000
RDB 是每隔一段时间进行持久化,没法做到实时持久化。在这相隔的时间内如果 Redis 宕机,数据就会丢失。
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
随着命令越来越多,AOF 文件也会越来越大。为了解决这个问题,Redis 提供了 bgrewriteaof 命令,用最少的命令完成对 AOF 文件的重写。
如图,AOF 原本有三个命令,但是 set num 123 和 set num 666
都是对 num 的操作,第二次会覆盖第一次的值,因此第一个命令记录下来没有意义。
所以重写命令后,AOF 文件内容就是:mset name jack num 666
异地容灾
。Redis 保证高并发和高可用主要有三种方式:搭建主从集群、哨兵机制、分片集群。
单节点 Redis 的并发能力是有限的,为了提高 Redis 的并发能力,就需要搭建主从集群,实现读写分离。主节点负责写,并将数据同步给从节点,从节点负责读,一主多从,多个从节点一起分担读的压力,这样读并发能力就得到提升了。
主从数据同步分为全量同步
和增量同步
。
Redis 提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。
作用:
监控
整个集群,对集群进行 故障恢复
。如果主节点挂了,在从节点中重新选出一个作为主节点来保证集群可以正常运行。并且主从发生切换会 通知
Java 客户端,这样就知道新的主节点和新的从节点是谁了,这样就可以去修改节点访问的地址了。哨兵 通过 心跳机制
监测服务状态,每隔 1 秒 向集群的每个实例发送 ping 命令:
一旦发现 master 故障,哨兵 需要在 slave 中选择一个作为新的 master,选择依据是这样的:
当选出一个新的 master 后,该如何实现切换呢?
流程如下:
缓存穿透
是指请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,这些请求都会直接打到数据库。
解决缓存穿透的目的是为了防止有人恶意攻击,如果知道请求的路径,不断发送这样的请求,就会造成缓存穿透。
常见的解决方案有两种:
缓存空对象:请求的数据不存在,就把空值存到缓存里
布隆过滤器:在客户端和 Redis 之间加了一层布隆过滤器,如果发送请求的数据在数据库里有,就放行去访问 Redis,不存在,就拦截,拒绝访问。
布隆过滤器原理
如何实现布隆过滤器?
Redission 提供了对布隆过滤器的实现,可以设置一个误判率,一般是 0.05,也就是 5% 的误判率。
缓存雪崩
是指由于设置缓存时 不同的 key 采用了相同的过期时间,在同一时段大量的 key 同时失效或者 Redis 服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
1. 给不同的 key 的 TTL 添加随机值
2. 搭建 Redis 集群保证高可用
缓存击穿
问题也叫热点 Key 问题,就是一个被高并发访问并且缓存重建耗时长的 key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
①:在设置 key 的时候,将逻辑时间存入缓存中,不给当前 key 设置过期时间
②:当查询的时候,从 Redis 取出数据后判断逻辑时间是否过期
③:如果过期则开启新线程进行数据同步,当前线程正常返回数据,会将过期的数据返回
当然两种方案各有利弊:
如果需要数据的强一致性,建议使用锁的方案,但是性能没那么高,可能会产生死锁
如果需要性能比较高,则使用逻辑过期的方案,但是数据同步这块做不到强一致。
缓存更新策略
内存淘汰 | 超时剔除 | 主动更新 |
---|---|---|
不用自己维护,当内存不足时自动淘汰部分数据。下次查询时更新缓存。 | 给缓存数据添加 TTL 时间,到期后自动删除缓存。下次查询时更新缓存。 | 编写业务逻辑,先更新数据库,再删缓存。 |
业务场景
具体例子
项目中 ShopController 中给查询商铺的缓存添加超时剔除和主动更新的策略
更新商铺时,保证数据库和缓存的一致性
@Transactional
public Result update(Shop shop) {
Long id = shop.getId();
if(id == null){
return Result.fail("店铺id不能为空");
}
// 1.更新数据库
updateById(shop);
// 2.删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
return Result.ok();
}
可以通过 expire 命令给 Redis 的 key 设置 TTL(存活时间):
可以发现,当 key 的 TTL 到期以后,再次访问 name 返回的是 nil,说明这个 key 已经不存在了,对应的内存也得到释放。从而起到内存回收的目的。
这里有两个问题需要我们思考:
Redis 数据库中有两个字典分别记录 键值对 和 过期时间
不会立即删除。Redis 采用的过期数据的删除策略是 惰性删除
和 定期删除
惰性删除指的是每次访问(增删改查) key 时判断是否过期,如果过期就删除。
定期删除指的是每隔一段时间,就对一些 key 进行检查,删除里面过期的 key。
Redis 的过期删除策略:惰性删除 + 定期删除 两种策略进行配合使用。
定期删除的两种模式: SLOW 模式 和 FAST模式
Redis 提供了8种 内存淘汰策略
来选择要删除的 key,默认是 noeviction,不删除任何 key,内存不足时直接报错。
可以在 Redis 的配置文件中选择内存淘汰策略。最常使用的是 allkeys-lru
,当内存不足时,删除最近最少使用的 key (用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高)。
如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。
如何找到大 Key?
使用 Redis 的 --bigkeys 命令来查找
如何处理 大 Key?
使用 UNLINK 命令删除大 Key
在 Redisson 中,提供了 WatchDog 看门狗机制,一个线程获取锁成功以后,WatchDog 会给持有锁的线程 续期(默认是每隔10秒续期一次),就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要释放锁就可以了。
不能。企业中一般会搭建 Redis 主从集群架构,为了分担读的压力,Redis 通过主从集群架构,实现读写分离,主节点负责写,并将数据同步给其他从节点,从节点负责读,从而实现高并发。假如主节点还没来得及写,主节点挂了,Redis 提供的哨兵模式,会在从节点中选出新的主节点。新的线程也会尝试获取锁,因为之前数据没有同步过来,新的线程也会加锁成功。这时候就出现了 2 个线程同时持有一把锁的问题,如果业务还在执行,可能就会出现脏数据的现象。
如果业务非要保证数据的强一致性,该怎么解决呢?
如果有强一致性要求高的业务,建议使用 zookeeper 实现的分布式锁