做网站要用到的技术,wordpress 取消边栏,手机号网站源码,建网站入门目录 缓存的处理流程缓存穿透解释产生原因解决方案1.针对不存在的数据也进行缓存2.设置合适的缓存过期时间3. 对缓存访问进行限流和降级4. 接口层增加校验5. 布隆过滤器原理优点缺点关于扩容其他使用场景SpringBoot 整合 布隆过滤器 缓存击穿产生原因解决方案1.设置热点数据永不… 目录 缓存的处理流程缓存穿透解释产生原因解决方案1.针对不存在的数据也进行缓存2.设置合适的缓存过期时间3. 对缓存访问进行限流和降级4. 接口层增加校验5. 布隆过滤器原理优点缺点关于扩容其他使用场景SpringBoot 整合 布隆过滤器 缓存击穿产生原因解决方案1.设置热点数据永不过期优点缺点示例 2.使用互斥锁优点缺点示例 3. 使用分布式缓存4. 设置随机过期时间 缓存雪崩产生原因解决方案对于大量的缓存key同时失效对于redis服务宕机 缓存的处理流程
客户端发起请求服务端先尝试从缓存中取数据若取到直接返回结果若取不到则从数据库中取然后将取出的数据更新到缓存并返回结果若数据库也没取到那直接返回空结果。 缓存穿透
解释
Redis缓存穿透指的是针对一个不存在于缓存中的数据进行查询操作由于缓存中不存在该数据所以会直接访问数据库获取数据。这种情况下如果有大量并发查询这个不存在于缓存中的数据就会导致大量的查询请求直接访问数据库增加了数据库的负载并且没有起到缓存的作用。
产生原因
缓存穿透的主要原因是恶意攻击或者查询频率极高的缓存击穿。恶意攻击指的是有人故意请求不存在于缓存中的数据以此来进行攻击。查询频率极高的缓存击穿是指某个热点数据在缓存中过期后大量并发访问该数据导致缓存失效并且直接访问数据库。
解决方案
1.针对不存在的数据也进行缓存
针对不存在的数据也进行缓存即使查询的数据不存在于数据库中也可以将查询的结果缓存起来标记为不存在。这样下次查询同样的数据时就可以直接从缓存中获取结果而不需要再查询数据库。
2.设置合适的缓存过期时间
设置合适的缓存过期时间对于热点数据可以设置较长的缓存过期时间让它在过期之前不会被失效。这样可以减少缓存击穿的风险。
3. 对缓存访问进行限流和降级
对缓存访问进行限流和降级限制并发访问缓存的请求数量避免由于高并发访问导致缓存失效。当缓存失效时可以选择使用备用方案如返回默认值或者降级处理。
4. 接口层增加校验
接口层增加校验如用户鉴权校验查询数据的id做基础校验id0的直接拦截
5. 布隆过滤器
使用布隆过滤器Bloom Filter来过滤掉不存在的数据布隆过滤器是一种高效的数据结构可以用来判断一个元素是否存在于一个集合中如果布隆过滤器判断一个元素不存在则可以避免进行数据库查询操作。它由位数组Bit Array和一系列哈希函数组成。
原理
初始化时位数组的所有位都被设置为0。当要插入一个元素时使用预先设定好的多个独立、均匀分布的哈希函数对元素进行哈希运算每个哈希函数都会计算出一个位数组的索引位置。将通过哈希运算得到的每个索引位置的位设置为1。查询一个元素是否存在时同样用相同的哈希函数对该元素进行运算并检查对应位数组的位置是否都是1。 如果所有位都为1则认为该元素可能存在于集合中如果有任何一个位为0则可以确定该元素肯定不在集合中。 由于哈希碰撞的存在当多位同时为1时可能出现误报False Positive即报告元素可能在集合中但实际上并未被插入过。但布隆过滤器不会出现漏报False Negative即如果布隆过滤器说元素不在集合中则这个结论是绝对正确的。
- 由于哈希碰撞的存在在实际应用中随着更多元素被插入相同哈希值对应的位会被多次置1这就导致原本未出现过的元素经过哈希运算后也可能指向已经置1的位置从而产生误报。不过通过调整位数组大小、哈希函数的数量以及负载因子等参数可以在误报率和存储空间之间取得平衡。 总之布隆过滤器提供了一种空间效率极高但牺牲了精确性的解决方案特别适合用于那些能够容忍一定误报率的大规模数据处理场景。
优点
布隆过滤器的主要优点是占用空间较小查询速度快。由于只需要位数组和哈希函数不需要存储实际的元素因此占用的空间相对较小。同时布隆过滤器查询一个元素的时间复杂度为O(k)与集合的大小无关查询速度非常快。
缺点
由于哈希函数的关系布隆过滤器一旦出现误判元素存在于集合中的情况就无法修正也无法直接删除其中的元素。此外布隆过滤器存在一定的误判率即有一定的概率将不存在的元素误判为存在这取决于位数组的大小和哈希函数的个数。
关于扩容
布隆过滤器步进无法直接删除而且在布隆过滤器设计时其容量是固定的因此不支持直接扩容。传统的布隆过滤器一旦创建它的位数组大小就无法改变这意味着如果需要处理的数据量超过了初始化时预设的容量将导致误报率增加且无法通过简单地增大位数组来解决这个问题在实际应用中为了应对数据增长的需求可以采用以下策略来进行扩容 并行布隆过滤器 可以维护多个独立的布隆过滤器随着数据增长当一个过滤器填满后新加入的数据放入新的布隆过滤器中。查询时需要对所有布隆过滤器进行查询只有当所有的过滤器都表明元素可能不存在时才能确定元素肯定不在集合中。 可扩展布隆过滤器 一些变种如 Scalable Bloom Filter 或 Dynamic Bloom Filter 允许添加额外的空间并重新哈希已有数据到更大的位数组中从而维持较低的误报率。扩容过程通常涉及构造一个新的更大容量的布隆过滤器然后迁移旧数据到新过滤器并从这一刻起在新过滤器中插入新数据。 层次结构布隆过滤器 创建一个多层的布隆过滤器结构新数据首先被插入到最顶层最小容量的过滤器中当某个层级的过滤器接近饱和时再启用下一个容量更大的过滤器。
其他使用场景
数据库索引优化对于大型数据库可以利用布隆过滤器作为辅助索引结构提前过滤掉大部分肯定不在结果集中的查询条件减轻主索引的压力。推荐系统在个性化推荐系统中用于快速排除用户已经浏览过或者不感兴趣的内容。垃圾邮件过滤用于电子邮件系统的垃圾邮件地址库快速判断收到的邮件是否可能来自已知的垃圾邮件发送者。数据分析与挖掘在大规模数据清洗阶段用来剔除重复样本或无效数据。实时监控与报警系统当大量事件流经系统时可以用于快速识别并过滤出已知异常事件降低报警系统误报率。重复数据检测 在爬虫抓取网页或者日志分析中用于URL去重确保不会重复抓取相同的页面或记录。在大数据处理中比如在Hadoop等框架中用来过滤掉重复的数据块或者记录减少计算和存储负担。 网络安全 网络防火墙和入侵检测系统中用于过滤已知恶意IP或攻击特征。社交网络和互联网服务在社交网络中用于快速检测用户上传的内容是否存在违规信息或是检查用户ID、账号是否存在黑名单中。
SpringBoot 整合 布隆过滤器 依赖 dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency!-- 引入Redisson的Spring Boot启动器 --dependencygroupIdorg.redisson/groupIdartifactIdredisson-spring-boot-starter/artifactIdversion3.16.2/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion5.1.47/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency配置 spring:datasource:username: xxpassword: xxxxxxdriver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/smbms?useUnicodetruecharacterEncodingutf-8serverTimezoneCTTcache:type: redisredis:database: 0port: 6379 # Redis服务器连接端口host: localhostpassword: 123456timeout: 5000 # 超时时间
mybatis:mapper-locations: classpath:mapper/*.xmlconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl工具类 package com.kgc.utils;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/*** author: zjl* datetime: 2024/6/7* desc: 复兴Java我辈义不容辞*/
Component
public class BloomFilterUtil {Resourceprivate RedissonClient redissonClient;/*** 创建布隆过滤器** param filterName 过滤器名称* param expectedInsertions 预测插入数量* param falsePositiveRate 误判率*/public T RBloomFilterT create(String filterName, long expectedInsertions, double falsePositiveRate) {RBloomFilterT bloomFilter redissonClient.getBloomFilter(filterName);bloomFilter.tryInit(expectedInsertions, falsePositiveRate);return bloomFilter;}
}业务示例 package com.kgc.service;import com.kgc.mapper.UserMapper;
import com.kgc.pojo.User;
import com.kgc.utils.BloomFilterUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;/*** author: zjl* datetime: 2024/6/7* desc: 复兴Java我辈义不容辞*/
Service
Slf4j
public class UserService {// 预期插入数量static long expectedInsertions 200L;// 误判率static double falseProbability 0.01;// 非法请求所返回的JSONstatic String illegalJson [{\id\:0,\userName\:\null\,\userCode\:null,\userRole\:0}];private RBloomFilterLong bloomFilter null;Resourceprivate BloomFilterUtil bloomFilterUtil;Resourceprivate RedissonClient redissonClient;Resourceprivate UserMapper userMapper;PostConstruct // 项目启动的时候执行该方法也可以理解为在spring容器初始化的时候执行该方法public void init() {// 启动项目时初始化bloomFilterListUser userList userMapper.selectUserList(null,0L);bloomFilter bloomFilterUtil.create(idWhiteList, expectedInsertions, falseProbability);for (User user : userList) {bloomFilter.add(user.getId());}}Cacheable(cacheNames user, key #id, unless #resultnull)public User findById(Long id) {// bloomFilter中不存在该key,为非法访问if (!bloomFilter.contains(id)) {log.info(所要查询的数据既不在缓存中也不在数据库中为非法key);/*** 设置unless #resultnull并在非法访问的时候返回null的目的是不将该次查询返回的null使用* RedissonConfig--RedisCacheManager--RedisCacheConfiguration--entryTtl设置的过期时间存入缓存。** 因为那段时间太长了在那段时间内可能该非法key又添加到bloomFilter比如之前不存在id为1234567的用户* 在那段时间可能刚好id为1234567的用户完成注册使该key成为合法key。** 所以我们需要在缓存中添加一个可容忍的短期过期的null或者是其它自定义的值,使得短时间内直接读取缓存中的该值。** 因为Spring Cache本身无法缓存null因此选择设置为一个其中所有值均为null的JSON*/redissonClient.getBucket(user:: id, new StringCodec()).set(illegalJson, new Random().nextInt(200) 300, TimeUnit.SECONDS);return null;}// 不是非法访问可以访问数据库log.info(数据库中得到数据.......);return userMapper.selectById(id);}// 先执行方法体中的代码成功执行之后删除缓存CacheEvict(cacheNames user, key #id)public boolean delete(Long id) {// 删除数据库中具有的数据,就算此key从此之后不再出现也不能从布隆过滤器删除return userMapper.deleteById(id) 1;}// 如果缓存中先前存在则更新缓存;如果不存在则将方法的返回值存入缓存CachePut(cacheNames user, key #user.id)public User update(User user) {userMapper.updateById(user);// 新生成key的加入布隆过滤器此key从此合法,因为该更新方法并不更新id,所以也不会产生新的合法的keybloomFilter.add(user.getId());return user;}CachePut(cacheNames user, key #user.id)public User insert(User user) {userMapper.insert(user);// 新生成key的加入布隆过滤器此key从此合法bloomFilter.add(user.getId());return user;}
}省略实体类、mapper、测试
缓存击穿
Redis缓存击穿是指在使用Redis作为缓存服务时当一个缓存键失效时大量的请求同时涌入导致数据库负载激增造成性能下降甚至崩溃的情况。
产生原因 热点数据失效当一个热点数据的缓存键失效时大量的请求会同时请求该数据导致数据库负载过高。 频繁的缓存失效如果某个应用频繁地对某个缓存键进行更新或者删除操作那么每次失效后都会触发大量的请求同时请求该数据。 缓存击穿攻击恶意用户故意请求不存在的缓存键造成大量的请求同时涌入。
解决方案
1.设置热点数据永不过期
设置热点数据永不过期对于一些热点数据可以设置其缓存键永不过期转为逻辑过期以避免缓存失效时大量请求涌入。
优点
逻辑过期的优点是可以减少缓存的更新次数避免在没有必要的情况下过多地读取后端数据源并且在数据本身有频繁更新的情况下可以避免缓存数据过时
缺点
逻辑过期的缺点是在某些极端情况下会出现缓存为空的情况如果此时恰巧有大量请求同时访问缓存则可能导致缓存击穿并且无法避免大量的并发请求直接落到后端并且实现起来也是比较复杂和数据无法保证一致性因为可能返回旧数据。
示例 Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}/*** 给热点key缓存预热* param id* param expireSeconds*/
private void saveShop2Redis(Long id, Long expireSeconds) {// 1.查询店铺数据Shop shop getById(id);// 2.封装逻辑过期时间RedisData redisData new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));// 3.写入RedisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY id, JSONUtil.toJsonStr(redisData));
}/*** 缓存击穿逻辑过期功能封装* param id* return*/
public Shop queryWithLogicalExpire(Long id) {String key CACHE_SHOP_KEY id;//1. 从Redis中查询商铺缓存String shopJson stringRedisTemplate.opsForValue().get(key);//2. 判断是否存在if(StrUtil.isBlank(shopJson)){//3. 不存在直接返回这里做的事热点key预热所以已经假定热点key已经在缓存中return null;}//4. 存在需要判断过期时间需要先把json反序列化为对象RedisData redisData JSONUtil.toBean(shopJson, RedisData.class);Shop shop JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime redisData.getExpireTime();//5. 判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {//5.1 未过期直接返回店铺信息return shop;}//5.2 已过期需要缓存重建//6. 缓存重建//6.1 获取互斥锁String lockKey LOCK_SHOP_KEY id;//6.2 判断是否获取锁成功boolean isLock tryLock(lockKey);if(isLock) {// 二次验证是否过期防止多线程下出现缓存重建多次String shopJson2 stringRedisTemplate.opsForValue().get(key);// 这里假定key存在所以不做存在校验// 存在需要判断过期时间需要先把json反序列化为对象RedisData redisData2 JSONUtil.toBean(shopJson2, RedisData.class);Shop shop2 JSONUtil.toBean((JSONObject) redisData2.getData(), Shop.class);LocalDateTime expireTime2 redisData2.getExpireTime();if(expireTime2.isAfter(LocalDateTime.now())) {// 未过期直接返回店铺信息return shop2;}//6.3 成功开启独立线程实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() - {try {// 重建缓存这里设置的值小一点方便观察程序执行效果实际开发应该设为30minthis.saveShop2Redis(id, 20L);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放锁unLock(lockKey);}});}//7. 返回return shop;
}测试
SpringBootTest
class HmDianPingApplicationTests {Resourceprivate ShopServiceImpl shopService;Testvoid testSaveShop() throws InterruptedException {shopService.saveShop2Redis(1L, 10L);}
}2.使用互斥锁
使用互斥锁在缓存失效时可以通过加锁的方式只允许一个请求去更新缓存其他请求等待直到缓存更新完成。
优点
互斥锁的优点是它可以确保只有一个线程在访问缓存内容并且在缓存中没有命中时只会读取一次后端数据库或其他数据源其余线程会等待读取完毕后再次读取缓存这避免了大量的并发请求直接落到后端从而减少了并发压力保证系统的稳定性并且可以保证数据的一致性
缺点
互斥锁的缺点是会增加单个请求的响应时间因为只有一个线程能够读取缓存值其他线程则需要等待这可能会在高并发场景下导致线程池饱和。
示例 /*** 获取锁* param key* return
*/
private boolean tryLock(String key) {// setnx 就是 setIfAbsent 如果存在Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.MINUTES);// 装箱是将值类型装换成引用类型的过程拆箱就是将引用类型转换成值类型的过程// 不要直接返回flag可能为nullreturn BooleanUtil.isTrue(flag);
}/*** 释放锁* param key*/
private void unLock(String key) {stringRedisTemplate.delete(key);
}/*** 互斥锁解决缓存击穿 queryWithMutex()* param id* return*/
public Shop queryWithMutex(Long id) {// 1.从redis查询商铺缓存String key CACHE_SHOP_KEY id;String shopJson stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {return JSONUtil.toBean(shopJson, Shop.class);}// 判断空值if (shopJson ! null) {// 返回一个错误信息return null;}String lockKey lock:shop: id;Shop shop null;try {// 4.实现缓存重建// 4.1获取互斥锁boolean isLock tryLock(lockKey);// 4.2判断是否成功if (!isLock) {// 4.3失败则休眠并重试Thread.sleep(50);// 递归return queryWithMutex(id);}// 4.4成功根据id查询数据库shop getById(id);// 模拟延迟Thread.sleep(200);// 5.不存在返回错误if (shop null) {stringRedisTemplate.opsForValue().set(key,,CACHE_NULL_TTL,TimeUnit.MINUTES);return null;}// 6.存在写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);} catch (InterruptedException ex) {throw new RuntimeException(ex);} finally {// 7.释放锁unLock(lockKey);}// 8.返回return shop;
}合并
/*** 缓存击穿和缓存穿透功能合并封装* param id* return*/
public Shop queryWithMutex(Long id) {String key CACHE_SHOP_KEY id;//1. 从Redis中查询商铺缓存String shopJson stringRedisTemplate.opsForValue().get(key);//2. 判断是否存在if(StrUtil.isNotBlank(shopJson)){//3. 存在直接返回return JSONUtil.toBean(shopJson, Shop.class);}// 这里要先判断命中的是否是null因为是null的话也是被上面逻辑判断为不存在// 这里要做缓存穿透处理所以要对null多做一次判断如果命中的是null则shopJson为if(.equals(shopJson)){return null;}//4. 实现缓存重建//4.1 获取互斥锁String lockKey LOCK_SHOP_KEY id;Shop shop null;try {boolean isLock tryLock(lockKey);//4.2 判断获取是否成功if(!isLock) {//4.3 失败则休眠并重试Thread.sleep(50);// 递归重试return queryWithMutex(id);}//4.4 成功根据id查询数据库shop getById(id);if(shop null) {//5. 不存在将null写入redis以便下次继续查询缓存时如果还是查询空值可以直接返回false信息stringRedisTemplate.opsForValue().set(key, , CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//6. 存在写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {//7. 释放互斥锁unLock(lockKey);}//8. 返回return shop;
}3. 使用分布式缓存
使用分布式缓存将缓存分散到多个Redis实例中以减轻单个Redis实例的负载压力。
4. 设置随机过期时间
设置随机过期时间在设置缓存键的过期时间时可以增加一个随机的过期时间以避免大量的缓存同时失效。
缓存雪崩
Redis缓存雪崩是指在使用Redis作为缓存服务时当缓存的大量键同时失效或者Redis服务发生故障时导致大量请求直接访问数据库造成数据库负载过高甚至导致数据库崩溃的情况。
产生原因 缓存键过期时间一致如果大量的缓存键设置了相同的过期时间同时失效那么所有请求都会直接访问数据库导致数据库压力过大。 Redis服务宕机当Redis服务发生故障无法提供缓存服务时所有的请求都会直接访问数据库造成数据库负载过高。 大规模数据更新在进行大规模数据更新时如果更新操作直接修改数据库而不是通过更新缓存那么所有请求都会直接访问数据库导致数据库负载过高。
解决方案
对于大量的缓存key同时失效
- 给不同的Key的TTL添加随机值比如将缓存失效时间分散开可以在原有的失效时间基础上增加一个随机值 比如1-5分钟随机这样每一个缓存的过期时间的重复率就会降低就很难引发集体失效的事件。
对于redis服务宕机 利用Redis集群提高服务的可用性比如哨兵模式、集群模式给缓存业务添加降级限流策略比如可以在ngxin或spring cloud gateway中处理 降级也可以做为系统的保底策略适用于穿透、击穿、雪崩 给业务添加多级缓存比如使用Guava或Caffeine作为一级缓存redis作为二级缓存等