Redis常⻅的数据类型
string
string 主要是⽤来存储字符串,底层是基于动态字符串sds 实现的,sds 通过动态调整⻓度来节省内存
应⽤场景
分布式session,分布式锁
常⽤指令
set key value 设置key value
get key 获取value
setnx key value 当key为空时设置value
setex key seconds value 设置key的value,以及几秒后过期
hash
hash 类似于 Map,底层采⽤两种⽅式来实现,当数据量较少并且元素占⽤内存少(⼩整数或短字符串)时,采⽤ ziplist(压缩列表),反之用hashtable,两种⽅式都是为了节省内存。
ziplist 是⼀个连续的内存空间,通过紧凑的存储来节省空间
hashtable 是基于dict(字典)实现,采⽤拉链法解决hash冲突
应⽤场景
实现购物⻋
常⽤指令
hset key field value 将hash中key中的字段field的值设为value
hget key field 获取key中field字段的值
hkeys 获取哈希表中的所有字段
hdel key field 删除指定字段
hvals key 获取哈希表中key的所有值
list
左边为链表头,右边为尾部。
list 是⼀个有序可重复集合,底层采⽤两种⽅式实现,当数据量较少并且元素占⽤
内存少(⼩整数或短字符串)时,采⽤ ziplist(压缩列表),反之采⽤ quicklist(快速列
表),两种⽅式都是为了节省内存。
ziplist 是⼀个连续的内存空间,通过紧凑的存储来节省空间
quicklist 是基于 ziplist 和 双向链表 实现的,可以在节省空间的同时保证⾼效增删
应⽤场景
栈(lpush + lpop)
队列(lpush + rpop)
阻塞队列(lpush + brpop)
发布订阅
常⽤指令
lpush key value1 value2... 头插
lpop key 头删
rpush key value 尾插
rpop key 尾删
lrange key start stop 获得start-stop的元素
llen key 返回链表长度
set
set 是⼀个⽆序不可重复集合,底层采⽤两种⽅式实现,当数据量较少且元素为整
数时,采⽤ intset(整数集合),反之采⽤ hashtable(哈希表),两种⽅式都是为了节
省内存
intset 是⼀个有序的整数数组,通过紧凑的存储来节省空间
hashtable 是基于dict(字典)实现,采⽤拉链法解决hash冲突
应⽤场景
抽奖(srandmember)
点赞收藏关注(sadd)
共同关注(sinter)
可能认识的⼈(sdiff)
常⽤指令
sadd key member1 member2...... 向集合key插入元素
scard key 返回集合元素个数
srem key member 删除集合的某个元素
smembers key 返回全部成员
sinter key1 key2 求交集
sunion key1 key2 求补集
sdiff key1 key2 差集
zset(sorted set)
zset 是⼀个有序不可重复集合,底层采⽤两种⽅式实现,当数据量较少并且元素
占⽤内存少(⼩整数或短字符串),采⽤ziplist(压缩列表),反之采⽤skiplist(跳表) +
dict(字典),两种⽅式都是为了节省内存,另外skiplist主要是为了提升score查询效
率
ziplist 是⼀个连续的内存空间,通过紧凑的存储来节省空间
skiplist 是⼀个有序链表配上多级索引,通过多级索引位置的跳转来实现快速
查找元素,主要⽤于按照分值对元素进⾏排序,同样也⽀持范围查询
跳表如何定位元素
每隔⼀个元素建⽴⼀个索引,通过建⽴多个索引,利⽤⼀次索引定
位到需要查询的元素,如果觉得慢,可以在⼀级索引的基础上建⽴⼆
级索引,依次类推,在多级索引之间来回转跳实现快速定位,当数据
量特别⼤的时候,查找时间复杂度为O(logN),因为它本身的思想就
类似⼆分查找
应⽤场景
排⾏榜(zrange/zreverange/zunionscore)
常⽤指令
zadd key score1 member1 score2 member2 向集合中插入元素
zrem key member 删除成员
zincrby key increment member 比如给zset1中的成员a的score加5 zincrby zset1 5 a
zrange key start stop[with scores] 获得start到stop的成员
通用命令
exist key 判断是否存在这个key
type key 获取key类型
del key 删除key
keys * 获取全部的key
bitmap
bitmap是⼀个位图
应⽤场景
月打卡、⽉活跃
月打卡可以通过将 当前第⼏天 作为 偏移量,如果打卡对应的位置为1,
反之为0
布隆过滤器
stream
stream 是参考kafka设计的消息队列,⽀持持久化,适合小基数的消息队列场景
JAVA spring Cache
Java spring Cache是一个上层框架,底层可以选用redis或者其他实现,可以通过注解方便的进行缓存数据的管理。
@EnableCaching:在启动类上加上这个注解,开启缓存注解功能
@Cacheable:Cache中存在数据则直接返回,如果Cache中没有则查数据库,并将数据缓存
@CacheEvict:删除一个或者多个缓存数据
@CachePut:添加一个缓存数据
@PostMapping
//@CachePut(value = "userCache",key = "#result.id")//result是方法返回值
@CachePut(value = "userCache",key = "#user.id")//key = userCache::key
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}
@DeleteMapping
@CacheEvict(cacheNames = "userCache",key = "#id")
public void deleteById(Long id){
userMapper.deleteById(id);
}
@DeleteMapping("/delAll")
@CacheEvict(cacheNames = "userCache",allEntries = true)//删除全部
public void deleteAll(){
userMapper.deleteAll();
}
@GetMapping
@Cacheable(cacheNames = "userCache",key = "#id")//key的生成 userCache::10
public User getById(Long id){
User user = userMapper.getById(id);
return user;
}
Redis主从复制和主从同步
单点Redis的并发能力是有上限的,要提高redis并发能力就要搭建redis主从集群。主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower) ; 数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。
主从同步:
- 根据Replication ID判断主从节点是否是同一数据集,如果不一致,说明从节点是第一次请求主节点的数据同步,主节点发送repid和offset给从节点;如果一致则不发送repid和offset,直接进入第三步
- 主节点生成RDB,并发送给从节点,从节点清空本地数据加载RDB文件
- 主节点记录RDB期间的所有命令,放在repl_baklog里面,并发送给从节点,从节点执行repl_baklog里面的命令和参考offset偏移量,来同步主从数据。
缓存穿透
一直查找不存在的数据,因为redis没有,所以会一直访问数据库,导致性能下降。
解决方法:
- redis缓存一个空数据
- 布隆过滤器过滤不存在的数据。布隆过滤器是一个数组(位图),当一个请求传来,将key进行hash操作,判断这个数据的对应位是否全为1;而设置布隆过滤器的时候,同样使用hash将这些位置全部置1。不过也有缺点,就是会产生误判,比如1和3是一直存在的数据,一个不存在的数据4经过hash之后指向了1和3hash的位置,导致误判4是存在的。
缓存击穿
当key过期的时候,这个时间恰好有大量的并发请求这个数据key,这个并发请求可能会压垮数据库。redis数据过期了,系统需要重建redis数据。
解决方法:
- 设置互斥锁,所有请求线程阻塞,当缓存重建完毕后释放锁才唤醒请求的线程,实现强一致性。
- 不设置实际过期时间,设置一个逻辑过期时间。当线程1数据过期后,线程1加上一个互斥锁,并且另外开一个线程来重建缓存数据,当重建完毕后才释放锁。但是当重建库的期间,线程1和其他请求线程会直接返回过期的数据,实现高可用性。
缓存预热如何实现?
使用定时任务,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。
缓存雪崩
某一时间,redis缓存中有大量数据过期,导致大量请求到数据库。
解决办法:
- 给不同key的TTL添加随机值
- 建立redis集群
- 给缓存业务添加降级限流策略
双写一致
强一致的:加共享锁和排他锁。共享锁加锁之后,其他线程可以读。排他锁加锁后,其他线程不能读写。
允许延迟一致的业务:MQ或者canal监听
Redis持久化
RDB:主进程fork另一个进程来读数据,并生成新的RDB文件。如果在读期间有其他进程要对数据写,则应该对数据做复制备份,让读写分离开,避免脏数据
AOF(Append Only File),redis处理的每一个命令都会记录在AOF文件中。但是命令写入AOF文件的频率有不同的策略。1.同步刷盘,每条命令都写2.每秒刷盘,数据写放在AOF缓冲区,每一秒写入到AOF文件中一次。缺点是可能比较冗余,因为如果命令频繁修改一个数据的值,显然只有最后一个写命令是有效的。
两者相比较:RDB在生成的时候任务是很繁重的,而AOF速度比较快;AOF文件比较RDB要大很多;恢复数据的时候RDB比AOF快很多。
AOF 为什么是在执行完命令之后记录日志?
- 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
- 在命令执行完之后再记录,不会阻塞当前的命令执行。
Redis后台线程有哪些?
- bio_close_file 后台线程来释放 AOF / RDB 等过程中产生的临时文件资源。
- 通过 bio_aof_fsync 后台线程调用 fsync 函数将系统内核缓冲区还未同步到到磁盘的数据强制刷到磁盘( AOF 文件)。
- 通过 bio_lazy_free后台线程释放大对象(已删除)占用的内存空间.
AOF 重写
当 AOF 变得太大时,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
由于 AOF 重写会进行大量的写入操作,为了避免对 Redis 正常处理命令请求造成影响,Redis 将 AOF 重写程序放到子进程里执行。AOF 文件重写期间,Redis 还会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。
redis数据过期策略
惰性删除:用到这个key的时候判断是否过期,过期则删除
定期删除:可以定时删除(比如10hz的频率),也可以在某一个时间间隔内删除(比如低于2ms)
应该两个方法配合使用,减少删除对cpu的影响
redis数据淘汰策略
redis淘汰策略主要可以分为两类,一个是淘汰过期数据还是所有数据,还有就是用什么算法去淘汰。算法可以分为:随机算法,LRU算法。我自己最常用的就是当redis内存不够的时候,LRU算法淘汰所有数据中的一部分数据。
redission实现的分布式锁
首先先介绍redis分布式锁。线程内部的锁不可以控制线程之间的行为,因此redis引入了分布式锁来控制线程间的行为,当一个线程申请了分布式锁,其他线程就不能申请进而阻塞。并且redis分布式锁存在一个很大的问题就是很难控制分布式锁的加锁时间,如果锁加的时间太长了如果服务宕机,这个锁就一直释放不出来;如果锁加的时间太短了,业务没做完就释放了明显不合适。所以,redission分布式锁加强了redis分布式锁的功能:
- 当一个线程加了redission分布式锁后,会fork一个进程监控这个锁,并定时对这个线程的分布式锁做续期
- 其他线程申请锁的话,会频繁执行加锁操作,直到成功
- redission分布式锁在同一个线程是可重入的(即可以多次申请锁,并记录申请这个锁的次数),不同线程就不可重入。
- 但是redission分布式锁不可以保证主从一致性。比如,某个应用要写数据,找到master节点并加了锁,在锁释放之前master节点宕机,此时哨兵会从slave节点中选举出一个新的master节点,而这个节点也可以申请加刚才那个锁,所以两个数据持有了同一把锁,而且刚刚写入的数据也没有保存。
- 底层基于lua脚本,保证原子性。
Redis事务
redis事务不支持原子性,持久性。而且不能回滚。
LUA脚本
Redis 通过 LUA 脚本创建具有原子性的命令: 当lua脚本命令正在运行的时候,不会有其他脚本或Redis 命令被执行,实现组合命令的原子操作,前提是LUA脚本不能出现错误。
lua脚本作用:
-
Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
-
Lua脚本可以将多条命令一次性打包,有效地减少网络开销
哨兵模式
哨兵是Redis的一种运行模式,它专注于对Redis实例(主节点、从节点)运行状态的监控,并能够在主节点发生故障时通过一系列的机制实现选主及主从切换,实现故障转移,确保整个Redis系统的可用性。结合Redis的官方文档,可以知道Redis哨兵具备的能力有如下几个:
监控(Monitoring):持续监控Redis主节点、从节点是否处于预期的工作状态。
通知(Notification):哨兵可以把Redis实例的运行故障信息通过API通知监控系统或者其他应用程序。
自动故障恢复(Automatic failover):当主节点运行故障时,哨兵会启动自动故障恢复流程:某个从节点会升级为主节点,其他从节点会使用新的主节点进行主从复制,通知客户端使用新的主节点进行。
主观宕机(Subjective Down, SDOWN):是指一个哨兵实例通过检测发现某个主节点发生故障的一种状态。
客观宕机(Objective Down, ODOWN):是指哨兵检测到某个主节点发生故障,通过命令SENTINEL is-master-down-by-addr与其他哨兵节点协商,并且在指定时间内接收到指定数量的其他哨兵的确认反馈时的一种状态。
简单来说,SDOWN是哨兵自己认为节点宕机,而ODOWN是不但哨兵自己认为节点宕机,而且该哨兵与其他节点沟通后,达到一定数量的哨兵都认为节点宕机了。
脑裂问题
redis的主从模式下脑裂是指因为网络问题,导致redis 主节点跟从节点和Sentinel集群处于不同的网络分区,此时因为Sentinel集群无法感知到 主节点的存在,就会将某一个从节点提升为主节点。此时就存在两个不同的主节点,就像一个大脑分裂成了两个。
集群脑裂问题中,如果客户端还在基于原来的主节点继续写入数据,那么新的主节点将无法同步这些数据,当网络问题解决之后,Sentinel 集群将原先的主节点降为从节点,此时再从新的主中同步数据,将会造成大量的数据丢失。
由于Redis使用异步复制,在这个场景中无法完全解决数据丢失的问题,但是可以设置最少从节点数量以及缩短主从数据同步的延迟时间,达不到要求会拒绝客户端的写入请求。
但是从故障发生到选项生效也需要一定的时间,这个过程中的数据丢失是无法避免的,依赖“Redis+Sentinel”的系统必须对数据的丢失有一定的容忍性,否则就需要采用支持强一致的系统了。
分片集群
单一的redis集群对于写操作负担是比较重的,并且写操作的能力跟集群的主节点明显相关,因此可以将集群分片,集群中存在多个主节点,每个主节点连有从节点。
- 每个主节点保存不同数据
- 主节点之间彼此检测健康状态
- 客户端可以访问集群中任意节点,最终都被转发到正确节点
那么如何找到这么多小集群中我想要的数据呢?
分片集群引入的插槽的概念,一个redis集群有16384个插槽,每个小集群应该平均分配整个集群中的插槽,将客户端传入的值进行哈希,找到对应插槽的小集群。
为什么redis是单线程的速度还是这么快?
1.redis都是基于内存的操作
2.可以避免多线程带来的上下文切换,避免不必要开销
3.IO多路复用提高网络性能,单个线程监听多个socket,并在socket可读可写时得到通知,避免无效等待。目前用的比较多的时epoll模型,当某个socket就绪后直接写入用户空间,而不要一个一个遍历socket查找是谁就绪了。
IO多路复用
IO多路复用技术(epoll函数模型):epoll函数模型主要是调用了三个函数:epoll_create() , epoll_ctl() , epoll_wait();
底层流程:
①通过epoll_create() 函数创建一个文件,返回一个文件描述符fd
② 创建socket接口号4,绑定socket号与端口号,监听事件,标记为非阻塞。通过epoll_ctl() 函数将该socket号以及需要监听的事件(如listen事件)写入fd中。
③循环调用epoll_wait() 函数进行监听,返回已经就绪事件序列的长度(返回0则说明无状态,大于0则说明有n个事件已就绪)。如果有客户端进行连接,则再调用accept()函数与4号socket进行连接,连接后返回一个新的socket号,且需要监听读事件,则再通过epoll_ctl()将新的socket号以及对应的事件(如read读事件)写入fd中,epoll_wait()进行监听。循环往复。
说说为什么Redis过期了为什么内存没释放?
- 当一个key在Redis中已经存在了,但是由于一些误操作使得key过期时间发生了改变,从而导致这个key在应该过期的时间内并没有过期,从而造成内存的占用。
- 一般Redis对过期key的处理策略有两种:惰性删除和定时删除。
先说惰性删除的情况,当一个key已经确定设置了xx秒过期同时中间也没有修改它,xx秒之后它确实已经过期了,但是惰性删除的策略它并不会马上删除这个key,而是当再次读写这个key时它才会去检查是否过期,如果过期了就会删除这个key。也就是说,惰性删除策略下,就算key过期了,也不会立刻释放内容,要等到下一次读写这个key才会删除key。
而定时删除会在一定时间内主动淘汰一部分已经过期的数据,默认的时间是每100ms过期一次。为定时删除策略每次只会淘汰一部分过期key,而不是所有的过期key,如果redis中数据比较多的话要是一次性全量删除对服务器的压力比较大,每一次只挑一批进行删除,所以很可能出现部分已经过期的key并没有及时的被清理掉,从而导致内存没有即时被释放。