参考牛客网高级项目教程
功能需求
-
1.为增加访问热帖的性能,可以使用缓存技术,
- 将热帖数据进行缓存,只是第一次加载需要访问DB,以后访问直接访问缓存,大大提高读的性能
-
2.有三种缓存策略:
- 本地缓存-数据缓存到本地服务器上
- 分布式缓存-数据缓存到NoSql数据库中
- 多级缓存-本地缓存,分布式缓存均存数据,避免缓存雪崩
一、缓存策略比较
1. 本地缓存
2. 分布式缓存
3. 多级缓存
- 特点:
- 将DB数据分别缓存到一级缓存,二级缓存
- 访问服务器时,先访问一级缓存,如果一级缓存没有,再访问二级缓存,二级没有,最后再访问DB
- 优点:
- 当数据量很大,一级缓存崩溃了,还有二级缓存,防止大量访问DB
- 提高了系统的可用性
二、优化热帖访问性能策略
1. 缓存策略
-
对于主页的热帖列表数据,可以将其缓存到本地缓存中,下次访问时,可以直接访问缓存,提高吞吐量 -
注意 :加入缓存的数据一般是为了提升读的性能,因此,适用于数据变化频率较低的情况
- 因热门帖子排行列表,使用定时器定期监测算分更新,因此,数据变化不频繁,使用与缓存
- 但普通帖子排行列表不适用,数据实时更新,每次更新都要更新缓存,效果不佳,也易出现数据不一致问题
-
本项目中,综合几种缓存工具,选择性能更好的Caffeine,内部算法优化更好
-
Caffeine基于java8的高性能,接近最优的缓存库。
Caffeine提供的内存缓存使用参考Google guava的API。
Caffeine是基于Google guava和 ConcurrentLinkedHashMap的设计经验上改进的成果。
-
虽Spring对缓存工具做了统一管理,但本项目中不使用Spring的管理工具
因为,Spring用一个缓存管理器管理所有缓存,对缓存数量和过期时间统一配置,这样不适用于多个业务的缓存,因为不同业务的需求不同,缓存数量和过期时间设置不同
如果想使用Spring缓存管理工具对多个缓存管理,需要设置多个管理器,配置起来相对麻烦
-
因此,综合考虑,单独使用自定义的Caffeine工具进行管理
2. Caffeine本地缓存工具使用
Caffeine官方手册
Caffeine使用指南
2.1 导入坐标依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.7.0</version>
</dependency>
2.2 配置最大缓存数量和过期时间
# caffeine
caffeine.posts.max-size=15
caffeine.posts.expire-seconds=180
2.3 优化Service层,加载缓存
三个核心加载接口
Cache - 手动加载
LoadingCache -同步加载
- 多个线程访问,需要等待,先加载完缓存后,再访问,本案例使用此种策略
AsyncLoadingCache -异步加载
private LoadingCache<String, List<DiscussPost>> postListCache;
private LoadingCache<Integer, Integer> postRowsCache;
初始化缓存
@PostConstruct
- 无需每次调用时加载初始化缓存,只要在service被调用时初始化一次即可
- 采用自动更新缓存的策略,即,当类被第一次初始化时,就调用缓存加载机制
- 缓存会根据自定义设置,在过期时间后自动清理,并重新加载新的缓存数据
load(@NonNull String key)
- 如果缓存中没有数据,在此方法中,定义从哪里加载数据到缓存中
String[] params = key.split(":")
- 缓存均是采用key、value健值对储存数据,定义key为指定页码和每页数量拼接的字符串
- 这样,可以查询指定页码的一页数据
@PostConstruct
public void init() {
postListCache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.build(new CacheLoader<String, List<DiscussPost>>() {
@Override
public @Nullable List<DiscussPost> load(@NonNull String key) throws Exception {
if (key == null || key.length() == 0) {
throw new IllegalArgumentException("参数错误!");
}
String[] params = key.split(":");
if (params == null || params.length != 2) {
throw new IllegalArgumentException("参数错误!");
}
int offset = Integer.valueOf(params[0]);
int limit = Integer.valueOf(params[1]);
logger.debug("load hot post list from DB.");
return discussPostMapper.getPosts(0, offset, limit, 1);
}
});
postRowsCache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.build(new CacheLoader<Integer, Integer>() {
@Override
public @Nullable Integer load(@NonNull Integer key) throws Exception {
logger.debug("load hot post rows from DB.");
return discussPostMapper.getPostRows(key);
}
});
}
查询缓存数据
get(offset + ":" + limit)
public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit, int orderMode) {
if (userId == 0 && orderMode == 1) {
return postListCache.get(offset + ":" + limit);
}
logger.debug("load post list from DB.");
return discussPostMapper.getPosts(userId, offset, limit, orderMode);
}
public int findDiscussPostRows(int userId) {
if(userId == 0) {
return postRowsCache.get(userId);
}
logger.debug("load post rows from DB.");
return discussPostMapper.getPostRows(userId);
}
2.4 测试缓存工具
@Test
public void testCache() {
System.out.println(postService.findDiscussPosts(0, 0, 10, 1));
System.out.println(postService.findDiscussPosts(0, 0, 10, 1));
System.out.println(postService.findDiscussPosts(0, 0, 10, 1));
System.out.println(postService.findDiscussPosts(0, 0, 10, 0));
System.out.println(postService.findDiscussPosts(0, 0, 10, 0));
System.out.println(postService.findDiscussPosts(0, 0, 10, 0));
}
结果显示:
- 当使用缓存时,只有第一次访问需要加载DB
- 后两次均直接访问缓存,没有加载数据库
三、使用Jmeter工具测试比较性能
1. 安装与设置
2. 添加测试计划
添加模拟测试计划
设置线程
在计划中添加请求
对请求设置
在计划中添加定时器
- 如果没有定时器,会不间断访问服务器,容易堵塞,处理器处理不了
- 因此,设置随机的间隔,模拟自然状态
设置随机间隔
在计划中添加监听器-聚合报告
3. 压力测试结果分析
没有加缓存的测试报告
- 一分钟后使用7毫秒释放线程
- 吞吐量在每秒11个请求左右
加了缓存的测试报告
|