智能协同云图库的升级:Redis与Caffeine协同腾讯云图片服务优化及分布式Session维持登录态
图片优化相关技术
图片优化技术概览
在云图库项目上线前,仍有较大的优化空间。此节将分享近10种主流图片优化技术,涵盖:
- 图片查询优化:分布式缓存、本地缓存、多级缓存
- 图片上传优化:压缩、秒传、分片上传、断点续传
- 图片加载优化:懒加载、缩略图、CDN加速、浏览器缓存
- 图片存储优化:降频存储(冷热数据分离)、清理策略
一、图片查询优化
缓存机制
对于频繁访问的数据,若每次都从数据库(硬盘)获取会较慢,可借助性能更优的存储提升系统响应速度,即缓存。合理运用缓存能显著减轻数据库压力、提升系统性能。
那么,哪些数据适合缓存呢?通常是“读多写少”的情况,具体而言:
- 高频访问的数据:像系统首页、热门推荐内容等。
- 计算成本较高的数据:例如复杂查询结果、大量数据的统计结果。
- 允许短时间延迟的数据:比如无需实时更新的排行榜、图片列表等。
在我们的项目中,主页是用户高频访问的内容,获取图片列表的接口也是高频访问的。且即便数据更新存在一定延迟,对用户体验影响不大,因而十分适合缓存。
Redis分布式缓存
分布式缓存是将缓存数据分布存储于多台服务器上,以在高并发场景下提供更高吞吐量与更好容错性。Redis是实现分布式缓存的主流方案,也是后端开发必学技能。其具备以下优势:
- 高性能:基于内存操作,访问速度极快。单节点Redis的读写QPS可达10w次每秒!
- 丰富的数据结构:支持字符串、列表、集合、哈希、位图等,适用于各类数据结构存储。
- 分布式支持:可通过Redis Cluster构建高可用、高性能的分布式缓存,还提供哨兵集群机制提升可用性、分片集群机制提高可扩展性。
缓存设计
需缓存首页的图片列表数据,即对listPictureVOByPage
接口进行缓存。首先依据缓存三要素“key、value、过期时间”设计。
(1) 缓存key设计
由于接口支持传入不同查询条件,对应数据不同,所以需将查询条件作为缓存key的一部分。可将查询条件对象转为JSON字符串,不过该JSON可能较长,可利用哈希算法(如MD5)压缩key。此外,因使用分布式缓存,可能由多个项目和业务共享,故需在key开头拼接前缀隔离。设计出的key如下:
yupicture:listPictureVOByPage:${查询条件key}
(2) 缓存value设计
缓存从数据库查到的Page
分页对象,存储为何种格式?有两种选择:一是为可读性,转为JSON结构字符串;二是为压缩空间,存为二进制等其他结构。但对应的Redis数据结构均为string
。
(3) 缓存过期时间设置
必须设置缓存过期时间!依据实际业务场景、缓存空间大小及数据一致性要求设置,合适即可。此处因查询条件多,且考虑图片持续更新,设为5~60分钟。
操作Redis的方式
Java有众多Redis操作库,如Jedis、Lettuce等。为便于与Spring项目集成,Spring提供了Spring Data Redis
作为操作Redis的高层抽象(默认用Lettuce作底层客户端)。因项目用Spring Boot,推荐用Spring Data Redis
,开发成本低。其使用简便,直接上手项目实战。
(1) 引入Maven依赖,整合Redis
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
下载Redis:
提取码:vmty,一路next即可;
连接Redis:下载Redis客户端后,可直接用IDEA连接Redis,若IDEA社区版或版本较老无Redis选项,可用Redis可视化工具代替操作Redis:
(2) 在application.yml
中添加Redis配置
spring:
# Redis配置
redis:
database: 0 # 指定使用的redis库, redis共有16个库
host: 127.0.0.1
port: 6379
timeout: 5000 # 超时时间, 在超时时间内连接失败, 回报错
(3) 编写JUnit单元测试文件,测试基础操作
@SpringBootTest
public class RedisStringTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void testRedisStringOperations() {
// 获取操作对象
ValueOperations<String, String> valueOps = stringRedisTemplate.opsForValue();
// Key和Value
String key = "testKey";
String value = "testValue";
// 1. 测试新增或更新操作
valueOps.set(key, value);
String storedValue = valueOps.get(key);
assertEquals(value, storedValue, "存储的值与预期不一致");
// 2. 测试修改操作
String updatedValue = "updatedValue";
valueOps.set(key, updatedValue);
storedValue = valueOps.get(key);
assertEquals(updatedValue, storedValue, "更新后的值与预期不一致");
// 3. 测试查询操作
storedValue = valueOps.get(key);
assertNotNull(storedValue, "查询的值为空");
assertEquals(updatedValue, storedValue, "查询的值与预期不一致");
// 4. 测试删除操作
stringRedisTemplate.delete(key);
storedValue = valueOps.get(key);
assertNull(storedValue, "删除后的值不为空");
}
}
运行单元测试方法:
通过JUnit单元测试文件,测试了使用StringRedisTemplate
对Redis的基础增删改查操作。
(4)编写带缓存的分页查询图片列表接口
注入Redis操作对象:通过@Resource
注解,将StringRedisTemplate
对象注入Spring容器,使其在当前类可用。
/**
* 分页获取图片列表(封装类, 这个接口的区别在于带上缓存 listPictureVOByPageWithCache, 但是前端依旧是使用没有缓存的接口 listPictureVOByPage)
*/
@PostMapping("/list/page/vo/cache")
public BaseResponse<Page<PictureVO>> listPictureVOByPageWithCache(@RequestBody PictureQueryRequest pictureQueryRequest,
HttpServletRequest request) {
long current = pictureQueryRequest.getCurrent();
long size = pictureQueryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
// 普通用户默认只能查询审核通过的数据
pictureQueryRequest.setReviewStatus(PictureReviewStatusEnum.PASS.getValue());
// 在查询数据库之前, 先查询缓存, 如果缓存中没有, 再查询数据库
// 构建缓存的key, value, 过期时间
// 将查询请求对象转为JSON格式
String queryCondition = JSONUtil.toJsonStr(pictureQueryRequest);
// 将查询条件JSON转为字节数组, 再将字节数组转为md5, 得到的结果作为缓存的哈希key
String hashKey = DigestUtils.md5DigestAsHex(queryCondition.getBytes());
// 根据方案设计, redisKey = 项目名 + 接口名(去掉WithCache) + 哈希key拼接
String redisKey = String.format("yupicture:listPictureVOByPage:%s", hashKey);
// 操作redis, 从缓存中查询拿到value
ValueOperations<String, String> opsedForValue = stringRedisTemplate.opsForValue();
// 如果有缓存, 直接作为接口响应返回前端
String cachedValue = opsedForValue.get(redisKey);
if(cachedValue != null){
// 如果缓存命中, 将缓存结果反序列化为JSON对象
Page<PictureVO> cachedPage = JSONUtil.toBean(cachedValue, Page.class);
// Page.class Page可以不带泛型, 泛型只是为了方便我们编码时使用的
return ResultUtils.success(cachedPage);
}
// 查询缓存的结果为空, 接下来进行查询数据库操作
Page<Picture> picturePage = pictureService.page(new Page<>(current, size),
pictureService.getQueryWrapper(pictureQueryRequest));
// 对数据库查询结果进行封装
Page<PictureVO> pictureVOPage = pictureService.getPictureVOPage(picturePage, request);
// 将查询数据库得到的结果, 存入redis缓存中, 下次就可以直接通过查询缓存得到结果
// 将数据库封装结果进行JSON序列化, 作为存入缓存的value
String cacheValue = JSONUtil.toJsonStr(pictureVOPage);
// 设置缓存的过期时间, 5~10 minute 过期
int cachedExpireTime = 300 + RandomUtil.randomInt(0, 300);
// 设置缓存过期时间为一个区间, 是为了解决缓存雪崩的问题
// 将key , value, 过期时间, 时间单位(s) 存入缓存
opsedForValue.set(redisKey, cacheValue, cachedExpireTime, TimeUnit.SECONDS);
// 对查询结果进行脱敏后, 统一封装返回
return ResultUtils.success(pictureVOPage);
}
缓存过期时间区间设置目的
在缓存系统中,缓存雪崩是常见问题。缓存雪崩指大量缓存的key在同一时间集中失效,导致接下来所有请求直接打到数据库。由于数据库需处理大量原本由缓存承担的请求,可能因压力过大崩溃。为避免此问题,需避免缓存同一时间集中过期,将缓存过期时间设为时间区间,增加过期时间随机性,使不同缓存过期时间分散,避免大量缓存同时失效。
Caffeine本地缓存
当应用需频繁访问某些数据时,可将数据缓存到应用内存中(如JVM中),下次访问直接从内存读取,无需经网络或其他存储系统。相比分布式缓存,本地缓存速度更快,但无法在多服务器间共享数据、不便扩容。其应用场景一般为:
- 数据访问量有限的小型数据集
- 不需要服务器间共享数据的单机应用
- 高频、低延迟的访问场景(如用户临时会话信息、短期热点数据)
对于Java项目,Caffeine是主流本地缓存技术,性能高、功能丰富。可精确控制缓存数量和大小、支持缓存过期、多种淘汰策略、异步操作、线程安全等。
💡 建议,若仅提升数据访问性能,优先考虑本地缓存而非分布式缓存,因其无需引入额外中间件,成本更低。
缓存设计
本地缓存设计与分布式缓存基本一致,有两点区别:一是本地缓存需自己创建初始化缓存结构(可简单理解为自己new
一个HashMap
);二是因本地缓存本身服务器隔离且占用服务器内存,key
可更精简,无需添加项目前缀。
后端开发
(1)引入Caffeine的Maven依赖
注意:若引入3.x版本Caffeine,Java版本需>=11!若不想升级JDK,可引入2.x版本。
<!-- 本地缓存Caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
(2)构造本地缓存,设置容量和过期时间
private final Cache<String, String> LOCAL_CACHE =
Caffeine.newBuilder()
.initialCapacity(1024) // 初始容量
.maximumSize(10000L) // 最大容量
.expireAfterWrite(5L, TimeUnit.MINUTES) // 缓存5分钟后移除
.build();
(3)参考分布式缓存代码,修改为本地缓存
在查询数据库前先查询本地缓存,若有数据则直接返回:
// 构建缓存key
String queryCondition = JSONUtil.toJsonStr(pictureQueryRequest);
String hashKey = DigestUtils.md5DigestAsHex(queryCondition.getBytes());
String cacheKey = "listPictureVOByPage:" + hashKey;
// 从本地缓存中查询
String cachedValue = LOCAL_CACHE.getIfPresent(cacheKey);
if (cachedValue != null) {
// 如果缓存命中,返回结果
Page<PictureVO> cachedPage = JSONUtil.toBean(cachedValue, Page.class);
return ResultUtils.success(cachedPage);
}
若没有数据则查询数据库,并将结果设置到本地缓存中:
// 查询数据库
Page<Picture> picturePage = pictureService.page(new Page<>(current, size),
pictureService.getQueryWrapper(pictureQueryRequest));
// 获取封装类
Page<PictureVO> pictureVOPage = pictureService.getPictureVOPage(picturePage, request);
// 存入本地缓存
String cacheValue = JSONUtil.toJsonStr(pictureVOPage);
LOCAL_CACHE.put(cacheKey, cacheValue);
性能测试与多级缓存优化
性能测试
通过Swagger测试返回结果是否正常,对比查数据库、查Redis的性能提升。
- 有缓存:最快可达12ms,性能进一步提升约1倍,相比数据库提升数倍;
- 当前环境:数据库和Redis均在本地,访问较快。若用远程数据库或Redis,性能提升更明显。
扩展思考
若想灵活切换本地缓存或分布式缓存,可采用策略模式或模板方法模式(利用变量灵活切换)。
多级缓存
多级缓存结合本地缓存和分布式缓存优点,构建两级缓存系统,兼顾本地缓存高性能、分布式缓存数据一致性和可靠性。
多级缓存工作流程
- 第一级(Caffeine本地缓存):优先从本地缓存读取数据,命中则直接返回。
- 第二级(Redis分布式缓存):本地缓存未命中则查询Redis分布式缓存,命中则返回数据并更新本地缓存。
- 数据库查询:Redis未命中则查询数据库,并将结果写入Redis和本地缓存。
流程图:
多级缓存提升系统容错性,即使Redis故障,本地缓存仍可提供服务,减少对数据库直接依赖。
后端开发
(1) 优先从本地缓存读取数据,命中则返回
// 构建缓存key
String queryCondition = JSONUtil.toJsonStr(pictureQueryRequest);
String hashKey = DigestUtils.md5DigestAsHex(queryCondition.getBytes());
String cacheKey = "yupicture:listPictureVOByPage:" + hashKey;
// 1. 查询本地缓存(Caffeine)
String cachedValue = LOCAL_CACHE.getIfPresent(cacheKey);
if (cachedValue != null) {
Page<PictureVO> cachedPage = JSONUtil.toBean(cachedValue, Page.class);
return ResultUtils.success(cachedPage);
}
(2) 本地缓存未命中则查询Redis,命中则返回并更新本地缓存
// 2. 查询分布式缓存(Redis)
ValueOperations<String, String> valueOps = stringRedisTemplate.opsForValue();
cachedValue = valueOps.get(cacheKey);
if (cachedValue != null) {
// 如果命中Redis,存入本地缓存并返回
LOCAL_CACHE.put(cacheKey, cachedValue);
Page<PictureVO> cachedPage = JSONUtil.toBean(cachedValue, Page.class);
return ResultUtils.success(cachedPage);
}
(3) Redis未命中则查询数据库,并更新缓存
```java
// 3. 查询数据库
Page
pictureService.getQueryWrapper(pictureQueryRequest));
Page<Picture
文章整理自互联网,只做测试使用。发布者:Lomu,转转请注明出处:https://www.it1024doc.com/13109.html