一边听着舒缓的轻音乐,一边写博客
最近是真的有点累了呢
原来精力再旺盛的哈士奇
也会有疲乏的时候呢
开篇有益:
为什么点赞信息要放在缓存中?
考虑一下:
你的新发布app,号称两亿人同时在线
中午吃饭时间,大家都休息
点赞排行榜,所有人都想看,高频操作,
同一时间数万查询量,怎么处理?
那么既然要放缓存里,又该如何去放呢?
这可是个技术活噢
大家先来看一下咖啡汪的思维导图,源码链接依旧是附在了思维导图后面
思维导图:(一张图截不下。。。)
源码链接:
https://github.com/HuskyCorps/distributeMiddleware
让我们来看一下程序(为了方便大家查看,本汪在代码上做了详细的注释):
1.我们配置redis模板,
(不启用她的事务支持也不使用管道,至于为什么不启用,大家可以查看本汪在Boolean enablePraise = redisTemplate.opsForValue().setIfAbsent(recordKey,1);
这行代码上的注释)
StringRedisTemplate 是RedisTemplate 的一个子类,这儿不做过多阐述。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* redis自定义注入-redis模板
*
* @author Yuezejian
* @date 2020年 08月25日 20:47:30
*/
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory connectionFactory;
@Bean
public RedisTemplate redisTemplate() {
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
//设置key/value的序列化策略
redisTemplate.setKeySerializer(new StringRedisSerializer());
//也可以使用new JdkSerializationRedisSerializer(),反序列化时注意匹配
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
// class StringRedisTemplate extends RedisTemplate<String, String>
//是子类
@Bean
public StringRedisTemplate stringRedisTemplate() {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(connectionFactory);
//开启事务支持
stringRedisTemplate.setEnableTransactionSupport(true);
return stringRedisTemplate;
}
}
2,Controller中我们写了:
获取文章列表, 获取文章详情及点赞信息, 文章点赞, 取消文章点赞,
获取当前用户点赞过的历史文章-用户详情, 获取文章点赞排行榜 ,这几块儿功能接口
太简单的查询,本汪不做太多描述,点赞的sql也都简单,本汪不细说
本汪先带大家,一起看一下他们的实现思路:
import com.tencent.bigdata.convenience.api.response.BaseResponse;
import com.tencent.bigdata.convenience.api.response.StatusCode;
import com.tencent.bigdata.convenience.model.dto.PraiseDto;
import com.tencent.bigdata.convenience.model.entity.Article;
import com.tencent.bigdata.convenience.model.mapper.ArticleMapper;
import com.tencent.bigdata.convenience.server.service.article.PraiseService;
import com.tencent.bigdata.convenience.server.service.log.LogAopAnnotation;
import com.tencent.bigdata.convenience.server.utils.ValidatorUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 文章点赞Controller
*
* @author Yuezejian
* @date 2020年 09月10日 19:59:46
*/
@RestController
@RequestMapping("praise")
public class PraiseController extends AbstractController{
@Autowired
private PraiseService praiseService;
@Autowired
private ArticleMapper articleMapper;
/**
* 获取文章列表
* @return
*/
@RequestMapping(value = "getArticleList", method = RequestMethod.GET)
public BaseResponse getArticleList() {
BaseResponse response = new BaseResponse(StatusCode.Success);
try {
response.setData(praiseService.getAll());
} catch (Exception e) {
response = new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
/**
* 获取文章详情及点赞信息
* @param articleId
* @param curUserId
* @return
*/
@RequestMapping("getArticleInfo")
public BaseResponse getArticleInfo(@RequestParam Integer articleId,Integer curUserId) {
BaseResponse response = new BaseResponse(StatusCode.Success);
try {
response.setData(praiseService.getArticleInfo(articleId,curUserId));
} catch (Exception e) {
response = new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
/**
* 文章点赞
* @param dto
* @param result
* @return
*/
@RequestMapping(value = "on",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@LogAopAnnotation(value = "文章点赞",operatorTable = "article_praise和article")
public BaseResponse praiseOn(@RequestBody @Validated PraiseDto dto, BindingResult result) {
String resCheck = ValidatorUtil.checkResult(result);
if (StringUtils.isNotBlank(resCheck)) {
return new BaseResponse(StatusCode.InvalidParams.getCode(),resCheck);
}
BaseResponse response = new BaseResponse(StatusCode.Success);
try {
//TODO:查询点赞文章是否失效
Article article = articleMapper.selectByPrimaryKey(dto.getArticleId());
if ( article != null) {
if (praiseService.praiseOn(dto)) {
response.setData("点赞成功");
} else {
response.setData("该文章已经点赞过了");
}
} else {
response.setData("该文章已下架或不存在");
}
} catch (Exception e) {
response = new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
/**
* 取消文章点赞
* @param dto
* @param result
* @return
*/
@RequestMapping(value = "off",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@LogAopAnnotation(value = "取消文章点赞",operatorTable = "article_praise和article")
public BaseResponse praiseOff(@RequestBody @Validated PraiseDto dto, BindingResult result) {
String resCheck = ValidatorUtil.checkResult(result);
if (StringUtils.isNotBlank(resCheck)) {
return new BaseResponse(StatusCode.InvalidParams.getCode(),resCheck);
}
BaseResponse response = new BaseResponse(StatusCode.Success);
try {
//TODO:查询点赞文章是否失效
Article article = articleMapper.selectByPrimaryKey(dto.getArticleId());
if (article != null) {
if (praiseService.praiseOff(dto)) {
response.setData("已成功取消点赞");
} else {
response.setData("您还未点赞过该文章");
}
} else {
response.setData("该文章已下架或不存在");
}
} catch (Exception e) {
response = new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
/**
* 获取当前用户点赞过的历史文章-用户详情
* @param currentUserId
* @return
*/
@RequestMapping(value = "praiseHistory",method = RequestMethod.POST)
public BaseResponse getHistoryPraiseArticleList(@RequestParam Integer currentUserId) {
if (currentUserId == null || currentUserId < 0){
return new BaseResponse(StatusCode.InvalidParams);
}
BaseResponse response = new BaseResponse(StatusCode.Success);
try {
response.setData(praiseService.getPraiseHistoryArticle(currentUserId));
} catch (Exception e) {
response = new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
/**
* 获取文章点赞排行榜
* @return
*/
@RequestMapping(value = "ranks" ,method = RequestMethod.POST)
public BaseResponse getRankingList() {
BaseResponse response = new BaseResponse(StatusCode.Success);
try {
response.setData(praiseService.getPraiseRanks());
} catch (Exception e) {
response = new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
}
service:
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.tencent.bigdata.convenience.model.dto.ArticlePraiseRankDto;
import com.tencent.bigdata.convenience.model.dto.PraiseDto;
import com.tencent.bigdata.convenience.model.entity.Article;
import com.tencent.bigdata.convenience.model.entity.ArticlePraise;
import com.tencent.bigdata.convenience.model.entity.User;
import com.tencent.bigdata.convenience.model.mapper.ArticleMapper;
import com.tencent.bigdata.convenience.model.mapper.ArticlePraiseMapper;
import com.tencent.bigdata.convenience.model.mapper.UserMapper;
import com.tencent.bigdata.convenience.server.enums.Constant;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 文章点赞Service
*
* @author Yuezejian
* @date 2020年 09月10日 21:54:36
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class PraiseService {
@Autowired
private ArticleMapper articleMapper;
@Autowired
private ArticlePraiseMapper praiseMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
/**
* 文章点赞
* @param dto
* @return
* @throws Exception
*/
public Boolean praiseOn(PraiseDto dto) throws Exception{
//将基于redis的缓存判断(redis指令:serNX,成功 返true)
final String recordKey = Constant.RedisArticlePraiseUser + dto.getUserId() + dto.getArticleId();
//TODO:setIfAbsent()可以解决并发安全问题(基于原子性,底层线性调用)
//TODO:注意(当开启事务支持或管道通信时,调用setIfAbsent()会返回null)
//TODO:把recordKey当Key ,1 当value, 往redis里存,如果存入成功,返回true; 如果存入时发现已经存过了,返回false;
Boolean enablePraise = redisTemplate.opsForValue().setIfAbsent(recordKey,1);
if (enablePraise) {
//TODO:缓存被穿透时,需要查数据库的记录
//TODO:因此我们不能认为缓存是绝对可靠的,进入此处的数据,应当查询数据库是否已经有了点赞记录
//TODO:有则不允许点赞,没有才允许点赞
//TODO:将点赞的数据插入DB
ArticlePraise entry = new ArticlePraise(dto.getArticleId(), dto.getUserId(), DateTime.now().toDate());
int res = praiseMapper.insertSelective(entry);
if (res > 0) {
articleMapper.updatePraiseTotal(dto.getArticleId(), 1);
//TODO:缓存点赞的相关信息
this.cachePraiseOn(dto);
//TODO:
}
}
return enablePraise;
}
/**
* 取消文章点赞
* @param dto
* @return
* @throws Exception
*/
public Boolean praiseOff(PraiseDto dto) throws Exception {
final String recordKey = Constant.RedisArticlePraiseUser + dto.getUserId() + dto.getArticleId();
//判断是否有点赞记录,有才能取消
Boolean hasPraise = redisTemplate.hasKey(recordKey);
if (hasPraise) {
//删除DB中的记录
int res = praiseMapper.cancelPraise(dto.getArticleId(), dto.getUserId());
if (res > 0) {
//移除缓存中,用户的点赞记录
redisTemplate.delete(recordKey);
//更新文章的点赞总数
articleMapper.updatePraiseTotal(dto.getArticleId(), -1);
//TODO:缓存取消点赞的相关信息
cachePraiseOff(dto);
}
}
return hasPraise;
}
/**
* 获取文章列表
* @return
* @throws Exception
*/
public List<Article> getAll() {
return articleMapper.selectAll();
}
/**
* 获取文章详情列表
* @param articleId 文章ID
* @param curUserId 当前用户ID
* @return
*/
public Map<String, Object> getArticleInfo(final Integer articleId, final Integer curUserId) {
Map<String, Object> resMap = Maps.newHashMap();
//TODO:获取文章详情信息
resMap.put("article-文章详情",articleMapper.selectByPK(articleId));
//TODO:获取点赞过当前文章的用户列表->获取用户昵称,用","拼接
HashOperations<String,String,Set<Integer>> praiseHash = redisTemplate.opsForHash();
Set<Integer> uIds = praiseHash.get(Constant.RedisArticlePraiseHashKey,articleId.toString());
if (uIds != null && !uIds.isEmpty()) {
//把id,用逗号拼接起来,sql用in(1,3,4)去查
resMap.put("userIds-用户Id列表",uIds);
String ids= Joiner.on(",").join(uIds);
//把查询结果用逗号拼接,返回前端,(李白,安琪拉,孙尚香)
//java8的Stream流,不是什么新东西,jdk1.8是2014年3月18日发版的,六年前的东西了
//不懂的自行查看本汪得相关博客
String names = userMapper.selectNamesById(ids)
.stream().map(User::getName).collect(Collectors.joining(","));
resMap.put("uersNames-用户姓名列表",names);
//TODO:当前用户是否点赞过这篇文章
if (curUserId != null) {
resMap.put("isCurUserPraiseCurArtilce",uIds.contains(curUserId));
}
} else {
resMap.put("userIds-用户Id列表",null);
resMap.put("uersNames-用户姓名列表",null);
resMap.put("isCurUserPraiseCurArtilce",false);
}
return resMap;
}
/**
* 获取文章点赞排行榜
* @return
*/
public Map<String,Object> getPraiseRanks() {
Map<String,Object> resMap = Maps.newHashMap();
//TODO:获取点赞排行榜
List<ArticlePraiseRankDto> rankList = Lists.newLinkedList();
ZSetOperations<String,String> praiseSort = redisTemplate.opsForZSet();
Long total = praiseSort.size(Constant.RedisArticlePraiseSortKey);
//倒序排列,取从(0-total)的数据
Set<String> set = praiseSort.reverseRange(Constant.RedisArticlePraiseSortKey,0L,total);
if (set != null && !set.isEmpty()) {
set.forEach(value -> {
//TODO:拿出他的得分情况
Double score = praiseSort.score(Constant.RedisArticlePraiseSortKey,value);
if (score > 0) {
//TODO: 拆分文章id,文章标题
Integer pos = StringUtils.indexOf(value, Constant.SplitChar);
if (pos > 0) {
String articleId = StringUtils.substring(value, 0, pos);
String articleTitle = StringUtils.substring(value, pos + 1);
rankList.add(new ArticlePraiseRankDto(articleId, articleTitle, score.toString(), score));
}
}
});
}
resMap.put("articlePraiseRank-文章点赞排行榜",rankList);
return resMap;
}
/**
* 获取用户点赞过的历史文章
* @param currentUserId
* @return
*/
public Map<String,Object> getPraiseHistoryArticle(final Integer currentUserId) {
Map<String,Object> resMap = Maps.newHashMap();
//TODO:查询用户详情
resMap.put("用户详情",userMapper.selectByPrimaryKey(currentUserId));
//TODO:用户点赞过的历史文章
List<PraiseDto> userPraiseArtilces = Lists.newLinkedList();
HashOperations<String,String,String> hash = redisTemplate.opsForHash();
//取出"用户点赞标识符",所对应存储的field-value键值对
//本汪提一嘴,别忘了,有点赞的文章,value为【文章标题】;已取消点赞的文章,value是被置空的
//是在【cacheUserPraiseArticle】设置的哦
Map<String,String> map = hash.entries(Constant.RedisArticleUserPraiseKey);
map.entrySet().forEach(entity -> {
String field = entity.getKey();
String value = entity.getValue();
String[] arr = StringUtils.split(field,Constant.SplitChar);
//判断 value是否为空-如果为空,则代表用户已经取消点赞,【cacheUserPraiseArticle】
if (StringUtils.isNotBlank(value)) {
//现在通过遍历+判断,来筛选出当前用户点赞过的文章
if (String.valueOf(currentUserId).equals(arr[0])) {
userPraiseArtilces.add(new PraiseDto(currentUserId,Integer.valueOf(arr[1]),value));
}
}
});
//TODO:
resMap.put("用户点赞过的文章列表",userPraiseArtilces);
return resMap;
}
//缓存点赞相关的信息
//这边都做了什么呢,本汪说下
//HashOperators,实际是Hash<k,Map<K,V>> ——> Hash<String,Map<String,Set<Integer>>>
//设置缓存时,首先通过Constant.RedisArticlePraiseHashKey这个固定的字符串 + 文章id,来对缓存进行功能模块的分组
// 获取时通过praise+文章id,锁定是点赞缓存里,某个固定的文章,查看点赞该文章的用户id组
//而Set<Integer>里,存放着的是所有点赞过该文章的用户的id组【1001,1002,2003】
//说明用户1001等人已经点赞过该文章
/**
* 缓存点赞
* @param dto
* @throws Exception
*/
private void cachePraiseOn(final PraiseDto dto) {
//选择的数据结构为Hash, Key --字符串, 存储redis的标志;field -文章id ; Value - 用户id列表
HashOperations<String,String, Set<Integer>> praiseHash = redisTemplate.opsForHash();
//记录点赞过当前文章的用户id列表
Set<Integer> uIds = praiseHash.get(Constant.RedisArticlePraiseHashKey,dto.getArticleId().toString());
if (uIds == null || uIds.isEmpty()) {
uIds = Sets.newHashSet();
}
uIds.add(dto.getUserId());
praiseHash.put(Constant.RedisArticlePraiseHashKey,dto.getArticleId().toString(),uIds);
//TODO:缓存点赞排行榜
this.cachePraiseRank(dto,uIds.size());
//TODO:缓存用户的点赞轨迹(用户点赞过的历史文章)
this.cacheUserPraiseArticle(dto,true);
}
/**
* 缓存取消点赞时的相关信息
* @param dto
* @throws Exception
*/
private void cachePraiseOff(final PraiseDto dto) {
//选择的数据结构为Hash, Key --字符串, 存储redis的标志;field -文章id ; Value - 用户id列表
HashOperations<String,String, Set<Integer>> praiseHash = redisTemplate.opsForHash();
//查询点赞过当前文章的用户id列表
Set<Integer> uIds = praiseHash.get(Constant.RedisArticlePraiseHashKey,dto.getArticleId().toString());
if (uIds != null && !uIds.isEmpty() && uIds.contains(dto.getUserId())) {
//如果有当前用户的点赞记录,就移除
uIds.remove(dto.getUserId());
//把移除后的数据重新放入Set集合
praiseHash.put(Constant.RedisArticlePraiseHashKey,dto.getArticleId().toString(),uIds);
}
//TODO:缓存点赞排行榜
this.cachePraiseRank(dto,uIds.size());
//TODO:缓存用户的点赞轨迹(用户点赞过的历史文章,用户维护)
this.cacheUserPraiseArticle(dto,false);
}
/**
* 缓存点赞排行榜(SortedSet->ZSet)
* value为 "文章ID" + "_" + "文章标题"
* score为 点赞总数
*/
private void cachePraiseRank(final PraiseDto dto,final Integer total) {
String value =dto.getArticleId()+Constant.SplitChar+dto.getTitle();
ZSetOperations<String,String> praiseSort = redisTemplate.opsForZSet();
//TODO:删除原始数据
praiseSort.remove(Constant.RedisArticlePraiseSortKey,value);
//@Nullable
// Boolean add(K key, V value, double score);
//TODO:存入最新现在排行榜数据
praiseSort.add(Constant.RedisArticlePraiseSortKey,value,total.doubleValue());
}
/**
* 以用户为维度,存储用户点赞过的信息
* key = 存储到redis的标准符
* field = 用户id + "-" 文章id
* value = 文章标题
* @param dto
* @param isOn true:点赞直接存储 false:取消点赞,重置value为null/""
*/
private void cacheUserPraiseArticle(final PraiseDto dto, Boolean isOn) {
final String field = dto.getUserId() + Constant.SplitChar + dto.getArticleId();
HashOperations<String,String,String> hash = redisTemplate.opsForHash();
if (isOn) {
hash.put(Constant.RedisArticleUserPraiseKey,field, dto.getTitle());
} else {
hash.put(Constant.RedisArticleUserPraiseKey,field,"");
}
}
}
运行结果: