1、布隆过滤器BloomFilter
需求:现有50亿个电话号码,现有10万个电话号码,如何要快速准确的判断这些电话号码是否已经存在?
布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的,它实际上是一个很长的二进制数组+一系列随机hash算法映射函数,主要用于判断一个元素是否在集合中。
通常我们会遇到很多要判断一个元素是否在某个集合中的业务场景,一般想到的是将集合中所有元素保存起来,然后通过比较确定。
链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路,但是随着集合中元素的增加,我们需要的存储空间也会呈现线性增长,最终达到瓶颈。同时检索速度也越来越慢,上:述三种结构的检索时间复杂度分别为O(n),O(logn),O(1)。这个时候,布隆过滤器(Bloom Filter)就应运而生
由—个初值都为零的bit数组和多个哈希函数构成,用来快速判断某个数据是否存在,本质就是判断一个具体数据,存不存在一个大的集合中,布隆过滤器是一种类似set的数据结构,只是统计结果不太准确。
1.1、布隆过滤器的特点
- 高效地插入和查询,占用空间少,返回的结果是不确定性的。
- 一个元素如果判断结果为存在的时候,元素不一定存在,但是判断结果为不存在的时候则一定不存在。
- 布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。
- 误判只会发生在过滤器没有添加过的无素对于添加过的元素不会发生误判。
1.2、布隆过滤器的用途
1.2.1、解决缓存穿透的问题
级存穿透是什么:一般情况下,先查询缓存redis是否有该条数据,缓存中没有时,再查询数据库。当数据库也不存在该条数据时,每次查询都要访问数据库,这就是缓存穿透。
缓存透带来的问题:当有大量请求查询数据库不存在的数据时,就会给数据库带来压力,甚至会拖垮数据库。
解决方式:可以使用布隆过滤器解决缓存穿透的问题,把已存在数据的key存在布隆过滤器中,相当于redis前面挡着一个布隆过滤器。
当有新的请求时,先到布隆过滤器中查询是否存在,如果布隆过滤器中不存在该条数据则直接返回,如果布隆过滤器中已存在,才去查询缓存redis,如果redis里没查询到则穿透到Mysql数据库。
1.2.2、解决黑、白名单校验
发现存在黑名单中的,就执行特定操作。比如:识别垃圾邮件,只要是邮箱在黑名单中的邮件,就识别为垃圾邮件。假设黑名单的数量是数以亿计的,存放起来就是非常耗费存储空间的,布隆过滤器则是一个较好的解决方案。把所有黑名单都放在布隆过滤器中,在收到邮件时,判断邮件地址是否在布隆过滤器中即可。白名单也是如此。
1.3、布隆过滤器的原理
1.3.1、哈希函数
哈希函数的概念是:将任意大小的输入数据转换成特定大小的输出数据的函数,转换后的数据称为哈希值或哈希编码,也叫散列值
如果两个散列值是不相同的(根据同一函数)那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数。
散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的,但也可能不同,这种情况称为“散列碰撞(collision)”,所以用hash表存储大数据量时,空间效率还是很低,当只有一个hash函数时,还很容易发生哈希碰撞。
System.out.println("Aa".hashCode());
System.out.println("BB".hashCode());
System.out.println("柳柴".hashCode());
System.out.println("柴柕".hashCode());
2112
2112
851553
851553
1.3.2、布隆过滤器原理
布隆过滤器原理:布隆过滤器(Bloom Filter)是一种专门用来解决去重问题的高级数据结构,实质就是一个初值都为零的大型位数组和多个哈希函构成,用来快速判断某个数据是否存在,但是跟HyperLogLog一样,它也一样有那么一点点不精确,也存在一定的误判概率。
- 添加key时 :当我们向布隆过滤器中添加数据时,为了尽量地址不冲突,会使用多个hash函数对key进行运算,算得一个下标索引值,然后对位数组长度进行取模运算得到一个位置,每个hash 函数都会算得一个不同的位置,再把位数组的这几个位置都置为1,就完成了add操作。
当有变量被加入集合时,通过N个映射函数将这个变量映射成位图中的N个点,把它们置为1(假定有两个变量都通过3个映射函数)。
- 查询key时:当向布隆过滤器查询某个key是否存在时,先把这个key通过相同的多个hash 函数进行运算,查看对应的位置是否都为1,只要有一个位为0,那么说明布隆过滤器中这个key不存在,如果这几个位置全都是1,那么说明极有可能存在,因为会存在误判,因为映射函数本身就是散列函数,散列函数是会有碰撞的。
为什么说是可能存在,而不是一定存在呢?
那是因为映射函数本身就是散列函数,散列函数是会有碰撞的。
- 误判: 就比如我们在add了字符串wmyskxz数据之后,很明显下面1/3/5这几个位置的1是因为第一次添加的wmyskxz而导致的;此时我们查询一个没添加过的不存在的字符串inexistent-key,它有可能计算后坑位也是1/3/5,这就是误判了…
1.3.3、布隆过滤器如何解决缓存穿透?
正是基于布隆过滤器的快速检测特性,我们可以在把数据写入数据库时,使用布隆过滤器做个标记,当缓存缺失后,应用查询数据库时,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用再去数据库中查询了。这样一来,即使发生缓存穿透了,大量请求只会查询Redis和布隆过滤器,而不会积压到数据库,也就不会影响数据库的正常运行。布隆过滤器可以使用Redis实现,本身就能承担较大的并发访问压力。
当我们向布隆过滤器中添加数据时,为了尽量地址不冲突,会使用多个hash函数对key进行运算,算得一个下标索引值,然后对位数组长度进行取模运算得到一个位置,每个hash 函数都会算得一个不同的位置,再把位数组的这几个位置都置为1,就完成了add操作。
例如,我们添加一个字符串wmyskxz
1.3.4、布隆过滤器误判率,为什么不要删除
布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit位被多次映射且置1。但是这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素。
特性
- 一个元素判断结果为没有时则一定没有,如果判断结果为存在的时候元素不一定存在。
- 布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。
- 使用时最好不要让实际元素数量远大于初始化数量。
- 当实际元素数量超过初始化数量时,应该对布隆过滤器进行重建,重新分配一个size更大的过滤器,再将所有的历史元素批量 add进行
1.3.5、布隆过滤器优缺点
-
优点
- 高效地插入和查询,占用空间少不能删除元素
- 因为删掉元素会导致误判率增加,因为hash冲突同一位值,可能是多个元素共享的。
-
缺点
- 你删除一个元素的同时可能也把其它的删除了。(存在误判)
- 不同的数据可能出来相同的hash值
-
布谷鸟过滤器:
为了解决布隆过滤器不能删除元素的问题,布谷鸟过滤器横空出世。谊文《Cuckoo Filter Better Than Bloom》
作者将布谷鸟过滤器和布隆过滤器进行了深入的对比。相比布谷鸟过滤器而言布隆过滤器有以下不足:查询性能弱、空间利用效率低、不支持反向操作(删除)以及不支持计数。
2、缓存预热+缓存雪崩+缓存击穿+缓存穿透
2.1、缓存雪崩:
缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
解决方案:
- redis缓存集群实现高可用
- 主从+哨兵
- Redis Cluster
- ehcache本地缓存+Hystrix或者阿里sentinel限流&降级以及开启Redis持久化机制aof/rdb,尽快恢复缓存集群
解决Z
2.2、缓存穿透
缓存穿透是指用户查询请求去查询一条记录,先redis后mysql发现都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增,redis变成了一个摆设,这种现象我们称为缓存穿透。
2.2.1、 方案1::空对象缓存或者缺省值
一旦发生缓存穿透,我们就可以针对查询的数据,在Redis中缓存一个空值或是和业务层协商确定的缺省值(例如,库存的缺省值可以设为0)。紧接着,应用发送的后续请求再进行查询时,就可以直接从Redis中读取空值或缺省值,返回给业务应用了,避免了把大量请求发送绐数据库处理,保持了数据库的正常运行。
黑客会对你的系统选行攻击,拿一个不存在的id去查询数据,会该生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉。
- id相同打你系统,第一次打到mysql,空对象缓存后,第二次就返回缓存数据了,避免mysql被攻击,不用再到数据库中去走一圈了
- id不同打你系统: 由于存在空对象缓存和缓存回写(看自己业务不限死),redis中的无关紧要的key也会越写越多(记得设置redis过期时间),空对象缓存或者缺省值,对这种情况不能很好的解决。
2.2.2、方案2: Google布隆过滤器Guava解决缓存穿透
Guava中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器
官网地址:https://github.com/google/guava/blob/master/guava/src/com/google/common/hash/BloomFilter.java
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
public class BloomFilterDemo {
public static final int _1w = 10000;//布隆过滤器里预计要插入多少数据
public static int size = 100 * _1w;//误判率,它越小误判的个数也就越少(思考,是不是可以设置的无限小,没有误判岂不更好)public static double fpp = 0.03;
public static double fpp = 0.03;
public void bloomFilter() {
//创建布隆过滤器对象
BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 100);//判断指定元素是否存在
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));//将元素添加进布隆过滤器
filter.put(1);
filter.put(2);
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
}
public void bloomFilter2() {
//构建布隆过滤器
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size);
//1先往布隆过滤器里面插入100万的样本数据
for (int i = 0; i < size; i++) {
bloomFilter.put(i);
}
List<Integer> listSample = new ArrayList<>(size);
//2这100万的样本数据,是否都在布隆过滤器里面存在?
for (int i = 0; i < size; i++) {
if (bloomFilter.mightContain(i)) {
listSample.add(i);
continue;
}
}
List<Integer> list = new ArrayList<>(10 * _1w);
//3故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里,误判率演示List<Integer> list = new ArrayList<>( initialCapacity: 10 *_1w);
for (int i = size + 1; i < size + 100000; i++) {
if (bloomFilter.mightContain(i)) {
System.out.println(i + "\t" + "被误判了.");
list.add(i);
}
}
System.out.println("存在的数量:" + list.size());
}
public static void main(String[] args) {
new BloomFilterDemo().bloomFilter2();
}
}
默认的是100w的数据,给的bit位数是7298440,误判率0.03,hash函数5个
100w的数据,给的bit位数是9585058,误判率0.01,hash函数7个
Guava缺点说明:Guava提供的布隆过滤器的实现还是很不错的(想要详细了解的可以看一下它的源码实现),但是它有一个重大的缺陷就是只能单机使用,而现在互联网一般都是分布式的场景,为了解决这个问题,我们就需要用到Redis中的布隆过滤器了
2.2.3、方案3: Redis布隆过滤器解决缓存穿透
白名单案例过滤器:
让布隆过滤器作白名单使用:白名单里面有的才让通过,没有直接返回。但是存在误判,由于误判率很小,1%的打到mysql,可以接受
使用注意:所有key都需要往redis和bloomfilter里面放入
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
public class RedisBloomFilterDemo {
public static final int _1w = 10000;//布隆过滤器里预计要插入多少数据
public static int size = 100 * _1w;//误判率,它越小误判的个数也就越少(思考,是不是可以设置的无限小,没有误判岂不更好)
public static double fpp = 0.03;
static RedissonClient redissonclient = null;
static RBloomFilter rBLoomFilter = null;
@Resource
private RestTemplate restTemplate;
static {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.111.147:6379").setDatabase(0);//构造redisson
redissonclient = Redisson.create(config);
//通过redisson构造rBLoomFilter
rBLoomFilter = redissonclient.getBloomFilter("phoneListBloomFilter", new StringCodec());
rBLoomFilter.tryInit(size, fpp);
//1.测试布隆过滤器有+redis有
rBLoomFilter.add("10086");
redissonclient.getBucket("10086", new StringCodec()).set("chinamobile10086");
// 2测试﹑布隆过滤器有+redis无
//rBLoomFilter.add("10087");
// 3测试,布隆过滤器无+redis无
}
private static String getPhoneListById(String IDNumber) {
String result = null;
if (IDNumber == null) {
return null;
}
//1先去布隆过滤器里面查
if (rBLoomFilter.contains(IDNumber)) {
//2 布隆过滤器里有,再去redis里面查询
RBucket<String> rBucket = redissonclient.getBucket(IDNumber, new StringCodec());
result = rBucket.get();
if (result != null) {
return "i come from redis: " + result;
} else {
result = getphonelistByMysQL(IDNumber);
//重新将数据更新回redis
if (result == null) {
return null;
}
//重新将数据更新回redis
redissonclient.getBucket(IDNumber, new StringCodec()).set(result);
return "i come from mysql: " + result;
}
}
return result;
}
private static String getphonelistByMysQL(String idNumber) {
return "chinamobile" + idNumber;
}
public static void main(String[] args) {
String phoneListById = getPhoneListById("10086");
// String phoneListById = getPhoneListById("10087");//请测试执行2次
// string phoneListById = getPhoneListById( "10088" );
System.out.println("------查询出来的结果:" + phoneListById);
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
redissonclient.shutdown();
}
}
}
白名单执行流程
黑名单应用场景:已读去重
在centos7下布隆过滤器2种安装方式
- docker安装RedisBloom插件
Redis在4.0之后有了插件功能(Module),可以使用外部的扩展功能,可以使用RedisBloom 作为Redis布隆过滤器插件。
docker run -p 6379:6379 --name=redis6379bloom -d redislabs/rebloom
docker exec -it redis6379bloom /bin/bash - 编译安装
布隆过滤器常用操作命令
- bf.reserve key error_rate的值initial_size的值,默认的error_rate是0.01,默认的initial_size是100。
- bf.add key值,添加值
- bf.exists key值,判断是否存在
- bf.madd一次添加多个元素
- bf.mexists一次查询多个元素是否存在
2.3、缓存击穿
缓存击穿:当大量的请求凤时查询一个key时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去简兽晚就是热点key突然失效了,暴打mysql,会造成某一时刻数据库请求量过大,压力剧增,服务宕机
2.3.1、方案2:对于访问频繁的热点key,干脆就不设置过期时间方案
2.3.2、方案3:互斥独占锁防止击穿
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
public User findUserById2(Integer id)
{
User user = null;
String key = CACHE_KEY_USER+id;
//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql
user = (User) redisTemplate.opsForValue().get(key);
if(user == null)
{
//2 大厂用,对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql
synchronized (UserService.class){
user = (User) redisTemplate.opsForValue().get(key);
//3 二次查redis还是null,可以去查mysql了(mysql默认有数据)
if (user == null) {
//4 查询mysql拿数据
user = userMapper.selectByPrimaryKey(id);//mysql有数据默认
if (user == null) {
return null;
}else{
//5 mysql里面有数据的,需要回写redis,完成数据一致性的同步工作
redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
}
}
}
}
return user;
}
2.3.3、双缓存解决缓存击穿
淘宝聚划算功能实现+防止缓存击穿、高并发的淘宝聚划算案例落地
分析过程:
- 100%高并发,绝对不可以用mysql实现
- 先把mysq|里面参加活动的数据抽取进redis,一般采用定时器扫描来决定上线活动还是下线取消。
- 支持分页功能,一页20条记录
redis里面什么样子的数据类型支持上述功能? list或者zset(更多的是排行榜类似的)
一般场景的解决方式:
@RestController
@Slf4j
@Api("聚划算功能控制层")
public class JHSProductController {
@Resource
private RedisTemplate redisTemplate;
@RequestMapping(value = "/product/find", method = RequestMethod.GET)
@ApiOperation("按照分页和每页显示容量,点击查看")
public List<Product> find(int page, int size) {
List<Product> list = null;
long start = (page - 1) * size;
long end = start + size - 1;
try {
//采用redis list数据结构的Lrange命令实现分页查询
list = this.redisTemplate.opsForList().range(Constants.JHS_KEY, start, end);
if (CollectionUtil.isEmpty(list)) {
// ToDo走DB查询
}
log.info("查询结果:", list);
} catch (Exception ex) {
//这里的异常,一般是redis瘫痪,或 redis 网络timeout
log.error("exception: ", ex);
//ToDo 走DB查询
}
return list;
}
}
@Service
@Slf4j
public class JHSTaskService {
@Resource
private RedisTemplate redisTemplate;
@PostConstruct
public void initJHS() {
log.info("启动定时器淘宝聚划算功能模拟...." + DateUtil.now());
new Thread(() -> {
//模拟定时器,定时把数据库的特价商品,刷新到redis中
while (true) {
//模拟从数据库读oo件特价商品,用于加载到聚划算的页面中
List<Product> list = this.products();
//采用redis list数据结构的Lpush来实现存储
this.redisTemplate.delete(Constants.JHS_KEY);
//Lpush命令
this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY, list);//间隔一分钟执行一遍
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("runJhs定时刷新..............");
}
}, "t1").start();
}
public List<Product> products() {
List<Product> list = new ArrayList<>();
for (int i = 1; i <= 20; i++) {
Random rand = new Random();
int id = rand.nextInt(10000);
Product obj = new Product((long) id, "product" + i, i, "detail");
list.add(obj);
}
return list;
}
}
实时高并发场景下的解决方式
因为在删除旧数据和加入新数据的过程不是原子性的,并且持续有高并发的请求,所以这里的解决的方式使用双缓存的机制,一个缓存的时间短为主缓存A,一个缓存的时间长为从缓存B,两个缓存数据一致,先查询缓存A,缓存A查不到,再去查询缓存B,当更新数据的时候,会会先更新缓存B,然后再去更新缓存A,当缓存A到期或者被删除要更换新的数据时,如果有持续的请求过来,请求缓存A,这时可能缓存A刚删除完数据,并没有加上,所以就会请求缓存B返回对应数据。
@RestController
@Slf4j
@Api("聚划算功能控制层")
public class JHSABProductController {
@Resource
private RedisTemplate redisTemplate;
@RequestMapping(value = "/product/find", method = RequestMethod.GET)
@ApiOperation("按照分页和每页显示容量,点击查看")
public List<Product> find(int page, int size) {
List<Product> list = null;
long start = (page - 1) * size;
long end = start + size - 1;
try {
//采用redis list数据结构的Lrange命令实现分页查询
list = this.redisTemplate.opsForList().range(Constants.JHS_KEY, start, end);
if (CollectionUtil.isEmpty(list)) {
log.info("=========A缓存已经失效了,记得人工修补,B缓存自动延续5天");
//用户先查询缓存A(上面的代码),如果缓存A查询不到(例如,更新缓存的时候删除了),
//再查询缓存B
this.redisTemplate.opsForList().range(Constants.JHS_KEY_B, start, end);
}
log.info("查询结果:", list);
} catch (Exception ex) {
//这里的异常,一般是redis瘫痪,或 redis 网络timeout
log.error("exception: ", ex);
//ToDo 走DB查询
}
return list;
}
}
@Service
@Slf4j
public class JHSABTaskService {
@Resource
private RedisTemplate redisTemplate;
@PostConstruct
public void initJHS() {
log.info("启动定时器淘宝聚划算功能模拟...." + DateUtil.now());
new Thread(() -> {
//模拟定时器,定时把数据库的特价商品,刷新到redis中
while (true) {
//模拟从数据库读取100件特价商品用于加载到聚划算的页面中
List<Product> list = this.products();
//先更新B缓引
this.redisTemplate.delete(Constants.JHS_KEY_B);
this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY_B, list);
this.redisTemplate.expire(Constants.JHS_KEY_B, 20L, TimeUnit.DAYS);
// 再更新A缓存
this.redisTemplate.delete(Constants.JHS_KEY_A);
this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY_A, list);
this.redisTemplate.expire(Constants.JHS_KEY_A, 15L, TimeUnit.DAYS);
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("runJhs定时刷新..............");
}
}, "t1").start();
}
public List<Product> products() {
List<Product> list = new ArrayList<>();
for (int i = 1; i <= 20; i++) {
Random rand = new Random();
int id = rand.nextInt(10000);
Product obj = new Product((long) id, "product" + i, i, "detail");
list.add(obj);
}
return list;
}
}
小总结:
2.3、缓存预热
缓存预热这个应该是一个比较常见的概念,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
2.4、缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
2.5、缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。