六、Redis
1、Redis是什么?
**概念:**Redis本身是一个key-value类型的内存数据库,整个数据库是加载在内存中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。
redis的好处:
- 速度快:因为数据的存储是key-value形式,所以查询与操作的时间复杂度都是O(1)
- 支持多种类型:String、List、Set、ZSet、hash
- 支持是事务:操作都是原子性的,也就是说对数据的更改要么全部成功,要么全部失败
- 丰富的特性:可用于缓存、消息,按key设置过期时间,过期后将会自动删除
2、Redis可以做什么
- 缓存,实现分布式缓存的首先中间件
- 数据库,实现点赞、排行等对性能要求高的需求
- 分布式锁
3、Redis的五大类型
- String:字符型
- 阅读数与点赞数量也是通过incr命令实现
-单个: 设置:set key value 获取:get key -多个: 设置:mset k1 v1 k2 v2 获取:mget k1 k2 -递增数字: incr key -增加指定数字: incrby key increment -递减数值: decr key -递减指定数值: decrby key decrement -获取字符串长度: strlen key -分布式锁: set key value [Ex seconds][PX milliseconds][NX][XX] EX:key 在多少秒之后过期 PX:key 在多少毫秒之后过期 NX:当 key 不存在的时候,才创建 key,效果等同于setnx key value XX:当 key 存在的时候,覆盖 key
- List:双向链表,能存储2^32 - 1个数据
- 微博粉丝列表、关注人列表,通过LRange key 0 -1获取所有元素
-添加元素 & 查看列表: LPUSH key value [value ...] RPUSH key value [value ....] LRANGE key start stop LRANGE key 0 -1 表示查看 list 中所有的元素 -删除元素: LPOP key RPOP key -获取列表中元素的个数 LLEN key
- Set:
- 抽奖
- 添加用户:sadd key useId
- 显示已经有多少人参与了抽奖:
SCARD key- 抽奖(从set中任意选取N个中奖人):spop key 【个数】
- 共同关注的好友
- 集合运算:sinter
-添加元素 & 删除元素 & 查看元素: 添加:SADD key member[member ...] 删除:SREM key member [member ...] 获取:SMEMBERS key -判断元素是否在集合中 SISMEMBER key member -获取集合中的元素个数 SCARD key -从集合中随机弹出几个元素 不删除:SRANDMEMBER key [数字] 删几个:SPOP key[数字] -集合运算: SDIFF key [key ...] SINTER key [key ...] SUNION key [key ...]
- **ZSet:**有序的set,可以通过额外提供一个优先级参数来为成员排序,
- 热搜
-添加元素 & 删除元素 & 获取元素 添加一个带分数(权值)的元素:ZADD key score member [score member ...] 删除 zset 中的指定元素:ZREM key member [member ...] 返回索引从start到stop之间的所有元素,并按照元素分数从小到大的顺序:ZRANGE key start stop [WITHSCORES],注:如果想要获取所有元素并且从小到大排序,可写为 ZRANGE key 0 -1 -获取元素的分数: ZSCORE key member
- hash:的结构类似于java的hashmap<String,HashMap<>()>();
- 用户信息,用普通的键值对来存储的话,以id为key,其他的信息封装成序列化进行储存,增加了序列化与反序列化 的开销或则是以一个属性名对应一个属性值,这样也会浪费内存空间
- 所以用hash类型存储,用户id作为key,其内部值还是一个hashmap类似的结构,所以再以属性名为key,最后对应的值为value
-单个: 设置:hset key field value 获取:hget key field -多个: 设置:hmset key field1 v1 field2 v2 获取:hmget key field1 field2 -获取所有字段: HGETALL key -获取某个key的全部数量: HLEN key -判断是否存在: HEXIT key field -删除某个key: HDEL key -递增数值: incrby key field increment -递减数值: decr key
4、为什么 Redis 是单线程的都那么快
1)大部分操作是在内存上完成的;
2)单线程减少上下文切换,同时保证原子性;
3)IO多路复用,使得redis在网络IO操作中能并发处理大量请求,实现高吞吐量;
5、Redis能否实现数据持久化,如何实现?
可以,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当redis重启后把硬盘文件重新加载到内存,就能实现数据恢复的目的。两种方式:RDB和AOF
RDB的持久化原理:在指定的时间间隔内将内存中的数据集快照写入磁盘。通过bgsave命令触发,然后父进程执行fork操作创建子进程,子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换;
手动的save指令,会让redis阻塞,直到rdb文件创建完成。
优点:
- RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复
- 生成RDB文件时支持异步处理,主进程不需要进行任何磁盘IO操作,最大化redis的性能
- RDB在恢复大数据集时的速度比AOF的恢复速度要快
缺点:
- 需要经常fork子进程来保存数据集到硬盘上,属于重量级操作,不适合频繁使用
- 非实时持久化
AOF的持久化原理:Redis每执行一个修改数据命令,都会把这个命令添加到AOF文件中。当服务器重启时会重新执行这些命令来恢复数据。默认不开启,修改配置启用:appendonly yes
优点:
- 可以实时持久化
- 与RDB方式可能会丢失大量数据相比,AOF的安全性高。
缺点:
- AOF文件体积会逐渐变大,且AOF的体积通常要大于RDB,且在恢复数据时慢于RDB
RDB-AOF混合模式:
- 根据数据库当前状态生成相应的RDB数据,写入至AOF文件中
- 对重写之后redis命令,则是以追加的方式加载到AOF文件的末尾
6、如何理解Redis是单线程但是又能fork出一个子进程
Redis单线程主要是指Redis的网络IO和键值对读取是由一个线程来完成的。而Redis的持久化、异步删除、集群数据同步等则是依赖其他线程完成的。所以Redis单线程只是习惯说法,其底层不是单线程。
7、Redis过期策略
1、定时删除:
概念:对一个key设置过期时间,当时间截至,立即对key进行删除
优点:对内存友好,保证一个key过期了立即就能删除;
缺点:不采用:当需要删除的key较多时,会占用一部分CPU的时间,影响服务器的响应时间与吞吐量
2、惰性删除:
概念:当一个key过期时,并不会立即从内存中删除,而是在使用key时,先检查key是否过期,过期则从内存中删除
优点:对CPU友好,不会浪费时间进行过期检查
缺点:对内存不友好,key过期了,却一直占用内存
3、定期删除:
概念:每隔一段时间,随机抽取设置了过期时间的key进行检查,删除过期的key
优点:可以通过限制删除操作执行的频率减少删除操作对CPU的影响,也能有效地释放过期键占用 的内存
缺点:难以确定删除操作执行的时长和频率,可能会导致很多key到时间了却没有被删除
8、内存淘汰策略
淘汰策略是当redis内存占用过高时,删除一部分key,保证redis的内存占用率不会过高。
- volatile(设置过期时间的数据集)
- volatile-LRU:从已设置过期时间的数据集中挑选最近最少使用的淘汰
- volatile-LFU:从已设置过期时间的数据集中挑选使用频率最低的数据进行淘汰
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
- volatile-TTL:从已设置过期时间的数据集中挑选将要过期的数据进行淘汰
- allkeys(所有数据集)
- allkeys-LRU:从数据集中挑选最近最少使用的数据淘汰
- allkeys-LFU:从数据集中挑选使用频率最低的数据淘汰
- allkeys-random:从数据集中任意选择进行淘汰
9、redis高可用
实现redis的高可用主要包括哨兵模式与集群模式
- 主从模式:
概念:一个master对应多个slave,主节点负责对数据的写操作,从节点负责对数据的读操作
优点:可以实现读写分离,主节点的数据会自动赋值到从节点,分担主节点的压力
缺点:当主节点宕机了,会导致部分数据位同步。无论主节点还是子节点宕机都需要重启后才能使用
- 哨兵模式sentiment:
概念:哨兵模式是一种分布式架构,包含若干哨兵节点和数据节点。每个哨兵节点都会对数据节点与其余哨兵节点进行监控,发现节点不可达时会对节点做下线标识。若被标示的是主节点,它就会与其他哨兵进行协商,当多数哨兵认为主节点不可达时,就会选举出一个哨兵完成自动故障转移的工作,同时把这个变化通知给应用方。整个过程都是自动完成的,不需要人工介入,有效地解决了redis的高可用问题。
哨兵节点特点:
1、监控:定期监控数据节点与其他哨兵节点是否可达
2、通知:将故障转移的结果通知给应用方
3、主节点故障转移:实现从节点晋升为主节点并维护后续正确的主从关系
4、配置提供者:在哨兵模式下,客户端在初始化时连接的时哨兵节点集合,从中获取主节点信息
5、防止误判:节点的故障判断是由多个哨兵节点共同完成
6、健壮性:哨兵节点集合是由多个哨兵节点组成,即使个别哨兵节点不可用,整个集合依然是健壮的
7、哨兵节点也是独立的Redis节点,但是他不存储数据,只支持部分命令
- 集群模式:(数据保持一致)
概念:Redis 集群实现的是数据的分布式存储。把所有的键依据哈希函数映射到0-16383整数槽以内(CRC16(key)&16383),每个节点负责维护一部分槽与槽所映射的键值数据。
特点:
1、解耦数据与节点之间的关系,简化了节点扩容与收缩的难度
2、节点自身维护槽的映射关系,不需要客户算或代理服务维护槽分区元数据
3、支持节点、槽、键之间的映射查询
10、缓存雪崩、缓存穿透、缓存击穿、缓存预热、缓存更新与缓存降级等问题
- 缓存雪崩:
概念:由于大量的key集中在一段时间内失效或Redis实例发生故障宕机,所有原本应该访问缓存的请求都去查询数据库,对数据库CPU和内存造成巨大压力,严重会造成数据库的宕机,从而形成一系列的连锁反应,造成整个系统崩溃。
解决办法:
1、大量key同一段时间失效:差异化设置过期时间:在创建key的时候,不要让大量的key在同一时间失效。比如过期时间添加一个较小的而随机数,让过期时间不同但差别又不是很大,既避免了大量数据同时过期又保证了数据在相近的时间失效。
2、redis实例故障宕机:(1)服务熔断:暂停业务应用对缓存服务的访问;(2)限流:控制每秒进入应用的请求数(3)使用redis集群,个别节点宕机,依然可以保持服务的整体可用
- 缓存穿透:
概念:用户所查询的数据缓存与数据库中都没有,导致用户每次查询时缓存中没有,然后去数据库中再查询一遍,缓存并没有起到分担数据库访问压力的作用。
解决办法:
1、若查询数据在数据库中未找到,则直接设置一个特定的值到缓存中。每次访问时,直接返回缓存中这个特定的值,就不需要继续访问数据库了。
2、把已存在的数据存放到布隆过滤器。当有新请求来时,先在布隆过滤器查询是否存在,若不存在该条数据直接拦截。
- 缓存击穿:
概念:一份热点数据,访问量极大。在缓存失效的瞬间,大量的请求达到数据库上,导致系统崩溃。
解决办法:
1、永不过期,热点数据不设置过期时间
2、加互斥锁:对数据的访问加互斥锁,当一个请求访问该数据时,其他线程只能等待。请求访问后,又把改数据加载到缓存中,后续访问就通过缓存就好了。
- 缓存预热:
现象:服务器启动后迅速宕机
问题:
1、当系统启动时,请求数量较高,大量的请求过来之后都需要去从缓存中找数据,发现缓存没有,又去数据库中进行查询,然后再存储于缓存,造成了短期内对redis的高强度操作从而导致问题
概念:系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,再把数据放到缓存中。从而实现用户直接查询事先被预热的缓存数据。
- 缓存更新:
被动更新:redis的内存淘汰机制
主动更新:
操作缓存和数据库时要考虑的三个问题:
1、删除缓存还是更新缓存?
- 更新缓存,每次更新数据库都要更新缓存,无效写操作较多
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存
3、先操作缓存还是先操作数据库
- **先删缓存后写DB可能会出现脏读的情况:**比如两个并发的操作一个更新一个读取,更新操作先把缓存删除后,读取操作没有找到缓存,转而向数据库中读取旧数据并存放到缓存中,然后更新操作更新了数据,但此时缓存中的数据任然是旧数据,导致读取的数据一直是旧的。
- **先写DB后删缓存:**出现脏读的可能较小,但是会出现一致性的问题:若两个并发的读取与更新操作,更新操作还没结束,则通过缓存查询到旧数据,但不会影响后续的查询。
- **解决:**缓存设置过期时间,实现最终的一致性。
- 缓存降级:
问题:当访问量剧增、服务出现问题或非核心服务影响到核心流程的性能时,任然需要保证主服务还是可用的,即使是有损服务。此时将其他次要访问的数据进行降级缓存,从而提高主服务的稳定性。(双十一的时候购物车无法修改地址,此服务被降级了,为了保证订单的正常提交和付款)
降级可以参考日志级别设置预案。
11、memcache与redis的区别
1、存储方式不同:memcache是把数据全部存储于内存中,断电之后会挂掉,且数据不能超过内存大小;Redis有部分存放在硬盘上,redis可以持久化数据
2、数据支持的类型:memcache只支持字符串类型;redis支持:string、list、set、zset、hash等数据结构的存储
3、redis的速度比memcache快许多
12、布隆过滤器的理解
概念:布隆过滤器包括两个核心部分,大型的位数组与若干个不同的哈希函数。布隆过滤器可用于检查一个元素是否存在于集合中。优点是:空间效率和查询时间比一般算法要好的多;缺点是:有一定的误识别率和删除困难。
工作原理:
1、添加key时,依据多个哈希函数计算出多个hash值,在由多个hash值计算出多个位置,把这些位置上的数据设置为1
2、查询key时,还是由hash函数得到多个hash值,并计算得到多个位置信息,如果其中有一个位置上的数据为0,则不存这个key,如果都是则存在(误判)。
13、Redis事务
Redis事务是通过MULTI、EXEC、Discard、watch四个原语实现
1、首先Multi命令开启一个事务,客户端可以继续向服务器发送任意多条命令,这些命令不会被执行,而是存放到一个队列中。
2、当调用exec命令时,所有队列中的命令则按照先后顺序进行执行,当一个命令被打断时,返回空值。
3、使用discard命令可以清空事务队列,并放弃执行事务,客户端从事务状态退出。
4、watch命令可以提供一个或多个键的cas操作,确保数据没有被修改才执行该事务,若key发生了变化,则服务器将拒绝执行客户端提交的事务,并返回一个空置。watch指令是一种乐观锁的机制。
14、Redis实现分布式锁
加锁:redis时单线程模式,可以使用setnx命令实现分布式锁。setnx命令返回1表示设置成功,若返回0表示设置失败。
解锁:通过调用del key删除key来解锁
解决死锁:
1、可以通过expire指令给key,在创建时设置一个过期时间,但两个命令不是原子的的,可能第二部会失败,任有死锁的问题
setnx key value expire key seconds2、通过set … nx命令将加锁、过期命令编排到一起,则此时就为原子操作可避免死锁
set key value nx ex seconds3、如果进程A的任务还未结束,锁已经到期被删除,等进程A结束后,他会尝试释放锁,此时释放的锁可能就是别的进程的锁。
1、加锁时给key设置一个标志,进程需要记住当前标志,在解锁时,要判断是自己持有的锁才进行释放。标志可以使用随机数充当。 2、解锁时需要先判断后释放,这两步也需要保证原子性,否则第二部失败会出现死锁。 2.1:采用LUA脚本,把两个命令编排到一起。 # 加锁 set key random-value nx ex seconds # 解锁 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end 2.2:利用redis自身的事务: 首先监控当前key; 判断保存的标识与key对应的value是否一致,不一致就直接解除watch 如果一致mutil开启事务,输入del命令,然后exec执行,若返回为空则循环判断当前value与保存的标识是否一致。 若返回不为空跳出循环。 while(true){ stringRedisTemplate.watch(REDIS_LOCK); if(stringRedisTemplate.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)){ stringRedisTemplate.setEnableTransactionSupport(true); stringRedisTemplate.multi(); stringRedisTemplate.delete(REDIS_LOCK); List<Object> list = stringRedisTemplate.exec(); if (list == null) { continue; } } stringRedisTemplate.unwatch(); break; }