Redis是当前比较热门的NOSQL系统之一,它是一个开源的使用ANSI c语言编写的key-value存储系统(区别于MySQL的二维表格的形式存储。)。和Memcache类似,但很大程度补偿了Memcache的不足。和Memcache一样,Redis数据都是缓存在计算机内存中,不同的是,Memcache只能将数据缓存到内存中,无法自动定期写入硬盘,这就表示,一断电或重启,内存清空,数据丢失。所以Memcache的应用场景适用于缓存无需持久化的数据。而Redis不同的是它会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,实现数据的持久化.

Redis的机制

高性能

Redis最牛逼的地方就是快,为什么这么快呢,主要原因如下

  • 纯内存操作
  • 数据结构简单,对数据操作也简单
  • 单线程操作,避免了频繁的上下文切换
    单线程好处:1.代码更清晰,处理逻辑更简单;2.不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;3.不存在多进程或者多线程导致的切换而消耗CPU
    单线程劣势:无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;

  • 采用了非阻塞I/O多路复用机制
    IO多路复用是需要操作系统支持的,目前,提供的多路复用函数库主要有四下个方法select、poll、epoll、kqueue等
    名词比较绕口,理解涵义就好。从网上抄来的一个epoll场景:
    一个酒吧服务员(一个线程),前面趴了一群醉汉,突然一个吼一声“倒酒”(事件),你小跑过去给他倒一杯,然后随他去吧,突然又一个要倒酒,你又过去倒上,就这样一个服务员服务好多人,有时没人喝酒,服务员处于空闲状态,可以干点别的玩玩手机。至于epoll与select,poll的区别在于后两者的场景中醉汉不说话,你要挨个问要不要酒,没时间玩手机了。epoll是linux下内核支持的函数,而kqueue是其他系统支持的函数。io多路复用大概就是指这几个醉汉共用一个服务员。

高可用性

了解一个技术的时候,一定要自己不断的去思考,为什么会出现这个技术,这个技术解决了什么问题,以及怎样解决的。
Redis还有一个很牛逼的特性就是高可用性,Redis是如何做到的呢,主要原因如下

  • 支持持久化机制,用于解决机器故障导致内存数据丢失的问题
  • 支持主从复制,用于解决读取数据压力大问题
  • 支持哨兵机制,能够及时做到故障转移
  • 支持集群机制,能够扩充Redis可以存放的数据量
  • 支持过期策略以及内存淘汰机制,解决写入数据高于Redis总内存问题
  • 事务控制和分布式锁,保证并发场景下正常的使用Redis

下面依次分析这几个机制

持久化机制

持久化作用
Redis持久化就是按照一定的机制及时将内存里的数据同步至磁盘。
为什么要去支持持久化呢?
Redis之前比较流行的内存数据库是memcached,由于它的数据都存储到内存里了,就导致它有一个很致命的缺陷,就是一旦机制出现故障,所有数据就会丢失,这一点就一定决定了memcached的应用场景会非常受限,只能应用于一些非核心业务,可以接受数据丢失。
持久化机制

  • RDB
    可以在指定的时间间隔内生成数据集的时间点快照
  • AOF
    持久化记录服务器执行的所有写操作命令
  • 同时启用AOF和RDB
    当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。为了缓解这一问题,Redis支持Rewrite重写,简单的讲就是基于一定算法,给Aof文件“瘦身”。

优缺点分析

  • RDB Redis会单独启动一个进程,先把数据收集到一个临时文件,等待上一个持久化操作结束后,并且按照你配置的持久策略去把数据同步到磁盘,对主进程无影响,性能高,但是缺点是,一旦出现机器故障,放在缓存里的数据就会丢失。
    RDB策略配置示例如下(redis.conf)
    每900s有一次写操作触发同步,或者每300s有10次写操作触发同步,或者每60s有10000次操作触发同步

    1
    2
    3
    save 900 1
    save 300 10
    save 60 10000
  • AOF Redis会将自己所有执行过的更新指令记录下来,并且日志文件只允许尾部添加操作。恢复数据的时候,Redis会从头到尾重新执行一遍。
    Aof目前同步策略:Always(每步)Everysec(每一秒)No(操作系统决定)。
    开启aof配置,以及同步策略配置示例如下(redis.conf)
    开启aof,每秒同步一次写操作到磁盘

    1
    2
    appendonly yes 
    appendfsync everysec

这两种机制没有好坏之说,使用的时候,需要你去分析自己的实际业务场景,哪个适合用哪个!
参考链接:Redis持久化

主从复制

作用
Redis相对于传统的db,读写非常快的,但是,面对更大的读请求来临时,Redis提供了主从复制策略来解决读取数据压力大问题
结构
Redis 主从master-slave级联图

redis-master-slave

主从同步策略

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
开启从服务器配置(redis.conf)
将当前服务器配置成192.168.1.1 6379 这个节点的从服务器

1
slaveof 192.168.1.1 6379

参考链接:主从复制

哨兵

作用

如果Redis主节点跪了的话,如何来保证Redis系统可用性呢?Redis提供了哨兵机制,哨兵会去监控Redis节点,当发现Redis主节点跪了的时候,会通过投票机制,从从节点中挑取一个节点自动升级为主节点,自动实现故障转移,同事还会向管理员发送Redis节点异常通知。
结构
Redis 哨兵监控图(哨兵也可以多个)
redis-master-slave
哨兵原理介绍

首先理解两个名词

  • 主观下线:当前哨兵节点连接某一Redis节点失败;
  • 客观下线:当sentinel监视的某个服务主观下线后,sentinel会询问其它监视该服务的sentinel,看它们是否也认为该服务主观下线,接收到足够数量(这个值可以配置)的sentinel判断为主观下线,既任务该服务客观下线,并对其做故障转移操作。
    选举领头哨兵

    一个redis服务被判断为客观下线时,多个监视该服务的sentinel协商,选举一个领头sentinel,对该redis服务进行古战转移操作。选举领头sentinel会遵循一些规则,有兴趣可以自行调研。
    注:你可能会关心为啥选举领头哨兵,其实道理很简单,做故障迁移的时候,不可能每个哨兵节点都私自决定用哪个节点替代坏掉的主节点,
    会乱套的,公平的投出一个节点作为领头者,然后基于一定规则去做故障转移最快
    进行故障转移,分为三个主要步骤:
    1) 从下线的主服务的所有从服务里面挑选一个从服务,将其转成主服务;
    2) 已下线主服务的所有从服务改为复制新的主服务;
    3) 将已下线的主服务设置成新的主服务的从服务,当其回复正常时,复制新的主服务,变成新的主服务的从服务。
    参考链接:Redis哨兵

    集群

    作用
    一台Redis节点内存资源总是有限的,如果不够使的话,怎么能扩展吗?
    Redis是支持集群的,默认关闭。
    开启集群配置(redis.conf)

    1
    cluster-enabled yes #开启cluster

Redis集群架构
redis-cluster
图上能看到的信息:
1) 对象保存到Redis之前先经过CRC16哈希到一个指定的Node上,例如Object4最终Hash到了Node1上。
2) 每个Node被平均分配了一个Slot段,对应着0-16384,Slot不能重复也不能缺失,否则会导致对象重复存储或无法存储。
3) Node之间也互相监听,一旦有Node退出或者加入,会按照Slot为单位做数据的迁移。例如Node1如果掉线了,0-5640这些Slot将会平均分摊到Node2和Node3上,由于Node2和Node3本身维护的Slot还会在自己身上不会被重新分配,所以迁移过程中不会影响到5641-16384Slot段的使用。

简单总结下哈希Slot的优缺点:

  • 优点:将Redis的写操作分摊到了多个节点上,提高写的并发能力,扩容简单。
  • 缺点:每个Node承担着互相监听、高并发数据写入、高并发数据读出,工作任务繁重

延展性问题:redis为什么采用一致性hash,而没有采用传统的取模算法?
其实道理很简答,因为在集群环境下,新增节点或者节点失效是很常见的,如果采用取模的那种算法,每次新增或者删除节点时,所有与数据都可能需要重新调整位置,这是无法接受的!
结合redis集群和主从复制两种思想,可以得到Redis集群的最终形态

redis-cluster-final

想扩展并发读就添加Slaver,想扩展并发写就添加Master,想扩容也就是添加Master,任何一个Slaver或者几个Master挂了都不会是灾难性的故障。完美!
参考链接:Redis集群设计原理

内存数据管理方案

作用
这一部分对应的问题很常见,就是如果你的redis只能存5g数据,可是你写入了10g数据,那么内存里最终会保留哪5g数据的问题。
redis采用的是定期删除+惰性删除策略。

为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.

定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。

采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
在redis.conf中有一行配置示例(当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key)

1
maxmemory-policy volatile-lru

参考链接:分布式之redis复习精讲

Redis事务和分布式锁

Redis事务
核心操作:multi开启事务,exec执行事务,discard撤销事务,watch监控key是否改过、UNWATCH对应watch操作【multi和exec执行后会自动unwatch调监控队列里的所有key,如果想提前释放,可以用这个命令】

redis事务采用的是乐观锁,就是先处理,再真正去执行的时候去看有没有被修改过,没有的话,就去更新,否则报错。这个思想是不是有点似曾相识,是的,concurrenthashmap的set值时也是用的这个思想。还是那句话,虽然技术上感觉不搭边,但是思想上确实想通的。

Redis分布式锁
核心操作:setNX,理解这个操作基本上就理解了分布式锁。这个方法的意思就是去给一个key“赋值”,如果之前没人这样做过,返回值是1,否则返回0.

参考链接:Redis事务和分布式锁

Redis的缺点

Redis会是完美的程序吗?肯定不可能的,世界上就没有完美的东西。我们需要做的事情,分析清楚事物的主要矛盾,在实际开发中,不要犯这些错误就好了。

以下典型问题,重点参考了分布式之redis复习精讲

缓存和数据库双写一致性问题

问题说明
一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。答这个问题,先明白一个前提。就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。
解决方案
详细解决方案见分布式之数据库和缓存双写一致性方案解析

缓存雪崩问题

缓存击穿问题和缓存雪崩问题都是大项目中可能会遇到,小项目比较难遇到。
问题说明
黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常
解决方案
1) 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试
2) 采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
3) 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。

缓存击穿问题

问题说明
缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常
解决方案
1) 给缓存的失效时间,加上一个随机值,避免集体失效。
2) 使用互斥锁,但是该方案吞吐量明显下降了。
3) 双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点

  • I 从缓存A读数据库,有则直接返回
  • II A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程。
  • III 更新线程同时更新缓存A和缓存B。

缓存的并发竞争问题

问题说明
这个问题大致就是,同时有多个子系统去set一个key。这个时候要注意什么呢?大家思考过么。需要说明一下,提前百度了一下,发现答案基本都是推荐用redis事务机制。博主不推荐使用redis的事务机制。因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上。因此,redis的事务机制,十分鸡肋。
解决方案
1) 如果对这个key操作,不要求顺序
这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。
2) 如果对这个key操作,要求顺序
假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC.
期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳


参考链接:https://www.toutiao.com/i6688926152364392963/