这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
Redis数据结构
1. String
字符串类型,是可以修改的字符串,类似于StringBuilder,采用预分配冗余空间的方式来减少内存的频繁分配,一般实际分配的空间 capacity 要高于字符串的实际长度 len。
2. list 列表
底层采用链表结构,插入和删除的时间复杂度为 O(1)。可以通过 list 的 lpop 和 rpop 操作实现队列和栈。
list 常用来做异步消息队列,将需要延后处理的任务序列化成字符串存到redis列表中,另一个线程从这个列表中轮询数据进行处理。
3. hash 散列表
也称“字典”。与 Java 中的 HashMap 类似,底层以数组+链表的结构的存储,发生冲突时,将元素以链表的方式连接。
与 HashMap 不同的是,Redis 中的 hash 结构采用的是渐进式rehash,渐进式rehash 会在 rehash 的同时,保留新旧两个 hash 结构,渐进的将旧hash 迁移到 新hash 中,查询时会同时查询两个 hash 结构(以空间换效率)。如果是写操作,会直接写进新hash中。
常用来存储对象,其中的一个个key就可以看作对象的field。
存储对象还可以采用String+JSON的方式,序列化比Hash容易,但数据修改麻烦。另外,hash 可以对对象的单个字段进行存储,获取对象信息时可以部分获取。
4. set 集合
set 类似于 Java 中的 HashSet,集合内的value值不重复。底层结构是一个特殊的字典hash,value都为 null。
可以做到全局去重。如果使用JVM自带的Set来实现全局去重,还要再起一个服务,太麻烦。
可以用来存储活动中奖用户的ID,因为有去重功能,所以能够保证同一个用户不会中奖两次。
5. zset 有序集合
首先,zset是一个 set,value值不重复。其次,zset给每个value都赋予一个score,通过 score 进行排序。
zset底层使用散列表+跳跃链表来存储。
zset 可以用来存粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按关注时间进行排序。还可以做排行榜功能,取TOP N。
Redis的集群以及主从特性
Redis集群的各个节点之间可通过ping命令测试连接是否正常,当客户端连接到任何一个节点进行操作时,都可能将操作转发到集群中的其它节点。
集群中的节点可以作为主机,与多台redis从机建立主从关系。
主机和从机上的数据保持实时同步,主机发生增删改操作时,会将数据同步到从机中。主机可以读写操作,从机只能读,并接收来自主机的数据。
Redis的持久化方式
1. RDB
RDB是redis默认的持久化方式。RDB是将数据以二进制的方式写进文件,是在某个时间点将数据写入某个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件。使用单独的子进程来进行持久化,保证了redis的高性能。
2. AOF
将“操作+数据”以字符串的方式追加到文件的尾部。当Redis重启时会通过执行文件中的写命令来重建整个数据库的数据。
Redis 为什么快?
1. 特殊的数据结构
String 类型的数据空间不够时,会申请更多的内存。多余的空间不会返还给系统,通过这种方式减少内存的申请次数。(空间换时间)
渐进式 Rehash。(空间换时间)
ZSet 跳跃表通过层实现节点跳跃,达到加速访问的目的。
2. 内存型数据库
Redis 的操作都是基于内存的。(除持久化)
3. 单线程
单线程相比于多线程的优点在于锁和上下文切换。所以 Redis 的性能瓶颈不在CPU,而在于内存。
4. IO 多路复用
当一个连接阻塞时,线程会去处理其他连接。通过IO多路复用的方式,监听多个套接字,来处理多个客户端请求的,保证系统的高吞吐量。
缓存雪崩、缓存穿透、缓存预热
1. 缓存雪崩
缓存雪崩是指在一段时间内,大量的缓存到期,访问这些缓存的请求就回去访问数据库,给数据库造成巨大压力。严重时数据库容易宕机。
解决方法:
a. 通过加锁或用队列的方式对访问数据库的线程进行限制。如果采用加锁的方式,集群使用分布式锁,单机可以使用线程锁。但加锁并不能提高系统的吞吐量,假如有1000个线程同时访问,但同一时间只能有一个线程正在访问,效率很低。
b. 不同的key,设置不同的过期时间,让过期时间尽量均匀一些。
c. 使用消息中间件
还有一个概念跟缓存雪崩很像:缓存击穿。缓存击穿是指大量请求都指向redis的同一个缓存,当这个缓存失效时,这些请求就全都去访问数据库。缓存击穿和缓存雪崩的区别:缓存击穿的对象的是一个缓存,缓存雪崩的对象是大量缓存。
2. 缓存穿透
用户要访问的数据在数据库中没有,在Redis中自然也不会有,导致每次缓存中找不到,都要去数据库中再查询一遍。用户每次查询都会查询两遍,给数据库造成压力。
解决方法:采用布隆过滤器,将所有可能存在的数据哈希到散列表中,一个一定不存在数据会被拦截掉,避免请求访问数据库。(原理:通过Hash函数将数据映射成位阵列中的点,只要看看这个点是不是1就知道有没有这个数据)
3. 缓存预热
系统上线后,将相关的缓存数据直接加载到Redis。用户的请求会直接访问被加载额数据。
Redis的过期策略和内存淘汰机制
redis采用的是定期删除+惰性删除策略。
为什么不用定时删除?
定时删除,也可以说是实时删除,用一个定时器监控key,过期则删除。虽然即使释放了内存空间,但十分消耗CPU资源。
定期删除,redis默认每100ms检查是否有key过期。不是每隔100ms便将所有key检查一遍,而是随机抽取进行检查。因此,如果只采用定期删除策略,会导致很多key到时间后没有被删除。
惰性删除,在获取某个key时,redis会检查这个key是否过期,如果过期了此时就会被删除。
定期删除+惰性删除能够做的一劳永逸吗?
如果有些key过期后,在定期删除时不会被随机检查到,而且也几乎不会被访问,这些key就会一直存在。
redis的解决方案:内存淘汰机制
在redis.conf中有一行配置:
maxmemory-policy volatile-lru
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
allkeys-random:从数据集中任意选择数据淘汰
管道 Pipeline
Redis分布式锁
分布式锁是控制分布式系统不同进程间访问共享资源的一种方式。多用于解决分布式系统下的多进程并发问题。
情景:线程A和线程B都共享某个变量X
如果是单机情况下(单JVM进程),线程之间共享内存,使用线程锁就可以解决并发问题。
如果是分布式情况下(多JVM进程),线程A和B可能不在同一JVM进程中,线程锁便无法起到作用,需要使用分布式锁来解决进程间的冲突问题。
redis实现分布式锁的关键:
排他性:在同一时间只能有一个客户端获取到锁,其他客户端无法获取
避免死锁: 锁信息必须是会过期的,不能让一个线程一直占有锁而导致死锁。
除了锁自动过期之外,还可以解锁,加锁和解锁必须是同一线程。
加锁:
Redis实现分布式锁,主要是依赖redis自身的原子操作:
SET user_key user_value NX PX 100
NX:只在在键不存在时,才对键进行设置操作,SET key value NX 效果等同于 SETNX key value。
PX 100:设置键的过期时间为100毫秒。
当某个进程设置成功之后,就可以去执行业务逻辑了,等业务逻辑执行完毕之后,再去进行解锁。
解锁:
解锁很简单,只需要删除这个key就可以了,不过删除之前需要判断,这个key对应的value是当初自己设置的value。加判断的话,会导致这段操作不是原子性操作,如果中途由于某些意外而中断,解锁的操作便得不到执行。这就需要使用 Lua 脚本来处理,因为 Lua 脚本可以保证多个连续指令的原子性操作。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
如果一个锁过期但线程业务还没执行完,怎么办?
可以创建一个守护线程,一直为锁续期。如果主线程没执行完,守护线程也不会死掉,直到主线程执行完毕。