1、缓存雪崩
① redis 主机挂了,Redis全盘崩溃
② 比如缓存中有大量数据同时过期
解决:
1、Redis缓存集群实现高可用
(1)主从+哨兵
(2)Redis Cluster
2、ehcache本地缓存+Hystrix或者阿里sentinel限流&降级
3、开启Redis持久化机制aof/rdb,尽快恢复缓存集群
2、缓存穿透
2.1、是什么
请求去查询一条记录,先redis后mysq发现都查询不到该条记录, 但是请求每次都会打到数据库上面去,导致后台数据库压力暴增, 这种现象我们称为缓存穿透,这个 redis变成了一个摆设。。。。。
简单说就是本来无一物,既不在 Redis缓存中,也不在数据库中
2.2、危害
第一次来查询后,一般我们有回写 redis机制
第二次来查的时候redis就有了,偶尔出现穿透现象一般情况无关紧要
2.3、解决
方案一:空对象缓存或者缺省值
一般ok:一旦发生缓存穿透,我们就可以针对查询的数据,在 Redis中缓存一个空值或是和业务层协商确定的缺省值 (例如,库存的缺省值可以设为0)。紧接着,应用发送的后续请求再进行查询时,就可以直接从 redis中读 取空值或缺省值,返回给业务应用了,避免了把大量请求发送给数据库处理,保持了数据库的正常运行。
恶意攻击无效:黑客会对你的系统进行攻击,拿一个不存在的id 去查询数据,会产生大量的请求到数据库去查询。 可能会导致你的数据库由于压力过大而宕掉
id相同打你系统
第一次打到 mysql,空对象缓存后第二次就返回null了, 避免 mysql被攻击,不用再到数据库中去走一圈了
id不同打你系统
由于存在空对象缓存和缓存回写(看自己业务不限死) ,redis中的无关紧要的key也会越写越多(记得设置redis过期时间)
方案二:Google布隆过滤器Guava解决缓存穿透
Guava中布隆过滤器的实现算是比较权威的, 所以实际项目中我们不需要手动实现一个布隆过滤器
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
package com.shuidi.redis.bloomfilter;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import io.swagger.models.auth.In;
import java.util.ArrayList;
import java.util.List;
/**
* @author shizan
* @Classname GuavaBloomfilterDemo
* @Description TODO
* @Date 2022/4/1 11:42 下午
*/
public class GuavaBloomfilterDemo {
public static final int _1w = 10000;
/**
* 布隆过滤器里预计要插入多少数据
*/
public static int size = 100 * _1w;
/**
* 误判率,它越小误判的个数也就越小(思考,是不是可以设置的无限小,没有误判岂不更好)
*/
public static double fpp = 0.03;
/**
* helloworld入门
*/
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;
}
}
System.out.println("存在的数量:" + listSample.size());
//3.故意取10万个不在过滤器里的值,看看有多少个会被认为在过滤器里,误判率演示
List<Integer> list = new ArrayList<>(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 GuavaBloomfilterDemo().bloomFilter2();
}
}
输出:
存在的数量:1000000
1000029 被误判了
1000049 被误判了
1000092 被误判了
。。。。。
。。。。。
1099882 被误判了
1099947 被误判了
误判的数量:3033
3033/100000≈0.03
Guava‘ BloomFilter源码剖析
精度提高hash函数使用的个数会增多,效率越低
布隆过滤器说明
方案三:Redis布隆过滤器解决缓存穿透
Guava缺点说明:
Guava提供的布隆过滤器的实现还是很不错的(想要详细了解的可以看一下它的源码实现),但是它有一个重大的缺陷就是只能单机使用 ,而现在互联网一般都是分布式的场景。
为了解决这个问题,我们就需要用到 Redis中的布隆过滤器了
案例:白名单过滤器
白名单架构说明:
误判率问题,但是概率小可以接受,不能从布隆过滤器删除
全部合法的key都需要放入过滤器+redis里面,不然数据就是返回null
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.redisson.config.Config;
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @author shizan
* @Classname RedissonBloomFilterDemo
* @Description redis布隆过滤器demo
* @Date 2022/4/3 1:03 上午
*/
public class RedissonBloomFilterDemo {
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
RedisTemplate redisTemplate;
static {
Config config = new Config();
config.useSingleServer().setAddress("redis://172.16.119.100:6379").setDatabase(0);
//构造redisson
redissonClient = Redisson.create(config);
//通过redisson构造BloomFilter
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);
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 phoneListById1 = getPhoneListById("10086");
System.out.println("---查询出来的结果:" + phoneListById1);
String phoneListById2 = getPhoneListById("10087");
System.out.println("---查询出来的结果:" + phoneListById2);
String phoneListById3 = getPhoneListById("10088");
System.out.println("---查询出来的结果:" + phoneListById3);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
redissonClient.shutdown();
}
}
输出结果:
---查询出来的结果:i come from redis: chinamobile10086
---查询出来的结果:i come from mysql: chinamobile10087
---查询出来的结果:null
重要总结
黑名单使用:
2.4、在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
redis-cli
布隆过滤器常用操作命令
127.0.0.1:6379> BF.ADD filter 10086 #添加10086到布隆过滤器filter
(integer) 1
127.0.0.1:6379> BF.EXISTS filter 10086 #判断10086是否在布隆过滤器
(integer) 1
127.0.0.1:6379> BF.EXISTS filter 10087
(integer) 0
127.0.0.1:6379> BF.MADD filter 10088 10089 #一次添加多个值
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> BF.MEXISTS filter 10088 10010 #一次判断多个值是否存在
1) (integer) 1
2) (integer) 0
#重置误判率
bf.reserve key error_rate 的值 initial_size的值 默认的error_rate是0.01,默认的initial_size是100
② 编译安装
3、缓存击穿
3.1、是什么
大量的请求同时查询一个key时, 此时这个key正好失效了,就会导致大量的请求都打到数据库上面去
简单说就是热点key突然失效了,暴打mysql
3.2、危害
会造成某一时刻数据库请求量过大,压力剧增。
3.3、解决
方案一:
缓存击穿 | 热点key失效 | 互斥更新、随机退避、差异失效时间 |
方案二:
对于访问频繁的热点key,干脆就不设置过期时间
方案三:
互斥独占锁防止击穿
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直 接走缓存。
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;
}
3.4、案例
淘宝聚划算功能实现+防止缓存击穿
分析过程:
步骤 | 说明 |
1 | 100%高并发,绝对不可以用 mysql实现 |
2 | 先把 mysql里面参加活动的数据抽取进redis,一般采用定时器扫描来决定上线活动还是下线取消。 |
3 | 支持分页功能,一页20条记录 |
请大家思考, redis里面什么样子的数据类型支持上述功能? |
redis数据类型选型
list
springboot + redis实现高并发的淘宝聚划算业务
代码一:
@Service
@Slf4j
public class JHSTaskService {
@Autowired
private RedisTemplate redisTemplate;
@PostConstruct
public void initJHS() {
log.info("启动定时器淘宝聚划算功能模拟......" + DateUtil.now());
new Thread(()->{
while (true){
List<Product> list = this.products();
this.redisTemplate.delete(Constants.JHS_KEY);
this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY,list);
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("runJhs定时更新.......");
}
},"t1").start();
}
/**
* 模拟从数据库读取20件特价商品,用于加载到聚划算的页面中
*
* @return
*/
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 product = new Product((long) id, "product:" + i, i, "detail:" + i);
list.add(product);
}
return list;
}
}
@RestController
@Slf4j
public class JHSProductController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(value = "product/find", method = RequestMethod.GET)
public List<Product> find(int page, int size) {
List<Product> list = null;
long start = (page - 1) * size;
long end = start + size - 1;
list = this.redisTemplate.opsForList().range(Constants.JHS_KEY, start, end);
if (CollectionUtil.isEmpty(list)) {
//TODO:走DB查询
}
log.info("查询结果:{}", list);
return list;
}
}
代码二:
@Service
@Slf4j
public class JHSTaskABService {
@Autowired
private RedisTemplate redisTemplate;
@PostConstruct
public void initJHS() {
log.info("启动定时器淘宝聚划算功能模拟......" + DateUtil.now());
new Thread(() -> {
while (true) {
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();
}
/**
* 模拟从数据库读取20件特价商品,用于加载到聚划算的页面中
*
* @return
*/
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 product = new Product((long) id, "product:" + i, i, "detail:" + i);
list.add(product);
}
return list;
}
}
@RestController
@Slf4j
public class JHSABProductController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(value = "product/findab", method = RequestMethod.GET)
public List<Product> find(int page, int size) {
List<Product> list = null;
long start = (page - 1) * size;
long end = start + size - 1;
try {
list = this.redisTemplate.opsForList().range(Constants.JHS_KEY_A, start, end);
if (CollectionUtil.isEmpty(list)) {
log.info("=========A缓存已经失效了,记得人工修补,B缓存自动延续5天");
//用户先查询缓存A(上面代码),如果缓存A查询不到(例如,更新缓存的时候删除了),再查询缓存B
list=this.redisTemplate.opsForList().range(Constants.JHS_KEY_B, start, end);
}
log.info("查询结果:{}", list);
} catch (Exception e) {
//这里的异常,一般是redis瘫痪,或者redis网络timeout
log.error("exception:", e);
//TODO:走DB查询
}
return list;
}
}
4、总结
缓存问题 | 产生原因 | 解决方案 |
缓存更新方式 | 数据变更、缓存时效性 | 同步更新、失效更新、异步更新、定时更新 |
缓存不一致 | 同步更新失败、异步更新 | 增加重试、补偿任务、最终一致 |
缓存穿透 | 恶意攻击 | 空对象缓存、 bloomfilter过滤器 |
缓存击穿 | 热点key失效 | 互斥更新、随机退避、差异失效时间 |
缓存雪崩 | 缓存挂掉 | 快速失败熔断、主从模式、集群模式 |