<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
server:
port: 8081
spring:
session.store-type: redis
redis:
database: 0
host: localhost
port: 6379
password: root
timeout: 300
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
完成上述配置之后,会自动创建两个对象:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1WXTr721-1606313432296)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201111094810912.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RMFxzrHy-1606313432298)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201111094051072.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vd63ZUSQ-1606313432300)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201111094935637.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dZvEMS3F-1606313432302)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201111101857482.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TLeWvlhC-1606313432304)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201111101933208.png)]
package com.sonnie.springbootredis_demo.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置类
* @program: redisdemo
* @Author: david
* @Description:
*/
//RedisConfig中重写RedisTemplate类(why?)
@Configuration
@EnableCaching //开启注解
public class RedisConfig extends CachingConfigurerSupport {
/**
* retemplate相关配置
* @param factory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(factory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 值采用json序列化
template.setValueSerializer(jacksonSeial);
//使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
// 设置hash key 和value序列化模式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jacksonSeial);
template.afterPropertiesSet();
return template;
}
/**
* 对hash类型的数据操作
*
* @param redisTemplate
* @return
*/
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
/**
* 对redis字符串类型数据操作
*
* @param redisTemplate
* @return
*/
@Bean
public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForValue();
}
/**
* 对链表类型的数据操作
*
* @param redisTemplate
* @return
*/
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
/**
* 对无序集合类型的数据操作
*
* @param redisTemplate
* @return
*/
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
/**
* 对有序集合类型的数据操作
*
* @param redisTemplate
* @return
*/
@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
}
package com.sonnie.springbootredis_demo.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* redisTemplate封装
*
* @author david
*/
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public RedisUtil(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
//============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 赋值操作,存在值返回true,不存在返回false;
*
* @param key 键
* @param value 值
* @param millisecond 有效期
* @return 赋值结果
*/
public boolean setIfAbsent(String key,String value, long millisecond) {
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, value,
millisecond, TimeUnit.MILLISECONDS);
return success != null && success;
}
/**
* 移除key对应的value。
*
* @param key 键
*
*/
public void delete(String key) {
redisTemplate.delete(key);
}
}
缓存就是存在于计算机内存中的一段数据;
针对于我们的程序而言,缓存就是存在于JVM(JVM也存在于内存中)中的一段数据。
a、读写快
b、断电既失
a、提高网站响应速度,优化网站的运行
b、减轻访问数据库时给数据库带来的压力
缓存一般应用于查询较多,增删极少的业务领域
项目结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uWeFzfjW-1606313432306)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201111160631566.png)]
——使用Mybatis框架提供二级缓存技术
——只需要在使用缓存模块mapper配置中加入标签即可
<mapper namespace="com.example.springbootredisdemo.dao.UserDao">
<cache/>
<select id="selectAll" resultType="com.example.springbootredisdemo.entity.User">
select id,name,password from users
select>
<select id="selectById" resultType="com.example.springbootredisdemo.entity.User" parameterType="String">
select id,name,password from users where id = #{id}
select>
<insert id="insert" parameterType="com.example.springbootredisdemo.entity.User">
insert into users (id,name,password) values (#{id},#{name},#{password})
insert>
<update id="updateById" parameterType="com.example.springbootredisdemo.entity.User">
update users set name # {name},password = #{password} where id = #{id}
update>
<delete id="deleteById" parameterType="com.example.springbootredisdemo.entity.User">
delete from users where id = #{id}
delete>
mapper>
——Mybatis自身缓存(也称作本地缓存)的缺点:
a、本地缓存存储在JVM内存中,如果数据过大,会影响JVM的性能
b、不能作为分布式缓存
package com.example.springbootredisdemo.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
//在自定义的类中获取工厂中已经创建好的某些对象
@Configuration
public class ApplicationContextUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
//根据bean id获取对象
public static Object getBean(String id){
return context.getBean(id);
}
//根据bean 类型获取对象
public static Object getBean(Class clazz){
return context.getBean(clazz);
}
//根据bean id和类型获取对象
public static Object getBean(String id,Class clazz){
return context.getBean(id,clazz);
}
}
package com.example.springbootredisdemo.config;
import com.example.springbootredisdemo.util.ApplicationContextUtil;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 重写Mybatis cache实现redis缓存
* 不能交给工厂管理
* */
public class RedisCache implements Cache {
//必须定义这个String类型的id,因为这个id表示当前加入缓存的namespace;
private String id;
public RedisCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
//放入缓存
@Override
public void putObject(Object key, Object value) {
//获取redisTemplate对象
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
//存储缓存数据
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置HashKey序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//hash模型
redisTemplate.opsForHash().put(id,key.toString(),value);
}
//从缓存中获取
@Override
public Object getObject(Object key) {
//获取redisTemplate对象
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置HashKey序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate.opsForHash().get(id.toString(),key.toString());
}
//从缓存中移除
//真正使用过程中,这个方法并不会被用到
@Override
public Boolean removeObject(Object key) {
return null;
}
//清除缓存
@Override
public void clear() {
//获取redisTemplate对象
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
redisTemplate.delete(id);
}
//缓存命中率计算
@Override
public int getSize() {
// 获取redisTemplate对象
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate.opsForHash().size(id.toString()).intValue();
}
/*
* ReadWriteLock读写锁 表示:读写之间互斥,读读之间不互斥,写写之间不互斥
* 区别于Synchronized 表示:读读之间互斥,写写之阿互斥,读写之间互斥
* 因此ReadWriteLock效率比Synchronized高
* 对于缓存,只有写操作,没有写操作
* */
@Override
public ReadWriteLock getReadWriteLock() {
return new ReentrantReadWriteLock();
}
}
<mapper namespace="com.example.springbootredisdemo.dao.UserDao">
<cache type="com.example.springbootredisdemo.config.RedisCache"/>
<select id="selectAll" resultType="com.example.springbootredisdemo.entity.User">
select id,name,password from users
select>
<select id="selectById" resultType="com.example.springbootredisdemo.entity.User" parameterType="String">
select id,name,password from users where id = #{id}
select>
<insert id="insert" parameterType="com.example.springbootredisdemo.entity.User">
insert into users (id,name,password) values (#{id},#{name},#{password})
insert>
<update id="updateById" parameterType="com.example.springbootredisdemo.entity.User">
update users set name # {name},password = #{password} where id = #{id}
update>
<delete id="deleteById" parameterType="com.example.springbootredisdemo.entity.User">
delete from users where id = #{id}
delete>
mapper>
1.Mybatis本地缓存的缺点:
a、本地缓存存储在JVM内存中,如果数据过大,会影响JVM的性能
b、不能在分布式系统中实现共享
2.redis缓存的优点:
a、本身就是内存数据库,在内存中存储数据,读写块
b、可以在分布式系统中实现共享
MyBatis 中涉及到动态 SQL 的原因,缓存项的 key 不能仅仅通过一个 String 来表示,所以通过CacheKey 来封装缓存的 key 值,CacheKey 可以封装多个影响缓存项的因素
a、namespace.id
b、指定查询结果集的范围(分页信息)
c、查询所使用的 SQL 语句
d、用户传递给 SQL 语句的实际参数值
通过这种方式确保了cacheKey的唯一。
a、初步设计:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-se6yHQX2-1606313432307)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201111174524613.png)]
缺点:在Redis中散列了很多的key,他们都被存储在一起;当进行清除操作时,会将其他模块的缓存一起清除掉。
b、优化改进:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yZw8J53R-1606313432308)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201111174958554.png)]
缓存雪崩:
**概念:**是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。
举个例子,目前电商首页以及热点数据都会去做缓存 ,一般缓存都是定时任务去刷新,或者是查不到之后 去更新的,定时任务刷新就有一个问题。如果所有首页的Key失效时间都是12小时,中午12点刷新的,我零点 有个秒杀活动大量用户涌入,假设当时每秒 6000 个请求,本来缓存在可以扛住每秒 5000 个请求,但是缓 存当时所有的Key都失效了。此时 1 秒 6000 个请求全部落数据库,数据库必然扛不住,它会报一下警,真 实情况可能DBA都没反应过来就直接挂了。此时,如果没用什么特别的方案来处理这个故障,DBA 很着 急,重启数据库,但是数据库立马又被新的流量给打死了。这就是我理解的缓存雪崩。
如何应对:
a、在业务数据量不是很大的情况下,可以设置缓存数据为永久存储。
b、基于不同的业务数据,设置不同的超时时间,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j6R33EBj-1606313432309)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201112092228645.png)]
缓存穿透:
**概念:**是指缓存和数据库中都没有的数据,而用户不断发起请求,每次都能绕开Redis直接打到数据库,数据 库也查不到,每次都这样,并发高点就容易崩掉了。
举个例子:我们数据库的 id 都是从1开始自增上去的,在不对参数做校验的情况下,如发起为id值为 -1 的 数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据 库。
如何处理:
a、其实,Redis集成Mybatis实现分布式缓存,已经解决了缓存穿透的问题。无论你查询的id在缓存和数 据库中是否存在,都会将其存入到缓存中,只不过value为null。当对上述的id值在数据库中做了添加操作 后,Redis缓存会将已经存在于缓存中id对应的数据清除。当再次发起对这个id的查询时,查询的自然也就是 刚存入数据库的数据。
b、在不使用Redis集成Mybatis实现分布式缓存的情况下,可以在接口层增加校验,比如用户鉴权校验, 参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等。
c、Redis
还有一个高级用法==布隆过滤器(Bloom Filter)==这个也能很好的防止缓存穿透的发生,他的原 理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好 了,存在你就去查了DB刷新KV再return。
缓存击穿:
**概念:**是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。
如何处理:设置热点数据永远不过期,或者加上互斥锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z7Sspax4-1606313432310)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201112085021200.png)]
标签作用:与指定dao共享同一个缓存
使用方法:
注意:和不能同时出现
a、memcache与tomcat整合,实现的是全局Session管理机制,也就是说整个服务器所有应用全部基于memcache管理。
b、Redis与应用整合,实现的是基于应用的Session管理,也就是说一个应用会话交给Redis管理。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CP759K1K-1606313432312)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201112093439765.png)]
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
spring:
session.store-type: redis
package com.example.springbootredisdemo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@Configuration
@EnableRedisHttpSession
//@EnableRedisHttpSession开启Redis全局会话管理
public class RedisSessionConfig {
}
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@RestController
public class SessionController {
@RequestMapping("/trs")
public void testRedisSession(HttpServletRequest request, HttpServletResponse response) throws IOException {
List<String> list = (List<String>) request.getSession().getAttribute("list");
if(list==null){
list = new ArrayList<>();
}
list.add("xxx");
request.getSession().setAttribute("list",list);
response.getWriter().println("Session:" + request.getSession().getId());
response.getWriter().println("counts:" + list.size());
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7rLfWoNo-1606313432313)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201112111019093.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V6EZvR7R-1606313432314)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201112111055270.png)]
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.5.RELEASEversion>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>springboot-redis-demoartifactId>
<version>0.0.1-SNAPSHOTversion>
<packaging>warpackaging>
<name>springboot-redis-demoname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.apache.tomcat.embedgroupId>
<artifactId>tomcat-embed-jasperartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-redisartifactId>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>1.3.2version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<fork>truefork>
<jvmArguments>-Dfile.encoding=UTF-8jvmArguments>
<mainClass>com.example.springbootredisdemo.SpringbootRedisDemoApplicationmainClass>
configuration>
plugin>
plugins>
build>
project>
spring:
datasource:
username: root
password:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mode: LEGACYHTML5
encoding: UTF-8
cache: false
session.store-type: redis
redis:
database: 0
host: localhost
port: 6379
password:
timeout: 300
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
mybatis:
mapper-locations: classpath:mapping/*Mapper.xml
type-aliases-package: com.example.entity
#showSql
logging:
level:
com:
example:
mapper : debug
package com.example.springbootredisdemo.cache;
import com.example.springbootredisdemo.util.ApplicationContextUtil;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 重写Mybatis cache实现redis缓存
* 不能交给工厂管理
* */
public class RedisCache implements Cache {
//必须定义这个String类型的id,因为这个id表示当前加入缓存的namespace;
private String id;
public RedisCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
//放入缓存
@Override
public void putObject(Object key, Object value) {
//获取redisTemplate对象
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
//存储缓存数据
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置HashKey序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//hash模型
redisTemplate.opsForHash().put(id,key.toString(),value);
}
//从缓存中获取
@Override
public Object getObject(Object key) {
//获取redisTemplate对象
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置HashKey序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate.opsForHash().get(id.toString(),key.toString());
}
//从缓存中移除
//真正使用过程中,这个方法并不会被用到
@Override
public Boolean removeObject(Object key) {
return null;
}
//清除缓存
@Override
public void clear() {
//获取redisTemplate对象
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
redisTemplate.delete(id);
}
//缓存命中率计算
@Override
public int getSize() {
// 获取redisTemplate对象
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate.opsForHash().size(id.toString()).intValue();
}
/*
* ReadWriteLock读写锁 表示:读写之间互斥,读读之间不互斥,写写之间不互斥
* 区别于Synchronized 表示:读读之间互斥,写写之阿互斥,读写之间互斥
* 因此ReadWriteLock效率比Synchronized高
* 对于缓存,只有写操作,没有写操作
* */
@Override
public ReadWriteLock getReadWriteLock() {
return new ReentrantReadWriteLock();
}
}
package com.example.springbootredisdemo.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* redis配置类
* @program: redisdemo
* @Author: david
* @Description:
*/
//RedisConfig中重写RedisTemplate类(why?)
@Configuration
@EnableCaching //开启注解
public class RedisConfig extends CachingConfigurerSupport {
/**
* retemplate相关配置
* @param factory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(factory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 值采用json序列化
template.setValueSerializer(jacksonSeial);
//使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
// 设置hash key 和value序列化模式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jacksonSeial);
template.afterPropertiesSet();
return template;
}
/**
* 对hash类型的数据操作
*
* @param redisTemplate
* @return
*/
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
/**
* 对redis字符串类型数据操作
*
* @param redisTemplate
* @return
*/
@Bean
public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForValue();
}
/**
* 对链表类型的数据操作
*
* @param redisTemplate
* @return
*/
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
/**
* 对无序集合类型的数据操作
*
* @param redisTemplate
* @return
*/
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
/**
* 对有序集合类型的数据操作
*
* @param redisTemplate
* @return
*/
@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
}
package com.example.springbootredisdemo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@Configuration
@EnableRedisHttpSession
//@EnableRedisHttpSession开启Redis全局会话管理
public class RedisSessionConfig {
}
package com.example.springbootredisdemo.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
//在自定义的类中获取工厂中已经创建好的某些对象
@Configuration
public class ApplicationContextUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
//根据bean id获取对象
public static Object getBean(String id){
return context.getBean(id);
}
//根据bean 类型获取对象
public static Object getBean(Class clazz){
return context.getBean(clazz);
}
//根据bean id和类型获取对象
public static Object getBean(String id,Class clazz){
return context.getBean(id,clazz);
}
}
package com.example.springbootredisdemo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@RestController
public class RedisSessionController {
@RequestMapping("/trs")
public void testRedisSession(HttpServletRequest request, HttpServletResponse response) throws IOException {
List<String> list = (List<String>) request.getSession().getAttribute("list");
if(list==null){
list = new ArrayList<>();
}
list.add("xxx");
request.getSession().setAttribute("list",list);
response.getWriter().println("Session:" + request.getSession().getId());
response.getWriter().println("counts:" + list.size());
}
}
主从复制,是指将一台Redis服务器的数据(主服务器master)复制到其他的Redis服务器(从服务器slave)。数据的复制是单向的,只能由主节点到从节点。默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。(主从之间是1 : n关系,n >= 0)
数据冗余备份;
但不能解决故障的自动转移。
(1)开启主从复制
主从复制的开启,完全是在从节点发起的,主节点被动接收从节点的请求并做出相应处理就好。从节点开启主从复制,有3种方式:
(2)关闭主从复制
通过slaveof 命令建立主从复制关系以后,可以通过****slaveof no one****断开。从节点断开复制后,不会删除已有的数据,只是不再接受主节点新的数据变化。
主从复制包括三个阶段:
a、建立连接阶段:保存主节点信息、建立socket连接、发送ping命令、身份验证、发送从节点端口信息
b、数据同步阶段:全量复制和部分复制
c、命令传播阶段:发送写命令、维持心跳机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zW7Dh594-1606313432316)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201112135250873.png)]
Redis的哨兵(sentinel) 系统用于管理多个 Redis 服务器,该系统执行以下三个任务:
a、监控(Monitoring): 哨兵(sentinel) 会不断地检查你的Master和Slave是否运作正常。
b、提醒(Notification):当被监控的某个 Redis出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。
c、自动故障迁移(Automatic failover):当一个Master不能正常工作时,哨兵(sentinel) 会开始一次自动故障迁移操作,它会将失效Master的其中一个Slave升级为新的Master, 并让失效Master的其他Slave改为复制新的Master; 当客户端试图连接失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用Master代替失效Master。
哨兵(sentinel) 是一个分布式系统,你可以在一个架构中运行多个哨兵(sentinel) 进程,这些进程使用流言协议(gossipprotocols)来接收关于Master是否下线的信息,并使用投票协议(agreement protocols)来决定是否执行自动故障迁移,以及选择哪个Slave作为新的Master。
每个哨兵(sentinel) 会向其它哨兵(sentinel)、master、slave定时发送消息,以确认对方是否”活”着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的”主观认为宕机” Subjective Down,简称sdown).
若“哨兵群”中的多数sentinel,都报告某一master没响应,系统才认为该master"彻底死亡"(即:客观上的真正down机,Objective Down,简称odown),通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置。
虽然哨兵(sentinel) 释出为一个单独的可执行文件 redis-sentinel ,但实际上它只是一个运行在特殊模式下的 Redis 服务器,你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动哨兵(sentinel)。
哨兵机制其实就是带有故障自动转移功能的主从式架构;
主要解决的是:
a、数据冗余备份;
b、故障自动转移;
但不能解决现有系统单节点并发压力和物理上限的问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xW39Kt9f-1606313432317)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201112140207946.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SAEyZYvj-1606313432318)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201112140052873.png)]
1):每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。
2):如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线。
3):如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。
4):当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。
5):在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令 。
6):当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 。
7):若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。
若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JoAAndvH-1606313432319)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20201112141856941.png)]
a、数据冗余备份;
b、故障自动转移;
c、单节点并发压力和物理上限的问题。
失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用Master代替失效Master。
哨兵(sentinel) 是一个分布式系统,你可以在一个架构中运行多个哨兵(sentinel) 进程,这些进程使用流言协议(gossipprotocols)来接收关于Master是否下线的信息,并使用投票协议(agreement protocols)来决定是否执行自动故障迁移,以及选择哪个Slave作为新的Master。
每个哨兵(sentinel) 会向其它哨兵(sentinel)、master、slave定时发送消息,以确认对方是否”活”着,如果发现对方在指定时间(可配置)内未回应,则暂时认为对方已挂(所谓的”主观认为宕机” Subjective Down,简称sdown).
若“哨兵群”中的多数sentinel,都报告某一master没响应,系统才认为该master"彻底死亡"(即:客观上的真正down机,Objective Down,简称odown),通过一定的vote算法,从剩下的slave节点中,选一台提升为master,然后自动修改相关配置。
虽然哨兵(sentinel) 释出为一个单独的可执行文件 redis-sentinel ,但实际上它只是一个运行在特殊模式下的 Redis 服务器,你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动哨兵(sentinel)。
哨兵机制其实就是带有故障自动转移功能的主从式架构;
主要解决的是:
a、数据冗余备份;
b、故障自动转移;
但不能解决现有系统单节点并发压力和物理上限的问题。
[外链图片转存中…(img-xW39Kt9f-1606313432317)]
[外链图片转存中…(img-SAEyZYvj-1606313432318)]
1):每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。
2):如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线。
3):如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。
4):当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。
5):在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令 。
6):当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 。
7):若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。
若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
[外链图片转存中…(img-JoAAndvH-1606313432319)]
a、数据冗余备份;
b、故障自动转移;
c、单节点并发压力和物理上限的问题。