引子
前面我们介绍了主从复制以及哨兵,它们可以提高读的并发,但是单个master容量有限,数据达到一定程度会有瓶颈,这个时候可以通过水平扩展为多master-slave成为集群。
那么接下来我们就介绍redis-cluster:它可以支撑多个master-slave,支持海量数据,实现高可用与高并发。
哨兵模式其实也是一种集群,它能够提高读请求的并发,但是容错方面可能会有些问题,比如master同步数据给slave的时候,这其实是异步复制吧,这个时候master挂了,那么slave上的数据就没有master新,数据同步需要时间的,1-2秒的数据会丢失。master恢复并转换成slave后,新数据则丢失。
特点
集群容错
构建Redis集群,需要至少3个节点作为master,以此组成一个高可用的集群,此外每个master都需要配备一个slave,所以整个集群需要6个节点,这也是最经典的Redis集群,也可以称之为三主三从,容错性更佳。所以在搭建的时候需要有6台虚拟机。
构建Redis集群配置
redis.conf配置
# 开启集群模式
cluster-enabled yes
# 每一个节点需要有一个配置文件,需要6份。每个节点处于集群的角色都需要告知其他所有节点,彼此知道,这个文件用于存储集群模式下的集群状态等信息,这个文件是由redis自己维护,我们不用管。如果你要重新创建集群,那么把这个文件删了就行
cluster-config-file nodes-201.conf
# 超时时间,超时则认为master宕机,随后主备切换
cluster-node-timeout 5000
# 开启AOF
appendonly yes
启动6个redis实例
创建集群
#####
# 注意1:如果你使用的是redis3.x版本,需要使用redis-trib.rb来构建集群,最新版使用C语言来构建了,这个要注意
# 注意2:以下为新版的redis构建方式
###### 创建集群,主节点和从节点比例为1,1-3为主,4-6为从,1和4,2和5,3和6分别对应为主从关系,这也是最经典用的最多的集群模式
redis-cli --cluster create ip1:port1 ip2:port2 ip3:port3 ip4:port4 ip5:port5 ip6:port6 --cluster-replicas 1
slots:槽,用于装数据,主节点有,从节点没有。
检查集群信息
redis-cli --cluster check 192.168.25.64:6380
springboot集成Redis集群配置:
- spring:
- redis:
- password: 1TdhblkFcdhx2a
- cluster:
- nodes: 192.168.1.201:6379,192.168.1.202:6379,192.168.1.203:6379,192.168.1.204:6379,192.168.1.205:6379,192.168.1.206:6379
Redis缓存穿透、雪崩方案与批量查询的优化设计
Redis缓存穿透
什么是缓存穿透?
热点key在缓存中查无数据,转向数据库也查无,大量的请求就会直接打在数据库上,给数据库增加压力。例如商品分类
解决方案:
布隆过滤器
缓存空对象
缓存穿透出现的原因:
例子:
用户查询数据,如果缓存中没有数据,就会去数据库中查询,然后再加入缓存中,之后的请求就可以在缓存中查询了;
如果用户在查询的时候,使用非法请求,每次查询到的都是数据库中没有的数据,这样就无法存入缓存之中了;
这样就会导致,如果用户连续发送这种数据库中根本不存在值的查询,这种请求上千上万次,这种不经过缓存而直接请求数据库,就被称为缓存穿透。
实例:
不为空才放入缓存之中(这样有可能会造成缓存穿透)

解决方案: 不管是否为空,都将其放入缓存
(为防止占用过多无效key占用存储,为这种类型的key设置一个过期时间 [5-10分钟])
为空的key设置过期时间,不为空的key设置永久存在

就算将来这种key的值会有值了, set方法是会去覆盖的,所以无需担心
布隆过滤器
相当于在Redis前加了一层过滤器,用户请求去查询数据,如果缓存中和数据库中都没有数据,
那么布隆过滤器中也必定没有,这样请求经过过滤器得知数据库中没有数据,那么请求就不会访问缓存和数据库了;

将key 存放在 二进制数组中,一个二进制单位可以存放一个或者多个key,这种方式查询起来key是否存在是非常快且迅速的; (1就是存在 key的二进制单位,0就是一个key都没存放)
但是尽量不要用,因为存在缺陷:
如果删除了数据库和redis中的数据,但是布隆过滤器中的数据是没法删除的,因为一个二进制单位中可能绑定有多个key,没法达到修改二进制单位为0而达到删除效果;
布隆过滤器存在误判的概念,二进制数组越大,误差率越低;
使用布隆过滤器,代码复杂度会增大, 维护难度会增大,(维护一个集合,这个集合中存在很多的key)
集群和分布式环境下,布隆过滤器需要和redis结合使用,数据需要保存到redis中,保证各个节点都要可用;
布隆过滤器基本原理
1、初始化所有数组值都为0
2、一个数值,更具hash算法,更改三个数值。如subCat:9999本不存在,但hash后发现hash的三个值,都是1,则会误判这个值是存在的。因此布隆过滤器可以判断一个值绝对不存在,但是不能判断一个值是否一定存在。
3、删除键是不能删的,因为可能有重复键值。

Redis缓存雪崩
什么是缓存雪崩
缓存的key是可以设置过期时间的;
如果同一个时刻,有大面积的key过期失效;
恰恰这个时候,又有大量的请求进来;
这个时候就会出现大量请求不经过缓存,而直接落到数据库上,造成数据库崩溃,这种现象就称为缓存雪崩。
如何预防雪崩
对于这种现象,想要完全解决是没法实现的,只能预防,去缓解雪崩这一现象;
可以设置key永不过期;
有些数据不存储在数据库中,可以设置有效时间,比如验证码;
其他一些数据,第一次需要经过数据库查询的就设置为永不过期,后期可以手动的进行过期删除;
尽量让缓存key,过期时间错开
每次服务器重启初始化的时候,大量的数据加载到缓存中,其中也包括这些设置了过期时间的缓存;
如果每种类型的数据过期时间都设置一样的话,有可能会导致过期时间一到,大面积key失效,造成雪崩
使用可以给每种key都设置成不同的过期时间,尽量避开同一时间大量缓存key失效;
多缓存结合
比如: 请求进来先查询Redis, 如果没有再查询其他缓存库,最终都没有再查询数据库,这样可以有效降低缓存雪崩出现的概率;
越后面,的缓存中的key,设置的过期时间越久
采购第三方Redis
Redis批量查询的优化
批量查询mget
- /**
- * 批量查询 mget
- * @param keys
- * @return
- */
- @GetMapping("/mget")
- public Object mget(String... keys) {
- List<String> keysList = Arrays.asList(keys);
- return redisOperator.mget(keysList);
- }
- /**
- * 批量查询,对应mget
- * @param keys
- * @return
- */
- public List<String> mget(List<String> keys) {
- return redisTemplate.opsForValue().multiGet(keys);
- }
批量查询pipeline
当客户端client与Redis建立连接后,在一段时间内,不会马上关闭管道连接;
管道批量查询支持的类型更加丰富;
mget这种,一次批量查询只能处理单个类型的批量数据;
- /**
- * 批量查询 pipeline
- * @param keys
- * @return
- */
- @GetMapping("/batchGet")
- public Object batchGet(String... keys) {
- List<String> keysList = Arrays.asList(keys);
- return redisOperator.batchGet(keysList);
- }
- /**
- * 批量查询,管道pipeline
- * @param keys
- * @return
- */
- public List<Object> batchGet(List<String> keys) {
-
- // nginx -> keepalive
- // redis -> pipeline
-
- List<Object> result = redisTemplate.executePipelined(new RedisCallback<String>() {
- @Override
- public String doInRedis(RedisConnection connection) throws DataAccessException {
- StringRedisConnection src = (StringRedisConnection)connection;
-
- for (String k : keys) {
- src.get(k);
- }
- return null;
- }
- });
-
- return result;
- }
RedisOperator工具类代码:
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.dao.DataAccessException;
- import org.springframework.data.redis.connection.RedisConnection;
- import org.springframework.data.redis.connection.StringRedisConnection;
- import org.springframework.data.redis.core.RedisCallback;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.stereotype.Component;
-
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.concurrent.TimeUnit;
-
- /**
- * @Title: Redis 工具类
- * @author lvxiaosha
- */
- @Component
- public class RedisOperator {
-
- // @Autowired
- // private RedisTemplate<String, Object> redisTemplate;
-
- @Autowired
- private StringRedisTemplate redisTemplate;
-
- // Key(键),简单的key-value操作
-
- /**
- * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
- *
- * @param key
- * @return
- */
- public long ttl(String key) {
- return redisTemplate.getExpire(key);
- }
-
- /**
- * 实现命令:expire 设置过期时间,单位秒
- *
- * @param key
- * @return
- */
- public void expire(String key, long timeout) {
- redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
- }
-
- /**
- * 实现命令:INCR key,增加key一次
- *
- * @param key
- * @return
- */
- public long incr(String key, long delta) {
- return redisTemplate.opsForValue().increment(key, delta);
- }
-
- /**
- * 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
- */
- public Set<String> keys(String pattern) {
- return redisTemplate.keys(pattern);
- }
-
- /**
- * 实现命令:DEL key,删除一个key
- *
- * @param key
- */
- public void del(String key) {
- redisTemplate.delete(key);
- }
-
- // String(字符串)
-
- /**
- * 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
- *
- * @param key
- * @param value
- */
- public void set(String key, String value) {
- redisTemplate.opsForValue().set(key, value);
- }
-
- /**
- * 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
- *
- * @param key
- * @param value
- * @param timeout
- * (以秒为单位)
- */
- public void set(String key, String value, long timeout) {
- redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
- }
-
- /**
- * 实现命令:GET key,返回 key所关联的字符串值。
- *
- * @param key
- * @return value
- */
- public String get(String key) {
- return (String)redisTemplate.opsForValue().get(key);
- }
-
- /**
- * 批量查询,对应mget
- * @param keys
- * @return
- */
- public List<String> mget(List<String> keys) {
- return redisTemplate.opsForValue().multiGet(keys);
- }
-
- /**
- * 批量查询,管道pipeline
- * @param keys
- * @return
- */
- public List<Object> batchGet(List<String> keys) {
-
- // nginx -> keepalive
- // redis -> pipeline
-
- List<Object> result = redisTemplate.executePipelined(new RedisCallback<String>() {
- @Override
- public String doInRedis(RedisConnection connection) throws DataAccessException {
- StringRedisConnection src = (StringRedisConnection)connection;
-
- for (String k : keys) {
- src.get(k);
- }
- return null;
- }
- });
-
- return result;
- }
-
-
- // Hash(哈希表)
-
- /**
- * 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
- *
- * @param key
- * @param field
- * @param value
- */
- public void hset(String key, String field, Object value) {
- redisTemplate.opsForHash().put(key, field, value);
- }
-
- /**
- * 实现命令:HGET key field,返回哈希表 key中给定域 field的值
- *
- * @param key
- * @param field
- * @return
- */
- public String hget(String key, String field) {
- return (String) redisTemplate.opsForHash().get(key, field);
- }
-
- /**
- * 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
- *
- * @param key
- * @param fields
- */
- public void hdel(String key, Object... fields) {
- redisTemplate.opsForHash().delete(key, fields);
- }
-
- /**
- * 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
- *
- * @param key
- * @return
- */
- public Map<Object, Object> hgetall(String key) {
- return redisTemplate.opsForHash().entries(key);
- }
-
- // List(列表)
-
- /**
- * 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
- *
- * @param key
- * @param value
- * @return 执行 LPUSH命令后,列表的长度。
- */
- public long lpush(String key, String value) {
- return redisTemplate.opsForList().leftPush(key, value);
- }
-
- /**
- * 实现命令:LPOP key,移除并返回列表 key的头元素。
- *
- * @param key
- * @return 列表key的头元素。
- */
- public String lpop(String key) {
- return (String)redisTemplate.opsForList().leftPop(key);
- }
-
- /**
- * 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
- *
- * @param key
- * @param value
- * @return 执行 LPUSH命令后,列表的长度。
- */
- public long rpush(String key, String value) {
- return redisTemplate.opsForList().rightPush(key, value);
- }
-
- }