博客系统点赞功能 使用策略模式及redis缓存和持久化 您所在的位置:网站首页 视频怎么存在数据库 博客系统点赞功能 使用策略模式及redis缓存和持久化

博客系统点赞功能 使用策略模式及redis缓存和持久化

2024-07-10 02:45| 来源: 网络整理| 查看: 265

场景概述

在实际开发中,点赞是高频操作,如果每一次点赞或者获取点赞数都要查询数据库,将会给数据库造成极大的压力,因此尝试用缓存技术来缓存操作。常用的有redis缓存技术。

现在想要做一个博客系统的点赞功能,现在有用户表user,文章表article,帖子表posts,评论表comment,文章、帖子和评论三种类型统称为“作品”,每一种作品类都点赞数字段。以下仅以文章点赞为例,其他作品类型的点赞功能实现大同小异。

创建文章点赞表article_like_record,这是一个关系表,储存点赞方和被点赞方,以此储存点赞关系。就两个字段:userid,targetId。

CREATE TABLE `article_like_record` ( `user_id` int unsigned NOT NULL COMMENT '点赞用户id', `target_id` int unsigned NOT NULL COMMENT '点赞的文章的id', PRIMARY KEY (`user_id`,`target_id`) );

不同类型的作品独立一个点赞表。所以一共有三个点赞表,分别存储文章、帖子和评论的点赞。当然,也可以合在一个表,用一个新字段type区分。我的看法是点赞是高频操作,点赞记录会很多,如果都挤在一个表,那么这个表不好维护。不过我现在还没有接触专业的储存方式和储存技术,成熟的网站想必有方式处理这个问题,而且这只是个小的博客项目,所以其实用一个表也是挺不错的。

此外,本来,从点赞关系表中使用count函数就可以获取某一个作品的总点赞数,但是那样子需要查询整个表,统计记录数,我个人觉得效率太低,所以就给三种类型的作品表都加了一个点赞数字段。当然,这样做无疑会造成一些数据冗余,不过这也是用空间换时间,我觉得是可以接受的。

思路 数据传输

当用户点赞(或取消点赞)时,通过ajax将行为传输到后端Controller接收。 需要传递的数据以json格式传输:

{ userid : "1", targetId : "2", //被点赞的目标作品的id targetType: "article", //目标作品类型 likeState : "1" //点赞状态,1-点赞 0-未点赞/取消赞 }

redis设计

redis采用hash结构储存点赞记录缓存和点赞数统计缓存。

所谓点赞记录缓存即“是否做了点赞这件事”,最终将持久化到数据库的点赞关系表上,用于表示某个用户是否已经点赞了某个作品。这里储存的是一种行为,或者称之为关系。而点赞数量缓存即缓存某一个作品现在有多少点赞数。它缓存的是一个数字,并不能表示哪个用户点赞了哪个表。这储存的是一种数据。

redis的hash可以指定一个Key,因此我们使用likeRecord和likeCount区分上述两种缓存。

redis的fieldname要求也为字符串。所以我们将Key为likeRecord的value的储存格式规定为为形如"targetType::userid::targetId"的字符串,而value则储存1或者0,分别表示点赞或者取消点赞,比如99999::12345::1的value为1,表示用户12345对文章99999做了点赞操作。这样就可以储存“谁对谁做了什么”这一个行为。

而likeCount更加简单,其对应的fieldname设置为"targetType::targetId",即作品类型和作品id,而value设置为作品的当前点赞数即可。  

数据更新

现在,reids的储存结构已经设计好了,那么在点赞和取消点赞的时候要怎么操作呢?

后端获取数据后,根据targetType和likeState执行对应操作,将点赞/取消点赞的行为储存在redis中。同时更新点赞数缓存。  

点赞

首先要根据targetType、userid和targetId拼接fieldname,然后通过jedis的hget方法获取对应的value。

如果value为1,则说明该用户已经点赞。也就是说当前该用户重复点赞了,此时不执行任何操作。如果value不为1,则有两种可能,第一是redis中没有缓存记录;第二是value为0,表示该用户之前取消过点赞,此时又再次点赞。这两种情况下我们都要修改缓存记录,将value修改为1

之后,还要修改点赞数缓存likeCount,可以通过jedis.hincrBy方法使对应的fieldname的value自增1,即jedis.hincrBy("likeCount", likeCountFieldName, 1L);

取消点赞

取消点赞和点赞操作大同小异,注意取消点赞时不能删除缓存记录,而要把对应的value设置为0。原因如下:

前文提到,我们要缓存的是“点赞的行为”,也就是说我们必须将“取消点赞”这一行为记录下来。最终我们的数据要持久化到数据库中,届时如果从reids中获取到取消点赞的缓存记录(即value为0),我们就可以将数据库的点赞记录删去,但是如果我们在取消点赞时直接删除缓存记录,那么在持久化的时候我们就会遗漏这一行为。所以在取消点赞时不能删除缓存记录,而要把对应的value设置为0。

redis持久化

使用ScheduledThreadPoolExecutor进行定时任务,定时持久化数据至数据库中。可以通过TomcatListener在服务器启动时启动定时任务。

在实现了Runnable的子类LikeRunnable中实例化Service对象,调用三种类型的DAO,并获取jedis对象,调用其hgetAll方法,获取所有点赞数据的Map,包括likeRecord和likeCount。之后通过DAO持久化。

持久化的主要问题是对ScheduledThreadPoolExecutor的理解,至于持久化操作时Service和DAO的事情,与普通的持久化操作别无二致。

代码部分 实体类 public class LikeRecord { /** * 数据库主键 */ private Long id; /** * 点赞的用户账号 */ private Long userid; /** * 点赞的目标编号 */ private Long targetId; /** * 目标类型 文章/评论/帖子 */ private int targetTypeInt; /** * 点赞状态 * 1 为 点赞 * 0 为 取消点赞或者未点赞 */ private int likeState; // 类型枚举 private TargetType targetType; // setter和getter省略 }

枚举类 作品枚举 public enum TargetType { /** 文章 */ ARTICLE(1, "article"), /** 帖子 */ POSTS(2, "posts"), /** 评论 */ COMMMENT(3, "comment"), ; private final int CODE; private final String VALUE; TargetType(int code, String value) { CODE = code; VALUE = value; } public int code() { return CODE; } public String val() { return VALUE; } /** * 通过字符串获取数值 * @param value * @return code */ public static int getCode(String value) { for (TargetType p : TargetType.values()) { if (p.val().equals(value)) { return p.code(); } } return -1; } /** * 通过字符串获取枚举 * @param value * @return */ public static TargetType getTargetType(String value) { for (TargetType p : TargetType.values()) { if (p.val().equals(value)) { return p; } } return null; } /** * 通过数字获取枚举 * @param value * @return */ public static TargetType getTargetType(int value) { for (TargetType p : TargetType.values()) { if (p.code() == value) { return p; } } return null; } } 点赞类型枚举 public class LikeEnum { /** redis(key) 点赞记录缓存 */ public static final String KEY_LIKE_RECORD = "likeRecord"; /** redis(key) 点赞数缓存 */ public static final String KEY_LIKE_COUNT = "likeCount"; /** 已点赞 */ public static final String HAVE_LIKED = "1"; /** 未点赞 */ public static final String HAVE_NOT_LIKED = "0"; }

Controller层

Controller的工作是:接收请求参数、判断空参和用户登录状态、调用Service层,以及返回响应结果

@WebServlet("/LikeServlet") public class LikeController extends BaseServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { LikeRecord record = GetParamChoose.getObjByJson(req, LikeRecord.class); //空参检查 if (record == null) { // 如果为空参,则通过自己写的策略模式的方法返回请求的响应结果 ResponseChoose.respNoParameterError(resp, "点赞"); return; } Long userId = ControllerUtil.getUserId(req); if (userId == null) { logger.error("点赞时用户未登录"); ResponseChoose.respUserUnloggedError(resp); return; } record.setUserid(userId); //点赞 LikeService service = ServiceFactory.getLikeService(); ResultType resultType = null; try { resultType = service.likeOrUnlike(record); } catch (Exception e) { e.printStackTrace(); } //自己写的策略模式,返回请求的响应结果 ResponseChoose.respOnlyStateToBrowser(resp, resultType, "点赞操作"); } }

Service层 Service接口 public interface LikeService { /** * 点赞 * @param likeRecord * @return * @throws Exception */ ResultType likeOrUnlike(LikeRecord likeRecord) throws Exception; /** * 点赞关系记录持久化到数据库点赞表中 * @throws Exception */ void persistLikeRecord() throws Exception; /** * 点赞数量统计持久化到数据库作品表中 * @throws Exception */ void persistLikeCount() throws Exception; } 实现类

Service实现类的工作是:判断行为类型(点赞/取消点赞),通过策略模式完成操作;同时也负责持久化的DAO调用

/** * @author 寒洲 * @description 点赞service */ public class LikeServiceImpl implements LikeService { private Logger logger = Logger.getLogger(LikeServiceImpl.class); LikeDao articleLikeDao; LikeDao postsLikeDao; LikeDao commentLikeDao; @Override public ResultType likeOrUnlike(LikeRecord likeRecord) throws Exception { Connection conn = JdbcUtil.getConnection(); //检查 if (likeRecord.getTargetType() == null) { logger.error("点赞类型为null 异常!"); throw new Exception("点赞类型为null"); } //获取属性 Long userid = likeRecord.getUserid(); Long targetId = likeRecord.getTargetId(); int likeState = likeRecord.getLikeState(); TargetType likeType = likeRecord.getTargetType(); if (likeState == 1) { //想要点赞 LikeStategyChoose stategyChoose = new LikeStategyChoose(new LikeStrategyImpl()); stategyChoose.likeOperator(userid, targetId, likeType); } else if (likeState == 0) { //想要取消点赞 LikeStategyChoose stategyChoose = new LikeStategyChoose(new CancelLikeStrategyImpl()); stategyChoose.likeOperator(userid, targetId, likeType); } return ResultType.SUCCESS; } @Override public void persistLikeRecord() throws Exception { logger.info("储存用户点赞关系"); Connection conn = JdbcUtil.getConnection(); Jedis jedis = JedisUtil.getJedis(); Map redisLikeData = jedis.hgetAll(LikeEnum.KEY_LIKE_RECORD); //实例化三个点赞DAO createDaoInstance(); //获取键值 for (Map.Entry vo : redisLikeData.entrySet()) { String likeRecordKey = vo.getKey(); LikeRecord likeRecord = getLikeRecord(likeRecordKey); String value = vo.getValue(); //根据不同的类型使用不同的预设DAO LikeDao dao = getLikeDaoByTargetType(likeRecord.getTargetType()); //检查数据库的点赞状态,true为存在点赞记录 boolean b = dao.countUserLikeRecord(conn, likeRecord); if (LikeEnum.HAVE_LIKED.equals(value)) { //储存点赞记录 if (!b) { //未点赞,添加记录 dao.createLikeRecord(conn, likeRecord); logger.trace("添加点赞记录"); } //else 已点赞,不操作 } else if (LikeEnum.HAVE_NOT_LIKED.equals(value)) { //删除点赞记录 if (b) { //数据库存在用户点赞记录,删除该记录,取消点赞 dao.deleteLikeRecord(conn, likeRecord); logger.trace("删除点赞记录"); } } } //在缓存数据都成功添加到数据库后再删除数据,防止回滚丢失数据 for (String key : redisLikeData.keySet()) { //根据key移除 jedis.hdel(LikeEnum.KEY_LIKE_RECORD, key); } } /** * 实例化三个点赞DAO */ private void createDaoInstance() { articleLikeDao = DaoFactory.getLikeDao(TargetType.ARTICLE); postsLikeDao = DaoFactory.getLikeDao(TargetType.POSTS); commentLikeDao = DaoFactory.getLikeDao(TargetType.COMMMENT); } /** * 根据不同的类型使用不同的DAO * @param type * @return */ private LikeDao getLikeDaoByTargetType(TargetType type) { LikeDao dao; //判断请求的类型 switch (type) { case ARTICLE: dao = articleLikeDao; break; case POSTS: dao = postsLikeDao; break; default: dao = commentLikeDao; } return dao; } @Override public void persistLikeCount() throws Exception { Connection conn = JdbcUtil.getConnection(); Jedis jedis = JedisUtil.getJedis(); // 获取所有缓存的点赞键值对(包含了目标对象的类型和id以及缓存的点赞数) Map redisLikeData = jedis.hgetAll(LikeEnum.KEY_LIKE_COUNT); //预设两个DAO,理论上每次都会用到两个DAO WritingDao aDao = DaoFactory.getArticleDao(); WritingDao pDao = DaoFactory.getPostsDao(); //获取键值 for (Map.Entry vo : redisLikeData.entrySet()) { String likeRecordKey = vo.getKey(); String[] splitKey = likeRecordKey.split("::"); // 点赞的目标id Long id = Long.valueOf(splitKey[1]); // 缓存的点赞数 int count = Integer.parseInt(vo.getValue()); //判断点赞类型 if (String.valueOf(TargetType.ARTICLE.code()).equals(splitKey[0])) { // 点赞了文章 // 获取文章当前的点赞数 int likeCount = aDao.getLikeCount(conn, id); // 获取最终点赞数 int result = count + likeCount; // 更新点赞数 aDao.updateLikeCount(conn, id, result); } else if (String.valueOf(TargetType.POSTS.code()).equals(splitKey[0])) { // 点赞了问贴 // 获取问贴当前的点赞数 int likeCount = pDao.getLikeCount(conn, id); // 获取最终点赞数 int result = count + likeCount; // 更新点赞数 pDao.updateLikeCount(conn, id, result); } } for (String key : redisLikeData.keySet()) { //储存数据成功后移出redis jedis.hdel(LikeEnum.KEY_LIKE_COUNT, key); } jedis.close(); } /** * 将redis的数据封装到实例中 * @param keys "targetType::userid::targetId" * @return */ private LikeRecord getLikeRecord(String keys) { //切割获取数据 String[] splitKey = keys.split("::"); LikeRecord record = new LikeRecord(); record.setTargetType(Integer.parseInt(splitKey[0])); record.setUserid(Long.valueOf(splitKey[1])); record.setTargetId(Long.valueOf(splitKey[2])); return record; } }

策略模式

策略模式方便以后的扩展

Choose选择类

就是Context类,我改了个名字

/** * @author 寒洲 * @description 点赞策略选择 */ public class LikeStategyChoose { private LikeStrategy likeStrategy; public LikeStategyChoose(LikeStrategy likeStrategy){ this.likeStrategy = likeStrategy; } /** * 点赞相关操作 * @param userid 点赞的用户 * @param targetId 被点赞的目标 * @param likeType 被点赞的目标类型 文章/帖子/评论 */ public void likeOperator(Long userid, Long targetId, TargetType likeType) { likeStrategy.likeOperate(userid, targetId, likeType); } } 策略抽象类

除了指定子类的抽象方法likeOperate,此处还提供了两个工具方法,方便子类操作。

public abstract class LikeStrategy { protected Logger logger = Logger.getLogger(LikeStrategy.class); /** * 点赞操作 * @param userid * @param targetId * @param likeType */ public abstract void likeOperate(Long userid, Long targetId, TargetType likeType); /** * 获取redis缓存的点赞关系的域名 * @param userid * @param targetId * @param targetType * @return 形如"targetType::userid::targetId" */ protected String getLikeFieldName(Long userid, Long targetId, int targetType) { String likeKey = targetType + "::" + userid + "::" + targetId; return likeKey; } /** * 获取redis缓存的点赞数量的域名 * @param targetId * @param targetType * @return 形如"targetType::targetId" */ protected String getLikeFieldName(Long targetId, int targetType) { String likeKey = targetType + "::" + targetId; return likeKey; } }

执行点赞类 /** * @author 寒洲 * @description 点赞策略 */ public class LikeStrategyImpl extends LikeStrategy { /** * 点赞的redis value */ private static final String LIKE_STATE = "1"; @Override public void likeOperate(Long userid, Long targetId, TargetType targetType) { /* 以"targetType::userid::targetId"为redis的field,点赞状态为值 点赞状态分为 1-已点赞 0-未点赞,可能未来会有踩,设为-1 */ logger.trace("userid=" + userid + ", targetId=" + targetId + ", likeState=" + LIKE_STATE + ", targetType=" + targetType); //获取存入redis的域名fieldname //点赞关系的域名 String likeRecordFieldName = getLikeFieldName(userid, targetId, targetType.code()); //用于点赞数量统计的域名 String likeCountFieldName = getLikeFieldName(targetId, targetType.code()); Jedis jedis = JedisUtil.getJedis(); // 获取用户点赞的数据,以userid和targetId为field,表为id String recordState = jedis.hget(LikeEnum.KEY_LIKE_RECORD, likeRecordFieldName); 、 //缓存点赞关系 if (LikeEnum.HAVE_LIKED.equals(recordState)) { // 已缓存点赞 // 不做任何操作,未来可能有更新的操作 } else { //未点赞或者无记录,修改记录。 //之后在缓存数据持久化到数据库时会检查是否已点赞过 logger.trace("未点赞或者无记录,修改缓存记录,暂不检查数据库"); jedis.hset(LikeEnum.KEY_LIKE_RECORD, likeRecordFieldName, LIKE_STATE); /* 更新缓存的点赞数量,点赞数+1 如果没有记录,会添加记录,并执行hincrby操作 */ jedis.hincrBy(LikeEnum.KEY_LIKE_COUNT, likeCountFieldName, 1L); } jedis.close(); } } 取消点赞类 /** * @author 寒洲 * @description 取消点赞策略 */ public class CancelLikeStrategyImpl extends LikeStrategy { /** * 取消点赞的redis value */ private static final String UNLIKE_STATE = "0"; @Override public void likeOperate(Long userid, Long targetId, TargetType targetType) { //点赞关系的域名 String likeRecordFieldKey = getLikeFieldName(userid, targetId, targetType.code()); //用于点赞数量统计的域名 String likeCountFieldKey = getLikeFieldName(targetId, targetType.code()); Jedis jedis = JedisUtil.getJedis(); // 获取用户点赞的数据,以userid和targetId为key,表为id String likeRecordState = jedis.hget(LikeEnum.KEY_LIKE_RECORD, likeRecordFieldKey); if (LikeEnum.HAVE_LIKED.equals(likeRecordState)) { //已点赞,取消点赞 logger.info("已点赞,取消点赞"); //将value设为0,这样子就记录了取消点赞的状态,可以持久化到数据库 jedis.hset(LikeEnum.KEY_LIKE_RECORD, likeRecordFieldKey, UNLIKE_STATE); /* 更新缓存的点赞数量,点赞数+1 如果没有记录,会添加记录,并执行hincrby操作 */ jedis.hincrBy(LikeEnum.KEY_LIKE_COUNT, likeCountFieldKey, -1L); } else { //TODO 未点赞或者无记录,无操作 } jedis.close(); } } 定时任务实现持久化

定时任务我同样采用了策略模式,此处只提供主要的代码,免得太乱了

定时任务类 public class LikePersistencebyMinutes { /** 单元时间单位 */ private static final TimeUnit TIME_UNIT = TimeUnit.MINUTES; /** 首次执行的延时时间 */ private static final long INITIAL_DELAY = 5; /** 定时执行的延迟时间 */ private static final long PERIOD = 5; /** * 定时任务 */ private static ScheduledThreadPoolExecutor scheduled; /** 启动定时任务 */ public static void runScheduled() { //创建线程池 scheduled = new ScheduledThreadPoolExecutor( 8, new NamedThreadFactory("点赞数据持久化")); // 第二个参数为首次执行的延时时间,第三个参数为定时执行的延迟时间 scheduled.scheduleWithFixedDelay(new LikeRunnable(), INITIAL_DELAY, PERIOD, TIME_UNIT); } /** * 关闭定时任务 * @throws Exception */ public static void shutDownScheduled() throws Exception { if (scheduled != null) { scheduled.shutdown(); } else { throw new Exception("scheduled对象未创建!"); } } } Runcable子类 public class LikeRunnable implements Runnable{ @Override public void run() { logger.trace("[" + Thread.currentThread().getName() + "]线程运行(run),redis持久化!"); LikeService service = ServiceFactory.getLikeService(); try { // 了解 消息队列 // 点赞是持久化待优化:获取记录时统计点赞数,并将关系储存在数据库,之后根据统计数更新字段 service.persistLikeCount(); service.persistLikeRecord(); } catch (Exception e) { logger.error("[" + Thread.currentThread().getName() + "]线程 redis持久化异常!"); e.printStackTrace(); } } }

参考 菜鸟教程-策略模式CSDN-设计模式-策略模式通用点赞设计与实现有关点赞缓存:点赞模块的设计及优化CSDN上关于点赞数据库表设计的讨论 最后

这是我第一次实际使用redis和jedis,也是第一次设计点赞功能,如果有不足之处请不吝赐教,我一点会虚心接受的!希望这篇文章对你有所帮助,有疑问请在评论区指出。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有