一、Redis集群
1.1 简介
从redis 3.0之后版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。
sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。cluster模式的出现就是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器。
其结构特点:
- redis是一个开源的key value存储系统,受到了广大互联网公司的青睐。redis3.0版本之前只支持单例模式,在3.0版本及以后才支持集群,我这里用的是redis5.0.7版本;
- redis集群采用P2P模式,是完全去中心化的,不存在中心节点或者代理节点;
- redis集群是没有统一的入口的,客户端(client)连接集群的时候连接集群中的任意节点(node)即可,集群内部的节点是相互通信的(PING-PONG机制),每个节点都是一个redis实例;
- 为了实现集群的高可用,即判断节点是否健康(能否正常使用),redis-cluster有这么一个投票容错机制:如果集群中超过半数的节点投票认为某个节点挂了,那么这个节点就挂了(fail)。这是判断节点是否挂了的方法;
- 那么如何判断集群是否挂了呢? 如果集群中任意一个节点挂了,而且该节点没有从节点(备份节点),那么这个集群就挂了。这是判断集群是否挂了的方法;
- 那么为什么任意一个节点挂了(没有从节点)这个集群就挂了呢?因为集群内置了16384个slot(哈希槽),并且把所有的物理节点映射到了这16384[0-16383]个slot上,或者说把这些slot均等的分配给了各个节点。当需要在Redis集群存放一个数据(key-value)时,redis会先对这个key进行crc16算法,然后得到一个结果。再把这个结果对16384进行求余,这个余数会对应[0-16383]其中一个槽,进而决定key-value存储到哪个节点中。所以一旦某个节点挂了,该节点对应的slot就无法使用,那么就会导致集群无法正常工作。
综上所述,每个Redis集群理论上最多可以有16384个节点。。
2.2 实现步骤
1、在redis目录下创建cluster目录,并将redis.conf拷贝到该目录下重命名为redis8001.conf
2、修改它的端口、日志文件、rbd文件等
92 port 8001
158 pidfile /var/run/redis_8001.pid
171 logfile "8001.log"
253 dbfilename dump8001.rdb
699 appendonly yes
703 appendfilename "appendonly8001.aof"
832 cluster-enabled yes
840 cluster-config-file nodes-8001.conf
3、将redis8001.conf拷贝5份,分别命名为redis8002.conf - redis8006.conf
4、分别将这5份文件中所有的8001修改为对应的数字
全局替换
:%s/8001/8002/g
5、运行这6个redis
在redis目录下创建start.sh文件 vi start.sh
并添加以下内容
redis-server cluster/redis8001.conf
redis-server cluster/redis8002.conf
redis-server cluster/redis8003.conf
redis-server cluster/redis8004.conf
redis-server cluster/redis8005.conf
redis-server cluster/redis8006.conf
ps -ef|grep redis
给该文件添加执行权限
chmod +x start.sh
执行start.sh,启动redis
./start.sh
6、通过指令让6个redis自动形成集群
redis-cli --cluster create 192.168.86.128:8001 192.168.86.128:8002 192.168.86.128:8003 192.168.86.128:8004 192.168.86.128:8005 192.168.86.128:8006 --cluster-replicas 1
—cluster-replicas 1 用来指定每个主机的从机数,此处为1,默认情况下会将前三台选为主机,后三台为从机
7、以集群方式连接任何一台主机
redis-cli -c -p 8001
8、批量停止redis
vi shutdown.sh
#!/bin/bash
PORT=8001
ENDPORT=8006
while [ $((PORT <= ENDPORT)) != "0" ]; do
echo "Stopping Redis $PORT"
redis-cli -p $PORT shutdown
PORT=$((PORT+1))
done
echo "done"
exit 0
修改权限:
chmod +x shutdown.sh
执行shutdown.sh,停止redis
./shutdown.sh
9、批量删除文件
同上,编辑并修改文件权限并执行
vi rm.sh
rm -f *.log
rm -f *.aof
rm -f *.rdb
rm -f nodes*
2.3 哈希槽
redis集群在工作时就是通过哈希槽进行数据定位,在Redis中哈希槽总共有16384个
Redis cluster采用数据分片的哈希槽来进行数据存储和数据的读取。
redis cluster一共有2^14(16384)个槽,所有的master节点都会有一个槽区比如0~1000,槽数是可以迁移的。master节点的slave节点不分配槽,只拥有读权限。但是注意在代码中redis cluster执行读写操作的都是master节点,并不是你想的读是从节点,写是主节点。第一次新建redis cluster时,16384个槽是被master节点均匀分布的,但是后期因为节点的加入、退出会导致不一定按照平均分布。
·key的定位规则是根据CRC-16(key)%16384的值来判断属于哪个槽区,从而判断该key属于哪个节点,
·redis cluster是采用master节点有多个slave节点机制来保证数据的完整性的,master节点写入数据,slave节点同步数据。当master节点挂机后,slave节点会通过选举机制选举出一个节点变成master节点,实现高可用。
2.4 SpringBoot中使用redis集群
在yml配置文件中配置:
spring:
redis:
host: 127.0.0.1
port: 6379
timeout: 20000
cluster:
nodes: xxx.xxx.xxx.xxx:xxxx,
xxx.xxx.xxx.xxx:xxxx,
xxx.xxx.xxx.xxx:xxxx
maxRedirects: 3
二、热点数据
热点数据:近期访问量比较大的数据
Redis是内存中的数据库,所有的数据都在内存,而内存是稀缺资源,所以在redis中存放的数据尽可能的应该热点数据,当redis存储空间不够时,应该要淘汰一些冷门的数据
2.1 内存淘汰策略
当内存不足时需要淘汰相对冷门数据,采用什么样的策略
LRU(Least Recently Used)
策略 | 解释 |
---|---|
volatile-lru | 从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。注意:redis并不是保证取得所有数据集中最近最少使用的键值对,而只是随机挑选的几个键值对中的, 当内存达到限制的时候无法写入非过期时间的数据集。 |
volatile-ttl | 从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。注意:redis 并不是保证取得所有数据集中最近将要过期的键值对,而只是随机挑选的几个键值对中的, 当内存达到限制的时候无法写入非过期时间的数据集。 |
volatile-random | 从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。当内存达到限制的时候无法写入非过期时间的数据集。 |
allkeys-lru(常用) | 从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰。当内存达到限制的时候,对所有数据集挑选最近最少使用的数据淘汰,可写入新的数据集。 |
allkeys-random | 从数据集(server.db[i].dict)中任意选择数据淘汰,当内存达到限制的时候,对所有数据集挑选随机淘汰,可写入新的数据集。 |
no-enviction | 当内存达到限制的时候,不淘汰任何数据,不可写入任何数据集,所有引起申请内存的命令会报错。 |
修改redis.conf
566行 redis内存超过多大时进行淘汰(单位:byte)
maxmemory 1024000000
597行 redis淘汰策略
maxmemory-policy allkeys-lru
2.2 面试题1:如何保证redis中数据都为热点数据
根据使用经验, 一般来说回收策略可以这样来配置:
- allkeys-lru:如果期望用户请求呈现幂律分布(power-law distribution),也就是,期望一部分子集元素被访问得远比其他元素多时,可以使用allkeys-lru策略。在你不确定时这是一个好的选择。
- allkeys-random:如果期望是循环周期的访问,所有的键被连续扫描,或者期望请求符合平均分布(每个元素以相同的概率被访问),可以使用allkeys-random策略。
- volatile-ttl:如果你期望能让 Redis 通过使用你创建缓存对象的时候设置的TTL值,确定哪些对象应该是较好的清除候选项,可以使用volatile-ttl策略。
另外值得注意的是,为键设置过期时间需要消耗内存,所以使用像allkeys-lru这样的策略会更高效,因为在内存压力下没有必要为键的回收设置过期时间。
2.3 面试题2:内存大小限制情况下,如何保证redis中都为热点数据
问题:由于内存限制,导致了Redis中只能存储最多1W条的数据信息,如何确保这1w条数据是最热门的数据?
解决方案:
- 热点数据排序(点击次数)
既然热门数据,那么就需要有排序,使用redis中的zset数据类型是很自然的想法。数据中的某个唯一字段作为zset中的value,而点击次数作为score,记为click_zset。这样就可选出最热门的数据。而数据,则直接用HashMap存储。
- 热点数据时间(近期访问)
既然只能存1w条数据且需要是热门数据,那么,点击次数是一方面,时效性也是一方面,如何保证?可以另起一个zset,数据的字段为value,而每次点击时更新当前时间戳为其score,记为time_zset这样,就可以记录时间。在后台跑一个任务,间隔一定时间段删除两个zset中长时间没有发生点击事件的键,并删除hash数据,为产生的新数据腾出数据空间。
- 处理新热点数据
如果有空间,则保存到自己的hashmap,并将key存到两个zset中。
而没有空间时,就应该在click_zset中取出点击次数排在最前第1w位后面的键,删除对应的hash数据。然后看这1w个score的值,然后把key放入两个zset中即可。
三、缓存穿透、缓存雪崩、缓存击穿
3.1 缓存穿透
程序在处理缓存时,一般是先从缓存查询,如果缓存没有这个key获取为null,则会从DB中查询,并设置到缓存中去。
按这种做法,那查询一个一定不存在的数据值,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决办法:
·最好对于每一个缓存key都有一定的规范约束,这样在程序中对不符合parttern的key 的请求可以拒绝。(但一般key都是通过程序自动生成的)
·将可能出现的缓存key的组合方式的所有数值以hash形式存储在一个很大的bitmap中<布隆过滤器>(需要考虑如何将这个可能出现的数据的hash值之后同步到bitmap中,后端每次新增一个可能的组合就同步一次,或者穷举),一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
·常用:如果对应在数据库中的数据都不存在,我们将此key对应的value设置为一个默认的值,比如“NULL”,并设置一个缓存的失效时间。当然这个key的时效比正常的时效要小的多
3.2 缓存雪崩
指的是大量缓存,集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。
解决办法:
·这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布,设置不同的过期时间。
·用加分布式锁或者分布式队列的方式保证缓存的单线程(进程)写 (例如:redis的 SETNX),从而避免失效时大量的并发请求落到底层存储系统上。在加锁方法内先从缓存中再获取一次(防止另外的线程优先获取锁已经写入了缓存),没有再查DB写入缓存。(当然也可以: 在没有获取锁(tryLock)的线程中一直轮询缓存,至超限时)
3.3 缓存击穿
指的是热点key在某个特殊的场景时间内恰好失效了,恰好有大量并发请求过来了,造成DB压力。
解决办法:
·与缓存雪崩的解决方法类似: 用加锁或者队列的方式保证缓存的单线程(进程)写,在加锁方法内先从缓存中再获取一次,没有再查DB写入缓存。
·还有一种比较好用的(针对缓存雪崩与缓存击穿):
物理上的缓存是不设置超时时间的(或者超时时间比较长),但是在缓存的对象上增加一个属性来标识超时时间(此时间相对小)。当获取到数据后,校验数据内部的标记时间,判定是否快超时了,如果是,异步发起一个线程(控制好并发)去主动更新该缓存。
这种方式会导致一定时间内,有些请求获取缓存会拿到过期的值,看业务是否能接受而定。