目录
redis缓存预热
Redis 缓存预热是指在系统启动或者缓存失效后,提前将部分数据加载到 Redis 缓存中,以便用户在访问时能够快速获取数据,提高系统性能和响应速度。
-
全量加载: 在系统启动时,将所有需要缓存的数据从数据库或其他数据源加载到 Redis 中,确保缓存中包含了所有可能被访问的数据。这种方式适用于数据量较小、变动频率低的场景。
-
按需加载: 根据访问模式和数据访问频率,预先加载部分热门数据到 Redis 缓存中,以及时满足用户请求。这种方式适用于数据量较大、热点数据集中的场景。
-
定时加载: 设置定时任务,在系统空闲时段或低峰期,定期从数据库加载数据到 Redis 缓存中,以确保缓存数据的及时性和完整性。
-
异步加载: 在系统启动后,通过异步任务的方式逐步将数据加载到 Redis 缓存中,避免系统启动时的压力过大。
redis缓存雪崩
缓存雪崩是指在某个时间段,缓存中大量的数据同时失效或者因为某种原因都不可用,导致大量请求直接落到了后端数据库上,压力过大而导致数据库或其他服务崩溃,最终影响整个系统的稳定性和可用性。
缓存雪崩一般发生在以下情况:
- 缓存服务器宕机或故障
- 所有缓存数据在同一时间失效
- 缓存数据没有设置过期时间或者过期时间过短
- 大量请求访问热门数据,导致缓存击穿,无法承受巨大的并发请求
避免缓存雪崩,可以采取以下措施:
-
分布式缓存: 将缓存分散到多个节点上,避免所有节点同时失效导致缓存雪崩。比如使用 Redis Cluster、Memcached Cluster 等分布式缓存方案。
-
缓存预热: 在系统启动时,预先将热门数据加载到缓存中,避免缓存全部失效时,大量请求落到数据库上。
-
设置过期时间: 设置合理的缓存过期时间,避免所有缓存同时失效。
-
限流降级: 当请求量超过系统承受范围时,通过限流或者降级等措施,分流请求,保证系统的稳定性和可用性。
-
多级缓存: 使用多级缓存架构,将热门数据放到内存中的缓存,避免频繁读取数据库和外部接口。
redis缓存击穿
缓存击穿是指当某个热点数据的缓存过期或不存在时,大量请求同时访问该数据,导致请求直接落到了后端数据库上,造成数据库压力过大,影响系统性能和可用性。
缓存击穿一般发生在以下情况:
- 热点数据缓存失效:由于缓存的过期时间到达或被手动删除等原因,导致热点数据不再存在于缓存中。
- 并发请求访问热点数据:大量并发请求同时访问热点数据,此时缓存未命中,导致请求直接访问数据库。
为了避免缓存击穿,可以采取以下措施:
-
加锁机制: 当检测到缓存失效时,先进行加锁操作,只允许一个请求去查询数据库,其他请求等待结果。查询完毕后,更新缓存并释放锁,其他请求再从缓存获取数据。
-
设置短暂的空值缓存: 在缓存失效时,先将空值(null)或者占位数据设置到缓存中,并给予较短的过期时间,避免大量请求同时访问数据库。同时,在缓存中设置一个较长的过期时间,以防止后续请求再次击穿。
-
异步更新缓存: 当检测到缓存失效后,异步更新缓存,而不是在请求时即时更新缓存。这样可以避免大量请求同时访问数据库,在缓存更新完成后,后续请求再从缓存获取数据。
-
使用互斥锁或分布式锁: 在多服务器环境下,使用互斥锁或者分布式锁来保证只有一个请求去查询数据库,其他请求等待结果,避免大量请求同时访问数据库。
redis缓存穿透
缓存穿透是指恶意请求或者不存在的数据被请求,导致缓存无法命中,每次请求都直接访问数据库或其他数据源,造成数据库压力过大。
缓存穿透一般发生在以下情况:
- 恶意请求:恶意用户发送的请求,例如使用遍历查询参数、非法字符等,导致缓存无法命中,直接访问数据库。
- 数据不存在:请求查询的数据本身就不存在于数据库或其他数据源中,无论怎样的请求都无法在缓存中命中。
为了避免缓存穿透,可以采取以下措施:
-
布隆过滤器(Bloom Filter): 使用布隆过滤器来预先过滤掉不存在的数据,当请求到来时,首先经过布隆过滤器的检查,如果不在布隆过滤器中,则直接拒绝请求,避免直接访问数据库。
-
空对象缓存: 当查询数据库为空时,在缓存中设置一个空对象(如null),并设置较短的过期时间,避免下次请求再次穿透到数据库。
-
合法性检查: 对于用户发送的请求参数进行合法性检查,避免恶意请求导致缓存穿透,可以使用验证 token、接口鉴权等方式进行有效性验证。
-
限流和监控: 对请求进行限流,避免大量的恶意请求进入系统,同时建立监控系统,及时发现异常请求并进行处理。
空对象缓存解决缓存穿透
public Customer findCustomerById(Integer customerId) {
Customer customer = null;
// 缓存redis的key名称
String key = CACHE_KEY_CUSTOMER + customerId;
// 1.去redis上查询
customer = (Customer) redisTemplate.opsForValue().get(key);
// 2. 如果redis有,直接返回 如果redis没有,在mysql上查询
if (customer == null) {
// 3.对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql(大公司的操作 )
synchronized (CustomerService.class) {
// 3.1 第二次查询redis,加锁后
customer = (Customer) redisTemplate.opsForValue().get(key);
// 4.再去查询我们的mysql
customer = customerMapper.selectByPrimaryKey(customerId);
// 5.mysql有,redis无
if (customer != null) {
// 6.把mysql查询到的数据会写到到redis, 保持双写一致性 7天过期
redisTemplate.opsForValue().set(key, customer, 7L, TimeUnit.DAYS);
}else {
// defaultNull 规定为redis查询为空、MySQL查询也没有,缓存一个defaultNull标识为空,以防缓存穿透
redisTemplate.opsForValue().set(key, "defaultNull", 7L, TimeUnit.DAYS);
}
}
}
return customer;
}
Google布隆过滤器Guava解决缓存穿透
添加pom文件
<!--guava Google 开源的 Guava 中自带的布隆过滤器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
业务类
-
GUavaBloomFilterController
import com.xfcy.service.GuavaBloomFilterService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;
@Api(tags = "gogle工具Guava处理布隆过滤器")
@RestController
@Slf4j
public class GuavaBloomFilterController {@Resource private GuavaBloomFilterService guavaBloomFilterService; @ApiOperation("guava布隆过滤器插入100万样本数据,额外10w(110w)测试是否存在") @RequestMapping(value = "/guavafilter", method = RequestMethod.GET) public void guavaBloomFilter() { guavaBloomFilterService.guavaBloomFilter(); }
}
-
GUavaBloomFilterService
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.util.ArrayList;
@Slf4j
@Service
public class GuavaBloomFilterService {
// 1.定义一个常量
public static final int _1W = 10000;
// 2.定义我们guava布隆过滤器,初始容量
public static final int SIZE = 100 * _1W;
// 3.误判率,它越小误判的个数也越少(思考:是否可以无限小? 没有误判岂不是更好)
public static double fpp = 0.01; // 这个数越小所用的hash函数越多,bitmap占用的位越多 默认的就是0.03,5个hash函数 0.01,7个函数
// 4.创建guava布隆过滤器
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE, fpp);public void guavaBloomFilter() { // 1.先让bloomFilter加入100w白名单数据 for (int i = 0; i < SIZE; i++) { bloomFilter.put(i); } // 2.故意取10w个不在合法范围内的数据,来进行误判率的演示 ArrayList<Integer> list = new ArrayList<>(10 * _1W); // 3.验证 for (int i = SIZE + 1; i < SIZE + (10 * _1W); i++){ if (bloomFilter.mightContain(i)){ log.info("被误判了:{}", i); list.add(i); } } log.info("误判总数量:{}", list.size()); }
}
缓存问题总结
|---------|-------------|-----------------------|
| 缓存问题 | 产生原因 | 解决方案 |
| 缓存更新不一致 | 数据变更、缓存时效性 | 同步更新、失效更新、异步更新、定时更新 |
| 缓存不一致 | 同步更新失败、异步更新 | 增加重试、补偿任务、最终一致 |
| 缓存穿透 | 恶意攻击 | 空对象缓存、bloomFilter 过滤器 |
| 缓存击穿 | 热点key失效 | 互斥更新、随即退避、差异失效时间 |
| 缓存雪崩 | 缓存挂掉 | 快速失败熔断、主从模式、集群模式 |