业务介绍
新闻头条查看小程序、用户以浏览为主,由于用户读取较多,考虑到数据库压力。小程序各分类下的文章前500 篇缓存(根据业务自定义最大值),如果缓存中存在则直接从缓存中取。若不存在则取自DB,并根据是否在自定义最大值区间内判断是否需要更新到缓存
图示功能类似
注:省略Redis整合的代码详情见之前发的Redis相关的文章
文章实体类
/**
* 文章实体类
*/
public class TArticle implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 文章id
*/
private String articleId;
/**
* 文章标题
*/
private String articleTitle;
/**
* 文章作者
*/
private String articleAuthor;
/**
* 文章内容
*/
private String articleContent;
/**
* 排序分值,分值越大 越靠前
*/
private Double sore;
/**
* 状态 0:删除 1:上架中 2:待上架
*/
private Integer status;
/**
* 新建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
//省略get/set方法
}
常量类
/**
* 常量
*/
public class DemoConstant {
//文章缓存Key前缀
public static String ARTICLE_LIST_KEY = "article_list_key_";
}
文章新增
注:文章新增过程保存到db同时,更新到缓存
流程图
文章新增参数BO
/**
* 文章保存 参数 bo
*/
public class TArticleSveBO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 文章标题
*/
private String articleTitle;
/**
* 文章作者
*/
private String articleAuthor;
/**
* 文章内容
*/
private String articleContent;
/**
* 排序分值
* 一般使用程序生成,目前演示就通过参数传了
*/
private Double sore;
/**
* 所属分类 id
* 同一片文章可以归属多个分类
*/
private List<String> categoryIds;
//省略 get/set方法
}
文章新增接口实现
import com.boot.redis.constant.DemoConstant;
import com.boot.redis.jredis.RedisCache;
import com.boot.redis.persistence.bo.TArticleSveBO;
import com.boot.redis.persistence.entity.TArticle;
import com.boot.redis.service.TArticlePutService;
import com.boot.redis.util.DemoDateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.Date;
/**
* 文章新增并且新增缓存
*/
@Service
public class TArticlePutServiceImpl implements TArticlePutService {
private Logger LOGGER = LoggerFactory.getLogger(TArticlePutServiceImpl.class);
@Autowired
private RedisCache redisCache;
/**
* 自定义最大值
*/
private static final Integer MAX_COUNT = 50;
/**
* 保存文章
* 并且保存有序集合到redis
*
* @param sveBO 文章保存参数bo
*/
@Override
public void saveArticle(TArticleSveBO sveBO) {
if (CollectionUtils.isEmpty(sveBO.getCategoryIds())) {
throw new RuntimeException("所属分类不为空");
}
TArticle tArticle = this.buildTArticle(sveBO);
//保存到db
LOGGER.info("此处省略保存到db的操作");
//按照不同的分类 进行保存
//保持同一分类下的缓存文章不超过 最大值
for (String str : sveBO.getCategoryIds()) {
//key=固定key+分类id
String key = DemoConstant.ARTICLE_LIST_KEY.concat(str);
//保存到redis
redisCache.zAddByScore(key, tArticle, tArticle.getSore());
//如果超出文章缓存保存最大值则移除超出最大长度的文章
this.removeCacheOver(key);
}
}
/**
* 如果超出文章缓存保存最大值
* 按照分值移除分值小的
*/
private void removeCacheOver(String key) {
int currentCount = (int) redisCache.zCard(key);
LOGGER.info("当前集合总长度 currentCount={}", currentCount);
if (currentCount > MAX_COUNT) {
LOGGER.info("超出总长度执行删除");
int dValue = currentCount - MAX_COUNT;
LOGGER.info("超出部分移除 0-{}", dValue);
redisCache.zRemRangeByRank(key, 0, dValue);
}
}
/**
* 构建文章实体
*/
private TArticle buildTArticle(TArticleSveBO sveBO) {
//模拟数据
TArticle tArticle = new TArticle();
tArticle.setArticleId(String.valueOf(sveBO.getSore().intValue()));
tArticle.setArticleAuthor(sveBO.getArticleAuthor());
tArticle.setArticleTitle(sveBO.getArticleTitle());
tArticle.setArticleContent(sveBO.getArticleContent());
//这里演示状态就写死了
tArticle.setStatus(1);
//业务中可以自定义排序分值,或者使用自增id也可以
tArticle.setSore(sveBO.getSore());
tArticle.setCreateTime(DemoDateUtil.endOfDay(new Date()));
tArticle.setUpdateTime(DemoDateUtil.endOfDay(new Date()));
return tArticle;
}
}
新增文章测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class ArticlePutTest {
private Logger LOGGER = LoggerFactory.getLogger(ArticlePutTest.class);
@Autowired
private TArticlePutService tArticlePutService;
/**
* 测试文章新增 并保存到Redis
*/
@Test
public void testAddArticle() {
List<String> categoryIds = new ArrayList<>();
categoryIds.add("1001");
categoryIds.add("1002");
categoryIds.add("1003");
for (int i = 0; i < 50; i++) {
TArticleSveBO sveBO = new TArticleSveBO();
sveBO.setArticleAuthor("作者 " + String.valueOf(i));
sveBO.setArticleTitle("标题 " + String.valueOf(i));
sveBO.setArticleContent("文章正文 " + String.valueOf(i));
sveBO.setCategoryIds(categoryIds);
//模拟分值
sveBO.setSore(Double.valueOf(i));
tArticlePutService.saveArticle(sveBO);
}
}
}
Redis图示
文章程序端查询
分页查询参数BO
/**
* 文章分页查询 参数 bo
*/
public class TArticlePageQueryBO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 所属分类id
*/
private String categoryId;
/**
* 当前页
*/
private Integer pageNo;
/**
* 页容量
*/
private Integer pageSize
//省略get/set方法
}
文章查询
流程图
文章查询接口实现
import com.boot.redis.constant.DemoConstant;
import com.boot.redis.jredis.RedisCache;
import com.boot.redis.persistence.bo.TArticlePageQueryBO;
import com.boot.redis.persistence.entity.TArticle;
import com.boot.redis.service.TArticleGetService;
import com.boot.redis.util.DemoDateUtil;
import com.boot.redis.util.PageUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
/**
* 文章获取接口
*/
@Service
public class TArticleGetServiceImpl implements TArticleGetService {
private Logger LOGGER = LoggerFactory.getLogger(TArticleGetServiceImpl.class);
/**
* 自定义缓存中存放最大值
*/
private static final int MAX_COUNT = 50;
/**
* 公平锁
*/
private ReentrantLock articleListLock = new ReentrantLock();
@Autowired
private RedisCache redisCache;
/**
* 分页获取文章信息
* 注:由于小程序 不需要返回分页信息总total,数据是下滑翻页的
*
* @param queryBO 文章查询bo
* @return 文章集合
*/
@Override
public List<TArticle> getArticleList(TArticlePageQueryBO queryBO) {
int currentNo = queryBO.getPageNo();
int pageSize = queryBO.getPageSize();
String categoryId = queryBO.getCategoryId();
//计算开始位置
int start = PageUtil.getStart(currentNo, pageSize);
//计算结束位置
int end = PageUtil.getEnd(start, pageSize);
LOGGER.info("start={},end={}", start, end);
//判断缓存中是否存在
boolean isExistCache = end < MAX_COUNT;
if (isExistCache) {
LOGGER.info("未超过缓存最大值尝试缓存中提取数据");
return this.getArticleFromCache(categoryId, start, end);
}
LOGGER.info("最大值超出缓存值则从缓存中提取数据");
return this.getArticleFromDb(categoryId, currentNo, pageSize);
}
/**
* 缓存中提取
*/
private List<TArticle> getArticleFromCache(String categoryId, int start, int end) {
//key=固定key+分类id
String key = DemoConstant.ARTICLE_LIST_KEY.concat(categoryId);
LOGGER.info("缓存key={}", key);
List<TArticle> articleDOList = redisCache.zRevRange(key, TArticle.class, start, end);
//判断缓存中是否存在
if (!CollectionUtils.isEmpty(articleDOList)) {
LOGGER.info("缓存中存在直接返回缓存中的数据 size={}", articleDOList.size());
return articleDOList;
}
//添加锁允许单线程访问
articleListLock.lock();
try {
//尝试再次从缓存中提取
//防止其它用户访问后已经添加到缓存中
articleDOList = redisCache.zRevRange(key, TArticle.class, start, end);
//判断缓存中是否存在
if (!CollectionUtils.isEmpty(articleDOList)) {
LOGGER.info("再次尝试缓存中获取");
LOGGER.info("缓存中存在直接返回缓存中的数据 size={}",
articleDOList.size());
return articleDOList;
}
LOGGER.info("缓存中不存在查询db");
articleDOList = this.getArticleFromDb(categoryId, start, end);
//保存到缓存
this.doSaveToCache(categoryId, articleDOList);
return articleDOList;
} finally {
articleListLock.unlock();
}
}
/**
* 保存到缓存
*/
private void doSaveToCache(String categoryId, List<TArticle> articleDbList) {
LOGGER.info("执行保存到缓存开始");
if (CollectionUtils.isEmpty(articleDbList)) {
return;
}
//key=固定key+分类id
String key = DemoConstant.ARTICLE_LIST_KEY.concat(categoryId);
for (TArticle tArticle : articleDbList) {
//保存到redis
redisCache.zAddByScore(key, tArticle, tArticle.getSore());
}
LOGGER.info("当前集合总长度 currentCount={}", redisCache.zCard(key));
LOGGER.info("执行保存到缓存结束");
}
/**
* 从 db 中取数据
* 此处做模拟数据
*/
private List<TArticle> getArticleFromDb(String categoryId, int currentNo, int pageSize) {
LOGGER.info("从 db 中取数据 currentNo={},pageSize={}", currentNo, pageSize);
List<TArticle> articleDOList = new ArrayList<>();
//计算页数 按照分数倒叙
int start = MAX_COUNT - (currentNo * pageSize);
int end = MAX_COUNT / currentNo - 1;
System.out.println("模拟数据 start" + start + " end" + end);
//模拟数据
TArticle tArticle;
//倒叙
for (int i = start; i <= end; i++) {
tArticle = new TArticle();
tArticle.setArticleId(String.valueOf(i));
tArticle.setArticleAuthor("作者 " + i);
tArticle.setArticleTitle("标题 " + i);
tArticle.setArticleContent("文章正文 " + i);
tArticle.setStatus(1);
tArticle.setSore(Double.valueOf(i));
tArticle.setCreateTime(DemoDateUtil.endOfDay(new Date()));
tArticle.setUpdateTime(DemoDateUtil.endOfDay(new Date()));
articleDOList.add(tArticle);
}
return articleDOList;
}
}
查询文章测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class ArticleGetTest {
private Logger LOGGER = LoggerFactory.getLogger(ArticleGetTest.class);
@Autowired
private TArticleGetService tArticleGetService;
@Autowired
private RedisCache redisCache;
@Test
public void testGetArticle() {
String categoryId = "1001";
//key=固定key+分类id
String key = DemoConstant.ARTICLE_LIST_KEY.concat(categoryId);
//获取目前集合长度
int currentCount = (int) redisCache.zCard(key);
System.out.println("目前长度 size={}" + currentCount);
TArticlePageQueryBO queryBO = new TArticlePageQueryBO();
queryBO.setCategoryId(categoryId);
queryBO.setPageNo(5);
queryBO.setPageSize(10);
List<TArticle> tArticleList =
tArticleGetService.getArticleList(queryBO);
if (!CollectionUtils.isEmpty(tArticleList)) {
for (TArticle tArticle : tArticleList) {
System.out.println(tArticle);
}
}
}
}
输出结果
注:按照分值从大到小查询,第五页 即下标40-49