06-Redis

六、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 seconds

2、通过set … nx命令将加锁、过期命令编排到一起,则此时就为原子操作可避免死锁

set key value nx ex seconds

3、如果进程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;
} 

版权声明:本文为xinjiao22222原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。