Java面试准备-缓存使用

问题链接转载  Java面试通关要点汇总集【终极版】

一、Redis 有哪些类型

  • string类型:string为最简单类型,一个key对应一个value
set mykey "wangzai"   ##设置key,如果该key存在会被覆盖
setnx mykey "wangzai" ##如果mykey存在则不改变,如果不存在则创建赋值
get mykey             ##获取key值
setex key1 10 1       ##给key1设置过期时间为10s,值为1
mset key1 value1 key2 value2    ##设置多个key
mget key1 key2        ##获取多个key值 
  • list类型:list是一个链表结构,主要功能是push,pop以及获取一个范围的所有值等。使用list结构可以轻松实现最新消息排行,另一个应用是消息队列,可以利用list的push操作,将任务存在list中,然后工作线程再用pop操作将任务取出进行执行(先进后出)
lpush list1 "wangzai"       ##在列表中加入一个元素
lrange list1 0 -1           ##查看list1里面的所有元素
lpop list1                  ##取出list1最新的元素
linsert list1 before "wangzai" "doubi"     ##在值为"wangzai"的前面插入一个元素为"doubi"
lset list1 3 "hehe"         ##把第五个元素修改为"hehe"
lindex list1 0              ##查看第一个元素
llen list1                  ##查看列表中有多少个元素
  • set类型:set是集合,对集合操作有添加删除元素,有对多个集合求交并差等操作。在微博应用中,可以将一个用户关注的所有人放在一个集合里,将所有粉丝放在一个集合里,因为redis为集合提供了求交集,并集,差集等操作,就可以方便地实现如共同关注,共同喜好等功能
sadd set1 a b c d         ##创建集合set1并设置值
smembers set1             ##查看集合set1的值
srem set1 a b             ##删除set1的值
spop set1                 ##随机取出一个元素并删除
sinter set1 set2          ##交集
sinterstore set1 set2 set3   ##把交集存储到set3
sunion set1 set3          ##并集
sunionstore set1 set2 set3     ##把并集存储到set3
sdiff set1 set2           ##差集
sdiffstore set1 set2 set3  ##把差集存储到set3
sismember set1 c           ##判断元素c是否属于集合set1
srandmember set1           ##随机取出一个元素,但不删除
  • sorted set类型:sorted set是有序集合,比set多一个权重参数score,使得集合元素能够按score进行有序排列
##存储一个班级同学的成绩,其集合value可以是学员学号,而score是考试得分

zadd zset1 1 a      ##增加一个集合zset1 , score为1,member为a
zrange zset1 0 -1   ##按score升序输出member
zrange zset1 0 -1 withscores   ##带上score
zrem zset1 a        ##删除指定元素
zrank zset1 a       ##返回元素的索引值,索引从0开始
zreverange zset1 0 -1   ##score降序输出member
zcard zset1         ##返回集合中所有元素的个数
zcount zset1 1 10   ##返回分值范围1 - 10的元素个数
zrangebyscore zset1 1 10   ##返回分值范围1-10的元素
zremrangebyscore zset1 1 10   ##删除分值范围1-10的元素
  • hash类型:把一些结构化的信息打包成hashmap
hset hash1 name wangzai        ##建立hash(hset name key value)
hget hash1 name                ##获取field值 HGET name key
hgetall hash1                  ##获取hash1中所有key和value
hmset hash2 name wangzai age 26 job it    ##批量建立键值对
hmget hash2 name age job       ##批量获取field值
hdel hash2 job                 ##删除指定field
hkeys hash2                    ##打印所有的key
hvals hash2                    ##打印所有的value
hlen hash2                     ##查看hash2有几个field

二、Redis 内部结构

Java面试准备-缓存使用_第1张图片

各功能模块说明如下

  • File Event:处理文件事件(在多个客户端中实现多路复用,接受它们发来的命令请求),即读事件;并将命令的执行结果返回给客户端,即写事件
  • Time Event:时间事件(更新统计信息,清理过期数据,附属节点同步,定期持久化等)
  • AOF:命令日志的数据持久化
  • RDB:实际的数据持久化
  • Lua Environment:Lua脚本的运行环境,为了让Lua环境符合Redis脚本功能的需求,Redis对Lua环境进行了一系列的修改,包括修改函数库,更换随机函数,保护全局变量等等
  • Command table(命令表):在执行命令时,根据字符来查找相应命令的实现函数
  • Share Objects(对象共享):

主要存储常见的值:

  1. 各种命令常见的返回值,例如返回值OK,ERROR,WRONGTYPE等字符
  2. 小于redis.h/REDIS_SHARED_INTEGERS(默认1000)的所有整数。通过预分配的一些常见的值对象,并在多个数据结构间共享对象,即这些常见的值在内存中只有一份
  • Databases:Redis数据库是真正存储数据的地方。当然数据库本身也是存储在内存中。Databases的数据结构伪代码如下

typedef struct redisDb{

    //保存着数据库以整数表示的号码
    int id;

    //保存着数据库中的所有键值对数据
    //这个属性也被称为键空间
    dict *dict

    //保存着键的过期信息
    dict *expires;
    
    //实现列表阻塞原语,如BLPOP
    dict *blocking_keys;
    dict *ready_keys;

    //用于实现WATCH命令
    dict *watched_keys;
} redisDb;

Java面试准备-缓存使用_第2张图片

Database的内容要点包括:

  1. 数据库主要由dict和expires两个字典构成,其中dict保存键值对,而expires则保存键的过期时间
  2. 数据库的键总是一个字符串对象,而值可以是任意一种Redis数据类型,包括字符串,哈希,集合,列表和有序集
  3. expires的某个键和dict的某个键共同指向同一个字符串对象,而expires键的值则是该键以毫秒计算的UNIX过期时间戳
  4. Redis使用惰性删除和定期删除两种策略来删除过期的键
  5. 更新后的RDB文件和重写后的AOF文件都不会保留过期的键
  6. 当一个过期键被删除后,程序会追加一条新的DEL命令到现有AOF文件末尾
  7. 当主节点删除一个过期键后,它会显式地发送一条DEL命令到所有附属节点
  8. 附属节点即使发现过期键,也不会主动删除它,而是等待主节点发来DEL命令,这样保持主节点和附属节点的数据一致

数据库的dict字典和expires字典的扩展策略和普通字典一样,它们的收缩策略是:当节点的填充百分比不足10%时,将可用节点数量减少至大于等于当前已用节点数量。

三、Redis 内存淘汰机制

redis内存数据集大小上升到一定大小时就会进行数据淘汰策略

  • 如何配置

通过配置redis.conf中的maxmemory这个值来开启内存淘汰功能

#maxmemory

值得注意的是,maxmemory为0时表示我们对Redis的内存使用没有限制

根据应用场景,选择淘汰策略

# maxmemory-policy noeviction

内存淘汰过程:

  1. 首先,客户端发起了需要申请更多内存的命令(如set)
  2. 然后,Redis检查内存使用情况,如果已使用的内存大于maxmemory则开始根据用户配置的不同淘汰策略来淘汰内存(key),从而换取一定的内存
  3. 最后,这个命令执行成功
  • 动态配置命令

此外,redis支持动态改配置,无需重启

#设置最大内存
config set maxmemory 100000

#设置淘汰策略
config set maxmemory-policy noeviction
  • 内存淘汰策略

volatile-lru

从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。redis并不是保证取得所有数据集中最近最少使用的键值对,而只是随机挑选的几个键值对中的, 当内存达到限制的时候无法写入非过期时间的数据集。

volatile-ttl

从已设置过期时间的数据集中挑选将要过期的数据淘汰。redis 并不是保证取得所有数据集中最近将要过期的键值对,而只是随机挑选的几个键值对中的, 当内存达到限制的时候无法写入非过期时间的数据集。

volatile-random

从已设置过期时间的数据集中任意选择数据淘汰。当内存达到限制的时候无法写入非过期时间的数据集。

allkeys-lru

从数据集中挑选最近最少使用的数据淘汰。当内存达到限制的时候,对所有数据集挑选最近最少使用的数据淘汰,可写入新的数据集。

allkeys-random

从数据集中任意选择数据淘汰,当内存达到限制的时候,对所有数据集挑选随机淘汰,可写入新的数据集。

no-enviction

当内存达到限制的时候,不淘汰任何数据,不可写入任何数据集,所有引起申请内存的命令会报错。

  • 如何选择淘汰策略

下面看看几种策略的适用场景

allkeys-lru:如果我们的应用对缓存的访问符合幂律分布,也就是存在相对热点数据,或者我们不太清楚我们应用的缓存访问分布状况,我们可以选择allkeys-lru策略。

allkeys-random:如果我们的应用对于缓存key的访问概率相等,则可以使用这个策略。

volatile-ttl:这种策略使得我们可以向Redis提示哪些key更适合被eviction。

另外,volatile-lru策略和volatile-random策略适合我们将一个Redis实例既应用于缓存和又应用于持久化存储的时候,然而我们也可以通过使用两个Redis实例来达到相同的效果,值得一提的是将key设置过期时间实际上会消耗更多的内存,因此我们建议使用allkeys-lru策略从而更有效率的使用内存。

四、聊聊 Redis 使用场景

随着数据量的增长,MySQL 已经满足不了大型互联网类应用的需求。因此,Redis 基于内存存储数据,可以极大的提高查询性能,对产品在架构上很好的补充。在某些场景下,可以充分的利用 Redis 的特性,大大提高效率。

缓存

对于热点数据,缓存以后可能读取数十万次,因此,对于热点数据,缓存的价值非常大。例如,分类栏目更新频率不高,但是绝大多数的页面都需要访问这个数据,因此读取频率相当高,可以考虑基于 Redis 实现缓存。

会话缓存

此外,还可以考虑使用 Redis 进行会话缓存。例如,将 web session 存放在 Redis 中。

时效性

例如验证码只有60秒有效期,超过时间无法使用,或者基于 Oauth2 的 Token 只能在 5 分钟内使用一次,超过时间也无法使用。

访问频率

出于减轻服务器的压力或防止恶意的洪水攻击的考虑,需要控制访问频率,例如限制 IP 在一段时间的最大访问量。

计数器

数据统计的需求非常普遍,通过原子递增保持计数。例如,应用数、资源数、点赞数、收藏数、分享数等。

社交列表

社交属性相关的列表信息,例如,用户点赞列表、用户分享列表、用户收藏列表、用户关注列表、用户粉丝列表等,使用 Hash 类型数据结构是个不错的选择。

记录用户判定信息

记录用户判定信息的需求也非常普遍,可以知道一个用户是否进行了某个操作。例如,用户是否点赞、用户是否收藏、用户是否分享等。

交集、并集和差集

在某些场景中,例如社交场景,通过交集、并集和差集运算,可以非常方便地实现共同好友,共同关注,共同偏好等社交关系。

热门列表与排行榜

按照得分进行排序,例如,展示最热、点击率最高、活跃度最高等条件的排名列表。

最新动态

按照时间顺序排列的最新动态,也是一个很好的应用,可以使用 Sorted Set 类型的分数权重存储 Unix 时间戳进行排序。

消息队列

Redis 能作为一个很好的消息队列来使用,依赖 List 类型利用 LPUSH 命令将数据添加到链表头部,通过 BRPOP 命令将元素从链表尾部取出。同时,市面上成熟的消息队列产品有很多,例如 RabbitMQ。因此,更加建议使用 RabbitMQ 作为消息中间件。

五、Redis 持久化机制

Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。

RDB

RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。

  1. # 快照的文件名
  2. dbfilename dump.rdb
  3. # 存放快照的目录
  4. dir /var/lib/redis
  5. # 在进行镜像备份时,是否进行压缩。
  6. # yes:压缩,但是需要一些cpu的消耗。
  7. # no:不压缩,需要更多的磁盘空间。
  8. rdbcompression yes
  9. #900秒后且至少1个key发生变化时创建快照
  10. save 900 1
  11. #300秒后且至少10个key发生变化时创建快照
  12. save 300 10
  13. #60秒后且至少10000个key发生变化时创建快照
  14. save 60 10000

一旦数据库出现问题,那么我们的RDB文件中保存的数据并不是全新的,从上次RDB文件生成到Redis停机这段时间的数据全部丢掉了。例如,每隔5分钟或者更长的时间来创建一次快照,Redis停止工作时(例如意外断电)就可能丢失最近几分钟的数据。

AOF

Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。

  1. # 是否开启AOF,默认关闭(no)
  2. appendonly yes

由于Linux会把对文件的写入操作通过buffer缓冲,因此Linux可能不是立即写入到文件,有对视数据的风险。Redis有三种不同的fsync策略供选择:no fsync at all、 fsync every second、 fsync at every query。默认为fsync every second此时的写性能仍然很好,且最坏的情况下可能丢失一秒钟的写操作。

  1. # Redis支持三种不同的刷写模式:
  2. #每次收到写命令就立即强制写入磁盘,是最有保证的完全的持久化,但速度也是最慢的,一般不推荐使用。
  3. # appendfsync always
  4. #每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,是受推荐的方式。
  5. appendfsync everysec
  6. #完全依赖OS的写入,一般为30秒左右一次,性能最好但是持久化最没有保证,不被推荐。
  7. # appendfsync no

AOF带来了另一个问题,持久化文件会变得越来越大。比如,我们调用INCR test命令100次,文件中就必须保存全部的100条命令,但其实99条都是多余的。因为要恢复数据库的状态其实文件中保存一条SET test 100就够了。为了合并重写AOF的持久化文件,Redis提供了bgrewriteaof命令。收到此命令后,Redis将使用与快照类似的方式将内存中的数据以命令的方式保存到临时文件中,最后替换原来的文件,以此来实现控制AOF文件的合并重写。由于是模拟快照的过程,因此在重写AOF文件时并没有读取旧的AOF文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的AOF文件。

  1. # AOF文件名
  2. appendfilename appendonly.aof
  3. #当进程中BGSAVE或BGREWRITEAOF命令正在执行时不阻止主进程中的fsync()调用(默认为no,当存在延迟问题时需调整为yes)
  4. no-appendfsync-on-rewrite no
  5. #当AOF增长率为100%且达到了64mb时开始自动重写AOF
  6. auto-aof-rewrite-percentage 100
  7. auto-aof-rewrite-min-size 64mb

六、Redis 集群方案与实现

下面介绍Redis的集群方案。

 

Replication(主从复制)

Redis的replication机制允许slave从master那里通过网络传输拷贝到完整的数据备份,从而达到主从机制。为了实现主从复制,我们准备三个redis服务,依次命名为master,slave1,slave2。

配置主服务器

为了测试效果,我们先修改主服务器的配置文件redis.conf的端口信息

  1. port 6300

配置从服务器

replication相关的配置比较简单,只需要把下面一行加到slave的配置文件中。你只需要把ip地址和端口号改一下。

  1. slaveof 192.168.1.1 6379

我们先修改从服务器1的配置文件redis.conf的端口信息和从服务器配置。

  1. port 6301
  2. slaveof 127.0.0.1 6300

我们再修改从服务器2的配置文件redis.conf的端口信息和从服务器配置。

  1. port 6302
  2. slaveof 127.0.0.1 6300

值得注意的是,从redis2.6版本开始,slave支持只读模式,而且是默认的。可以通过配置项slave-read-only来进行配置。
此外,如果master通过requirepass配置项设置了密码,slave每次同步操作都需要验证密码,可以通过在slave的配置文件中添加以下配置项

  1. masterauth

测试

分别启动主服务器,从服务器,我们来验证下主从复制。我们在主服务器写入一条消息,然后再其他从服务器查看是否成功复制了。

Sentinel(哨兵)

主从机制,上面的方案中主服务器可能存在单点故障,万一主服务器宕机,这是个麻烦事情,所以Redis提供了Redis-Sentinel,以此来实现主从切换的功能,类似与zookeeper。

Redis-Sentinel是Redis官方推荐的高可用性(HA)解决方案,当用Redis做master-slave的高可用方案时,假如master宕机了,Redis本身(包括它的很多客户端)都没有实现自动进行主备切换,而Redis-Sentinel本身也是一个独立运行的进程,它能监控多个master-slave集群,发现master宕机后能进行自动切换。

它的主要功能有以下几点

  • 监控(Monitoring):不断地检查redis的主服务器和从服务器是否运作正常。
  • 提醒(Notification):如果发现某个redis服务器运行出现状况,可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover):能够进行自动切换。当一个主服务器不能正常工作时,会将失效主服务器的其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

Redis Sentinel 兼容 Redis 2.4.16 或以上版本, 推荐使用 Redis 2.8.0 或以上的版本。

配置Sentinel

必须指定一个sentinel的配置文件sentinel.conf,如果不指定将无法启动sentinel。首先,我们先创建一个配置文件sentinel.conf

  1. port 26379
  2. sentinel monitor mymaster 127.0.0.1 6300 2

官方典型的配置如下

  1. sentinel monitor mymaster 127.0.0.1 6379 2
  2. sentinel down-after-milliseconds mymaster 60000
  3. sentinel failover-timeout mymaster 180000
  4. sentinel parallel-syncs mymaster 1
  5.  
  6. sentinel monitor resque 192.168.1.3 6380 4
  7. sentinel down-after-milliseconds resque 10000
  8. sentinel failover-timeout resque 180000
  9. sentinel parallel-syncs resque 5

配置文件只需要配置master的信息就好啦,不用配置slave的信息,因为slave能够被自动检测到(master节点会有关于slave的消息)。

需要注意的是,配置文件在sentinel运行期间是会被动态修改的,例如当发生主备切换时候,配置文件中的master会被修改为另外一个slave。这样,之后sentinel如果重启时,就可以根据这个配置来恢复其之前所监控的redis集群的状态。

接下来我们将一行一行地解释上面的配置项:

  1. sentinel monitor mymaster 127.0.0.1 6379 2

这行配置指示 Sentinel 去监视一个名为 mymaster 的主服务器, 这个主服务器的 IP 地址为 127.0.0.1 , 端口号为 6300, 而将这个主服务器判断为失效至少需要 2 个 Sentinel 同意,只要同意 Sentinel 的数量不达标,自动故障迁移就不会执行。

不过要注意, 无论你设置要多少个 Sentinel 同意才能判断一个服务器失效, 一个 Sentinel 都需要获得系统中多数(majority) Sentinel 的支持, 才能发起一次自动故障迁移, 并预留一个给定的配置纪元 (configuration Epoch ,一个配置纪元就是一个新主服务器配置的版本号)。换句话说, 在只有少数(minority) Sentinel 进程正常运作的情况下, Sentinel 是不能执行自动故障迁移的。sentinel集群中各个sentinel也有互相通信,通过gossip协议。

除了第一行配置,我们发现剩下的配置都有一个统一的格式:

  1. sentinel

接下来我们根据上面格式中的option_name一个一个来解释这些配置项:

  • down-after-milliseconds 选项指定了 Sentinel 认为服务器已经断线所需的毫秒数。
  • parallel-syncs 选项指定了在执行故障转移时, 最多可以有多少个从服务器同时对新的主服务器进行同步, 这个数字越小, 完成故障转移所需的时间就越长。

启动 Sentinel

对于 redis-sentinel 程序, 你可以用以下命令来启动 Sentinel 系统

  1. redis-sentinel sentinel.conf

对于 redis-server 程序, 你可以用以下命令来启动一个运行在 Sentinel 模式下的 Redis 服务器

  1. redis-server sentinel.conf --sentinel

以上两种方式,都必须指定一个sentinel的配置文件sentinel.conf, 如果不指定将无法启动sentinel。sentinel默认监听26379端口,所以运行前必须确定该端口没有被别的进程占用。
Java面试准备-缓存使用_第3张图片

测试

此时,我们开启两个Sentinel,关闭主服务器,我们来验证下Sentinel。发现,服务器发生切换了。
Java面试准备-缓存使用_第4张图片
当6300端口的这个服务重启的时候,他会变成6301端口服务的slave。

Twemproxy

Twemproxy是由Twitter开源的Redis代理, Redis客户端把请求发送到Twemproxy,Twemproxy根据路由规则发送到正确的Redis实例,最后Twemproxy把结果汇集返回给客户端。

Twemproxy通过引入一个代理层,将多个Redis实例进行统一管理,使Redis客户端只需要在Twemproxy上进行操作,而不需要关心后面有多少个Redis实例,从而实现了Redis集群。
Java面试准备-缓存使用_第5张图片
Twemproxy本身也是单点,需要用Keepalived做高可用方案。

这么些年来,Twenproxy作为应用范围最广、稳定性最高、最久经考验的分布式中间件,在业界广泛使用。

但是,Twemproxy存在诸多不方便之处,最主要的是,Twemproxy无法平滑地增加Redis实例,业务量突增,需增加Redis服务器;业务量萎缩,需要减少Redis服务器。但对Twemproxy而言,基本上都很难操作。其次,没有友好的监控管理后台界面,不利于运维监控。

Codis

Codis解决了Twemproxy的这两大痛点,由豌豆荚于2014年11月开源,基于Go和C开发、现已广泛用于豌豆荚的各种Redis业务场景。

Codis 3.x 由以下组件组成:

  • Codis Server:基于 redis-2.8.21 分支开发。增加了额外的数据结构,以支持 slot 有关的操作以及数据迁移指令。具体的修改可以参考文档 redis 的修改。
  • Codis Proxy:客户端连接的 Redis 代理服务, 实现了 Redis 协议。 除部分命令不支持以外(不支持的命令列表),表现的和原生的 Redis 没有区别(就像 Twemproxy)。对于同一个业务集群而言,可以同时部署多个 codis-proxy 实例;不同 codis-proxy 之间由 codis-dashboard 保证状态同步。
  • Codis Dashboard:集群管理工具,支持 codis-proxy、codis-server 的添加、删除,以及据迁移等操作。在集群状态发生改变时,codis-dashboard 维护集群下所有 codis-proxy 的状态的一致性。对于同一个业务集群而言,同一个时刻 codis-dashboard 只能有 0个或者1个;所有对集群的修改都必须通过 codis-dashboard 完成。
  • Codis Admin:集群管理的命令行工具。可用于控制 codis-proxy、codis-dashboard 状态以及访问外部存储。
  • Codis FE:集群管理界面。多个集群实例共享可以共享同一个前端展示页面;通过配置文件管理后端 codis-dashboard 列表,配置文件可自动更新。
  • Codis HA:为集群提供高可用。依赖 codis-dashboard 实例,自动抓取集群各个组件的状态;会根据当前集群状态自动生成主从切换策略,并在需要时通过 codis-dashboard 完成主从切换。
  • Storage:为集群状态提供外部存储。提供 Namespace 概念,不同集群的会按照不同 product name 进行组织;目前仅提供了 Zookeeper 和 Etcd 两种实现,但是提供了抽象的 interface 可自行扩展。
    Java面试准备-缓存使用_第6张图片

Codis引入了Group的概念,每个Group包括1个Redis Master及一个或多个Redis Slave,这是和Twemproxy的区别之一,实现了Redis集群的高可用。当1个Redis Master挂掉时,Codis不会自动把一个Slave提升为Master,这涉及数据的一致性问题,Redis本身的数据同步是采用主从异步复制,当数据在Maste写入成功时,Slave是否已读入这个数据是没法保证的,需要管理员在管理界面上手动把Slave提升为Master。

Codis使用,可以参考官方文档https://github.com/CodisLabs/codis/blob/release3.0/doc/tutorial_zh.md

Redis 3.0集群

Redis 3.0集群采用了P2P的模式,完全去中心化。支持多节点数据集自动分片,提供一定程度的分区可用性,部分节点挂掉或者无法连接其他节点后,服务可以正常运行。Redis 3.0集群采用Hash Slot方案,而不是一致性哈希。Redis把所有的Key分成了16384个slot,每个Redis实例负责其中一部分slot。集群中的所有信息(节点、端口、slot等),都通过节点之间定期的数据交换而更新。

Redis客户端在任意一个Redis实例发出请求,如果所需数据不在该实例中,通过重定向命令引导客户端访问所需的实例。

Redis 3.0集群,目前支持的cluster特性

  • 节点自动发现
  • slave->master 选举,集群容错
  • Hot resharding:在线分片
  • 集群管理:cluster xxx
  • 基于配置(nodes-port.conf)的集群管理
  • ASK 转向/MOVED 转向机制
    Java面试准备-缓存使用_第7张图片

如上图所示,所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。节点的fail是通过集群中超过半数的节点检测失效时才生效。客户端与redis节点直连,不需要中间proxy层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。redis-cluster把所有的物理节点映射到[0-16383]slot上cluster负责维护node<->slot<->value。

Java面试准备-缓存使用_第8张图片
选举过程是集群中所有master参与,如果半数以上master节点与master节点通信超时,认为当前master节点挂掉。

当集群不可用时,所有对集群的操作做都不可用,收到((error) CLUSTERDOWN The cluster is down)错误。如果集群任意master挂掉,且当前master没有slave,集群进入fail状态,也可以理解成进群的slot映射[0-16383]不完成时进入fail状态。如果进群超过半数以上master挂掉,无论是否有slave集群进入fail状态。

环境搭建

现在,我们进行集群环境搭建。集群环境至少需要3个主服务器节点。本次测试,使用另外3个节点作为从服务器的节点,即3个主服务器,3个从服务器。

修改配置文件,其它的保持默认即可。

  1. # 根据实际情况修改
  2. port 7000
  3. # 允许redis支持集群模式
  4. cluster-enabled yes
  5. # 节点配置文件,由redis自动维护
  6. cluster-config-file nodes.conf
  7. # 节点超时毫秒
  8. cluster-node-timeout 5000
  9. # 开启AOF同步模式
  10. appendonly yes

创建集群

目前这些实例虽然都开启了cluster模式,但是彼此还不认识对方,接下来可以通过Redis集群的命令行工具redis-trib.rb来完成集群创建。
首先,下载 https://raw.githubusercontent.com/antirez/redis/unstable/src/redis-trib.rb。

然后,搭建Redis 的 Ruby 支持环境。这里,不进行扩展,参考相关文档。

现在,接下来运行以下命令。这个命令在这里用于创建一个新的集群, 选项–replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。

  1. redis-trib.rb create --replicas 1 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006

Java面试准备-缓存使用_第9张图片

5.3、测试
Java面试准备-缓存使用_第10张图片

七、Redis 为什么是单线程的

Redis为什么是单线程的?

因为CPU不是Redis的瓶颈。Redis的瓶颈最有可能是机器内存或者网络带宽。(以上主要来自官方FAQ)既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。关于redis的性能,官方网站也有,普通笔记本轻松处理每秒几十万的请求,参见:How fast is Redis?

 

如果万一CPU成为你的Redis瓶颈了,或者,你就是不想让服务器其他核闲置,那怎么办?

那也很简单,你多起几个Redis进程就好了。Redis是keyvalue数据库,又不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。redis-cluster可以帮你做的更好。

 

单线程可以处理高并发请求吗?

当然可以了,Redis都实现了。

有一点概念需要澄清,并发并不是并行。

(相关概念:并发性I/O流,意味着能够让一个计算单元来处理来自多个客户端的流请求。并行性,意味着服务器能够同时执行几个事情,具有多个计算单元)

 

Redis总体快速的原因:

采用队列模式将并发访问变为串行访问(?)

单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),其他模块仍用了多个线程。

总体来说快速的原因如下:

1)绝大部分请求是纯粹的内存操作(非常快速)

2)采用单线程,避免了不必要的上下文切换和竞争条件

3)非阻塞IO

内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间

这3个条件不是相互独立的,特别是第一条,如果请求都是耗时的,采用单线程吞吐量及性能可想而知了。应该说redis为特殊的场景选择了合适的技术方案。

 

======

 

1. Redis服务端是个单线程的架构,不同的Client虽然看似可以同时保持连接,但发出去的命令是序列化执行的,这在通常的数据库理论下是最高级别的隔离(serialize)
2. 用MULTI/EXEC 来把多个命令组装成一次发送,达到原子性
3. 用WATCH提供的乐观锁功能,在你EXEC的那一刻,如果被WATCH的键发生过改动,则MULTI到EXEC之间的指令全部不执行,不需要rollback
4. 其他回答中提到的DISCARD指令只是用来撤销EXEC之前被暂存的指令,并不是回滚

八、缓存雪崩

由于原有的缓存过期失效,新的缓存还没有缓存进来,有一只请求缓存请求不到,导致所有请求都跑去了数据库,导致数据库IO、内存和CPU眼里过大,甚至导致宕机,使得整个系统崩溃。

解决思路:
1,采用加锁计数,或者使用合理的队列数量来避免缓存失效时对数据库造成太大的压力。这种办法虽然能缓解数据库的压力,但是同时又降低了系统的吞吐量。
2,分析用户行为,尽量让失效时间点均匀分布。避免缓存雪崩的出现。
3,如果是因为某台缓存服务器宕机,可以考虑做主备,比如:redis主备,但是双缓存涉及到更新事务的问题,update可能读到脏数据,需要好好解决。

加锁:加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

public class CacheDemo

{

    public object GetCacheDataList()

        {

            const int cacheTime = 60;   

            const string lockKey = cacheKey;

            const string cacheKey = "datainfolist";      

            var cacheValue = CacheHelper.Get(cacheKey);

            if (cacheValue != null)

            {

                return cacheValue;

            }

            else

            {

                lock (lockKey)

                {

                    cacheValue = CacheHelper.Get(cacheKey);

                    if (cacheValue != null)

                    {

                        return cacheValue;

                    }

                    else

                    {

                        cacheValue = GetDataBaseInfo();              

                        CacheHelper.Add(cacheKey, cacheValue, cacheTime);

                    }                   

                }

                return cacheValue;

            }

        }

}

  标记失效缓存:

缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存。

  缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。

  这样做后,就可以一定程度上提高系统吞吐量。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

public object GetProductListNew()

        {

            const int cacheTime = 30;

            const string cacheKey = "product_list";

            //缓存标记。

            const string cacheSign = cacheKey + "_sign";

             

            var sign = CacheHelper.Get(cacheSign);

            //获取缓存值

            var cacheValue = CacheHelper.Get(cacheKey);

            if (sign != null)

            {

                return cacheValue; //未过期,直接返回。

            }

            else

            {

                CacheHelper.Add(cacheSign, "1", cacheTime);

                ThreadPool.QueueUserWorkItem((arg) =>

                {

                    cacheValue = GetProductListFromDB(); //这里一般是 sql查询数据。

                    CacheHelper.Add(cacheKey, cacheValue, cacheTime*2); //日期设缓存时间的2倍,用于脏读。               

                });

                 

                return cacheValue;

            }

        }

九、缓存穿透:

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。

  解决的办法就是:如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库中查询。

解决思路:

1,如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。

2,根据缓存数据Key的规则。例如我们公司是做机顶盒的,缓存数据以Mac为Key,Mac是有规则,如果不符合规则就过滤掉,这样可以过滤一部分查询。在做缓存规划的时候,Key有一定规则的话,可以采取这种办法。这种办法只能缓解一部分的压力,过滤和系统无关的查询,但是无法根治。

3,采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的BitSet中,不存在的数据将会被拦截掉,从而避免了对底层存储系统的查询压力。关于布隆过滤器,详情查看:基于BitSet的布隆过滤器(Bloom Filter) 

大并发的缓存穿透会导致缓存雪崩。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

public object GetProductListNew()

        {

            const int cacheTime = 30;

            const string cacheKey = "product_list";

 

            var cacheValue = CacheHelper.Get(cacheKey);

            if (cacheValue != null)

                return cacheValue;

                 

            cacheValue = CacheHelper.Get(cacheKey);

            if (cacheValue != null)

            {

                return cacheValue;

            }

            else

            {

                cacheValue = GetProductListFromDB(); //数据库查询不到,为空。

                 

                if (cacheValue == null)

                {

                    cacheValue = string.Empty; //如果发现为空,设置个默认值,也缓存起来。               

                }

                CacheHelper.Add(cacheKey, cacheValue, cacheTime);

                 

                return cacheValue;

            }

        }

  把空结果,也给缓存起来,这样下次同样的请求就可以直接返回空了,即可以避免当查询的值为空时引起的缓存穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key进行预先校验,然后再放行给后面的正常缓存处理逻辑。

 

 

缓存预热

  缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样避免,用户请求的时候,再去加载相关的数据。

  解决思路:

    1,直接写个缓存刷新页面,上线时手工操作下。

    2,数据量不大,可以在WEB系统启动的时候加载。

    3,定时刷新缓存,

 

缓存更新

  缓存淘汰的策略有两种:

    (1) 定时去清理过期的缓存。

    (2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。 

  两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂,具体用哪种方案,大家可以根据自己的应用场景来权衡。1. 预估失效时间 2. 版本号(必须单调递增,时间戳是最好的选择)3. 提供手动清理缓存的接口。

 

 

 

十、使用缓存的合理性问题

如何使用缓存,怎么才能更加合理?今天的话题,结合我之前的项目场景,讨论下使用缓存合理性问题。

 

热点数据,缓存才有价值

对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。

对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。

频繁修改的数据,看情况考虑使用缓存

数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

对于上面两个例子,寿星列表、导航信息都存在一个特点,就是信息修改频率不高,读取通常非常高的场景。

那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

数据不一致性

一般会对缓存设置失效时间,一旦超过失效时间,就要从数据库重新加载,因此应用要容忍一定时间的数据不一致。还有一种是在数据更新时立即更新缓存,不过这也会更多系统开销和事务一致性问题。

缓存更新机制

使用缓存过程中,我们经常会遇到缓存数据的不一致性和与脏读现象,我们有什么解决策略呢?

一般情况下,我们采取缓存双淘汰机制,在更新数据库的时候淘汰缓存。此外,设定超时时间,例如30分钟。极限场景下,即使有脏数据入cache,这个脏数据也最多存在三十分钟。

缓存可用性

缓存是提高数据读取性能的,缓存数据丢失和缓存不可用不会影响应用程序的处理。因此,一般的操作手段是,如果Redis出现异常,我们手动捕获这个异常,记录日志,并且去数据库查询数据返回给用户。

缓存服务降级

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

缓存预热

在新启动的缓存系统中,如果没有任何数据,在重建缓存数据过程中,系统的性能和数据库复制都不太好,那么最好的缓存系统启动时就把热点数据加载好,例如对于缓存信息,在启动缓存加载数据库中全部数据进行预热。一般情况下,我们会开通一个同步数据的接口,进行缓存预热。

缓存穿透

如果因为不恰当的业务,或者恶意攻击持续地发请求某些不存在的数据,由于缓存没有保存该数据,所有的请求都会落到数据库上,会对数据库造成很大压力,甚至奔溃。一个简单的对策是将不存在的数据也缓存起来。

你可能感兴趣的:(Java面试准备-缓存使用)