需求说明
用户可点赞或踩,每赞一次,“赞”数量+1,每踩一次,“踩”数量+1,“点赞”和“点踩” 当天内二选一当天内有效
场景:用户A 点赞 文章a,文章a 点赞量+1 ,同一用户,同一文章 当天再次点击无效,赞与踩二选一,隔天再次点击有效
使用Redis 的 优缺点
优点
1.Redis的数据存放在内存,速度快
2.Redis计数器,完全符合此场景
3.减轻数据库压力
4.速度快提升用户体验
缺点:
1.代码处理逻辑相对复杂
2.需要单独处理持久化数据,包括使用消息队列异步持久化,或者定时任务都是可以的
表设计
文章的赞踩总记录表
CREATE TABLE `article_love_record` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`article_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文章id',
`love_type` tinyint(4) NOT NULL COMMENT '类型:1-赞;2-踩;',
`love_times` int(11) NOT NULL DEFAULT 0 COMMENT '赞总次数',
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '新建时间',
`update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_article_id`(`article_id`) USING BTREE
)ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER
SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '文章的赞踩总记录表' ROW_FORMAT = Dynamic;
文章赞踩历史记录表
CREATE TABLE `article_user_love_history_record` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`article_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文章id',
`user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户id',
`love_type` tinyint(4) NOT NULL COMMENT '类型:1-赞;2-踩;',
`create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '新建时间',
`update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_article_id`(`article_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER
SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '文章赞踩历史记录表' ROW_FORMAT = Dynamic;
表设计说明:
使用一张历史表记录就可以实现所有统计,但添加另一张文章维度总计表的优势是,
如果缓存不存在,查询时需要查询db返回数据在放入缓存,
查询接口如果历史记录表数据庞大再使用count(*) 统计文章点赞踩的数量效率低下耗时长,用户体验差
如果有总计表只需要查询这张表即可
实现方案分析
上报用户喜好行为实现
- 使用redis 中Hash 计数器记录文章点赞数量统计信息,效期一个月
key: 固定前缀+文章id
filed:类型 1 赞 2 踩
value:自增数量
2)使用redis 中Hash 类型记录用户文章赞踩记录(主要用于限制当天是否可再点击)
key: 固定前缀+文章id+yyyyMMdd
filed:用户id
value:用户文章点赞记录信息序列化后对象值
注意:使用文章id维度记录缓存key的原因是相比用户维度,当天发布文章的数量更可控,设置失效时间进行淘汰
使用用户维度记录若突然用户当日猛增,造成key值会因为用户的数量而猛增不可控,另一方面尽量减少redis的key数量
减少空间。
3)添加用户喜好行为信息到消息队列,异步监听排队处理持久化到数据库中
本次功能演示中未涉及消息队列处理持久化过程,请自行处理
主要步骤 1.添加文章喜好记录历史表 2.根据文章id累加文章的喜好总记录表
查询文章喜好行为实现
入参:文章id ,用户id
出参:文章总赞数量,文章总踩数量,当前用户是否可赞,当前用户是否可踩
1.判断当前文章缓存信息是否存在
1.1 不存在
1.1.1根据文章id 查询db文章的赞踩数(此处可以添加锁防止缓存穿透),返回并更新到缓存
1.2 存在
1.2.1根据当前文章id拼接缓存key值,查询当前文章缓存的点赞踩数量值并返回
2.用户是否可以再次点击信息处理
2.1 redis 中查询是否存在,存在则不可再次操作
java代码实现
import com.boot.redis.constant.CacheKey;
import com.boot.redis.dto.UserArticleLoveActionCacheDto;
import com.boot.redis.enums.UserArticleLoveActionEnum;
import com.boot.redis.jredis.RedisCache;
import com.boot.redis.service.UserArticleLoveActionService;
import com.boot.redis.vo.ArticleUserLoveVO;
import com.xiaoleilu.hutool.date.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* 文章点赞、踩行为接口
*/
@Service
public class UserArticleLoveActionServiceImpl implements UserArticleLoveActionService {
private Logger LOGGER = LoggerFactory.getLogger(UserArticleLoveActionServiceImpl.class);
private static final Integer INCREMENT_ONE = 1;
@Autowired
private RedisCache redisCache;
/**
* 上报用户文章喜好行为
*
* @param articleId 文章id
* @param userId 用户id
* @param type 类型 1 点赞 2 点踩
* 步骤
* 1.使用redis 计数器,累计文章总点赞,踩数量
* 2.记录用户的赞踩行为到Redis (用于判断用户是否可再次点击)
* 3.发送消息事件,异步处理 持久化记录到DB
*/
@Override
public void uploadLoveAction(String articleId, String userId, int type) {
//保存文章赞踩行为统计到缓存 (因为要定时过期,所以需要按照单个文章维度存储,方便单个key过期)
this.doCacheLoveActionCount(articleId, String.valueOf(type), INCREMENT_ONE);
//记录当前用户赞踩行为到缓存
this.doCacheUserArticleLoveAction(articleId, userId, type);
LOGGER.info("此处代替发消息到消息队列代码");
LOGGER.info("demo 项目省略 消息处理代码以及消息消费持久化处理代码!");
}
/**
* 获取文章详情赞踩数
*
* @param articleId 文章id
* @param userId 用户id
*/
@Override
public ArticleUserLoveVO getArticleLoveDetail(String articleId, String userId) {
//当前文章赞踩数量获取返回对象
ArticleUserLoveVO result = new ArticleUserLoveVO();
result.setArticleId(articleId);
// 处理文章总赞、踩数量
this.processArticleActionCount(result, articleId);
//处理用户是否可以赞踩
this.processUserCanAction(result, userId, articleId);
return result;
}
/**
* 处理文章赞、踩数量
*/
private void processArticleActionCount(ArticleUserLoveVO result, String articleId) {
//记录文章总点赞踩数量 缓存key
String cacheKey = CacheKey.ARTICLE_LOVE_KEY.concat(articleId);
//类型枚举值
String loveType = String.valueOf(UserArticleLoveActionEnum.LOVE.getValue());
String hateType = String.valueOf(UserArticleLoveActionEnum.HATE.getValue());
int loveNum = 0;
int hateNum = 0;
//缓存是否存在
boolean keyExists = redisCache.exists(cacheKey);
if (keyExists) {
//文章赞数量
Integer loveHashNum = redisCache.getHash(cacheKey, loveType, Integer.class);
if (null != loveHashNum) {
loveNum = loveHashNum;
}
//文章踩数量
Integer hateHashNum = redisCache.getHash(cacheKey, hateType, Integer.class);
if (null != hateHashNum) {
hateNum = hateHashNum;
}
} else {
//不存在则db取
//文章已赞数量
loveNum = this.getArticleLoveNumFromDB(articleId);
//文章已踩数量
hateNum = this.getArticleHateNumFromDB(articleId);
//已赞总数量 重新进入缓存
this.doCacheLoveActionCount(articleId, loveType, loveNum);
//已踩总数量 重新进入缓存
this.doCacheLoveActionCount(articleId, hateType, hateNum);
}
result.setLoveNum(loveNum);
result.setHateNum(hateNum);
}
/**
* demo 项目
* 仅做模拟数据库查询点赞数量
*/
private int getArticleLoveNumFromDB(String articleId) {
return 101;
}
/**
* demo 项目
* 仅做模拟数据库查询点踩数量
*/
private int getArticleHateNumFromDB(String articleId) {
return 99;
}
/**
* 处理用户是否可赞、踩
*/
private void processUserCanAction(ArticleUserLoveVO result, String userId, String articleId) {
//判断用户是否可点击
String userLoveCacheKey = this.getUserArticleLoveActionKey(articleId);
//缓存中查询
UserArticleLoveActionCacheDto loveActionCacheDto =
redisCache.getHash(userLoveCacheKey, userId, UserArticleLoveActionCacheDto.class);
boolean canLoveFlag = true;
boolean canHateFlag = true;
if (null != loveActionCacheDto) {
canLoveFlag = false;
canHateFlag = false;
}
result.setCanLoveFlag(canLoveFlag);
result.setCanHateFlag(canHateFlag);
}
/**
* 记录文章用户赞踩记录行为
* 按照天的维度统计,零点重置
* 缓存 hash类型 key:固定前缀+文章id+yyyyMMdd
* filed:用户id
* value:用户喜好记录对象信息
* 注:按照文章维度做大key hash 存储原因
* 文章每天平台发布的数量是可控的,但是用户访问量是不可控的,用户量某一天暴增的时候缓存key就特别多
* redis 内存也是很宝贵的
*
* @param articleId 文章id
* @param userId 人员id
* @param type 类型
*/
private void doCacheUserArticleLoveAction(String articleId, String userId, int type) {
//用户文章点赞、踩缓存key =文章id+分隔符+yyyyMMdd
String cacheKey = this.getUserArticleLoveActionKey(articleId);
//赞踩中间对象
UserArticleLoveActionCacheDto loveActionCacheDto = new UserArticleLoveActionCacheDto();
loveActionCacheDto.setArticleId(articleId);
loveActionCacheDto.setType(type);
//保存到hash (相同文章下,不同人员进行区分)
redisCache.setHash(cacheKey, userId, loveActionCacheDto);
//过期时间1天(1天后用户可以再次点击)
redisCache.expire(cacheKey, 1, TimeUnit.DAYS);
}
/**
* 获取用户文章点赞、踩缓存key
*/
private String getUserArticleLoveActionKey(String articleId) {
String regDate = DateUtil.format(new Date(), "yyyyMMdd");
String cacheKey = CacheKey.USER_ARTICLE_LOVE_KEY
.concat(articleId)
.concat("_")
.concat(regDate);
return cacheKey;
}
/**
* 保存点赞或踩行为 到缓存
* 并添加失效时间 30 天
*
* @param articleId 文章id
* @param type 类型
* @param increment 增量值
*/
private void doCacheLoveActionCount(String articleId, String type, int increment) {
//记录文章点赞踩数量 缓存key
String cacheKey = CacheKey.ARTICLE_LOVE_KEY.concat(articleId);
// 使用 redis, hash类型 按照类型区分的计数器
redisCache.hashIncrBy(cacheKey, type, increment);
//文章计数缓存保存30 天
redisCache.expire(cacheKey, 30, TimeUnit.DAYS);
}
}
注:redis 的配置实现 可以参考
https://blog.csdn.net/qq_38011415/article/details/82823778
Controller实现
/**
* 用户赞踩行为事件记录controller
*/
@RestController
public class UserArticleLoveActionController {
@Autowired
private UserArticleLoveActionService loveActionService;
/**
* 上报用户文章赞、踩行为
*/
@RequestMapping("/article/upload-love")
public void uploadLoveAction(@RequestBody UserArticleLoveUploadBO articleLoveUploadBO) {
String userId = articleLoveUploadBO.getUserId();
String articleId = articleLoveUploadBO.getArticleId();
Integer type = articleLoveUploadBO.getType();
loveActionService.uploadLoveAction(articleId, userId, type);
}
/**
* 获取文章详情赞踩数
*
*/
@RequestMapping("/article/love-detail")
public ArticleUserLoveVO getArticleLoveDetail(@RequestBody UserArticleLoveUploadBO articleLoveUploadBO) {
String userId = articleLoveUploadBO.getUserId();
String articleId = articleLoveUploadBO.getArticleId();
return loveActionService.getArticleLoveDetail(articleId, userId);
}
}