Redis实现点赞、关注、取top榜单实现
admin
2024-03-25 06:54:12
0

假设读者现在已经对RedisTemplate的基本操作已经很熟悉了,开题之前先介绍一下RedisTemplate 核心方法 execute

RedisTemplate 中,定义了几个 execute() 方法,这些方法是 RedisTemplate 的核心方法。RedisTemplate 中很多其他方法均是通过调用 execute 来执行具体的操作。例如:

/** (non-Javadoc)* @see org.springframework.data.redis.core.RedisOperations#delete(java.util.Collection)*/
@Override
public Long delete(Collection keys) {if (CollectionUtils.isEmpty(keys)) {return 0L;}byte[][] rawKeys = rawKeys(keys);return execute(connection -> connection.del(rawKeys), true);
}

上述方法是 RedisTemplatedelete 方法的源码,它就是使用 execute() 来执行具体的删除操作(即调用 connection.del(rawKeys) 方法)。

方法说明如下表:

方法定义方法说明
T execute(RedisCallback action)在 Redis 连接中执行给定的操作
T execute(RedisCallback action, boolean exposeConnection)在连接中执行给定的操作对象,可以公开也可以不公开。
T execute(RedisCallback action, boolean exposeConnection, boolean pipeline)在可以公开或不公开的连接中执行给定的操作对象。
T execute(RedisScript script, List keys, Object… args)执行给定的 RedisScript
T execute(RedisScript script, RedisSerializer argsSerializer, RedisSerializer resultSerializer, List keys, Object… args)执行给定的 RedisScript,使用提供的 RedisSerializers 序列化脚本参数和结果。
T execute(SessionCallback session)执行 Redis 会话

示例
execute(RedisCallback) 简单用法
使用 RedisTemplate 直接调用 opsFor** 来操作 Redis 数据库,每执行一条命令是要重新拿一个连接,因此很耗资源。如果让一个连接直接执行多条语句的方法就是使用 RedisCallback(它太复杂,不常用),推荐使用 SessionCallback。

本例将演示使用 RedisCallback 向 Redis 写入数据,然后再将写入的数据取出来,输出到控制台。如下:

示例:

execute(RedisCallback) 简单用法
使用RedisTemplate直接调用 opsFor** 来操作Redis数据库,每执行一条命令是要重新拿一个连接,因此很耗资源。如果让一个连接直接执行多条语句的方法就是使用 RedisCallback(它太复杂,不常用),推荐使用 SessionCallback。

本例将演示使用RedisCallback 向 Redis写入数据,然后再将写入的数据取出来,输出到控制台。如下:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class)
@SpringBootTest
public class ExecuteSimple {/** 注入 RedisTemplate */@Autowiredprivate RedisTemplate redisTemplate;@Testpublic void contextLoads() {redisTemplate.execute(new RedisCallback() {@Overridepublic String doInRedis(RedisConnection connection) throws DataAccessException {RedisStringCommands commands = connection.stringCommands();// 写入缓存commands.set("execute_key".getBytes(), "hello world".getBytes());// 从缓存获取值byte[] value = commands.get("execute_key".getBytes());System.out.println(new String(value));return null;}});}}

运行示例,输出结果如下:

hello world

其实,在 RedisTemplate 中,其他很多方法均是通过调用 execute() 方法来实现,只是不同的方法实现不同的回调接口。部分源码如下:

// ...
@Override
public Long increment(K key) {byte[] rawKey = rawKey(key);return execute(connection -> connection.incr(rawKey), true);
}/** (non-Javadoc)* @see org.springframework.data.redis.core.ValueOperations#increment(java.lang.Object, long)*/
@Override
public Long increment(K key, long delta) {byte[] rawKey = rawKey(key);return execute(connection -> connection.incrBy(rawKey, delta), true);
}@Override
public void set(K key, V value, long timeout, TimeUnit unit) {byte[] rawKey = rawKey(key);byte[] rawValue = rawValue(value);execute(new RedisCallback() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {potentiallyUsePsetEx(connection);return null;}public void potentiallyUsePsetEx(RedisConnection connection) {if (!TimeUnit.MILLISECONDS.equals(unit) || !failsafeInvokePsetEx(connection)) {connection.setEx(rawKey, TimeoutUtils.toSeconds(timeout, unit), rawValue);}}private boolean failsafeInvokePsetEx(RedisConnection connection) {boolean failed = false;try {connection.pSetEx(rawKey, timeout, rawValue);} catch (UnsupportedOperationException e) {// in case the connection does not support pSetEx return false to allow fallback to other operation.failed = true;}return !failed;}}, true);
}
// ...
 

execute(SessionCallback) 简单用法

使用 RedisTemplate 直接调用 opsFor** 来操作 Redis 数据库,每执行一条命令是要重新拿一个连接,因此很耗资源。如果让一个连接直接执行多条语句的方法就是使用 SessionCallback,还可以使用 RedisCallback(它太复杂,不常用)。

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class)
@SpringBootTest
public class SessionCallbackSimple {/** 注入 RedisTemplate */@Autowiredprivate RedisTemplate redisTemplate;@Testpublic void contextLoads() {redisTemplate.execute(new SessionCallback() {@Overridepublic String execute(RedisOperations operations) throws DataAccessException {operations.opsForValue().set("valueK1", "value1");System.out.println("valueK1 = " + operations.opsForValue().get("valueK1"));operations.opsForList().leftPushAll("listK1", "one", "two");System.out.println("listK1 = " + operations.opsForList().size("listK1"));return null;}});}}

运行示例,输出如下:

valueK1 = value1
listK1 = 2

关于RedisTemplate的底层核心方法详情可参考RedisTemplate 核心方法 execute

下面代码演示各大社交网站常见功能

点赞

实现思路:

  • 1.获取当前登录的用户
  • 2.使用redis的set类型的数据结构,k-被点赞的实体类型+对应实体id,v-对该实体进行点赞操作的用户的id
  • 3

代码实现:

public void like(int userId,int entityType,int entityId,int entityUserId){redisTemplate.execute(new SessionCallback() {@Overridepublic Object execute(RedisOperations operations) throws DataAccessException {// 构建被点赞的实体对应redis的kString entityLikeKey = "like:entity:entityType:entityId";// 构建被点赞的实体对应的作者再redis中的key,用于统计后期某用户总共收获了多少个赞,社交论坛(例如虎扑)就有这个功能String userLikeKey = "like:user:entityUserId";// 判断集合中是否有userId这个值Boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);// 开启事务operations.multi();if (isMember){// 移除userId这个值operations.opsForSet().remove(entityLikeKey,userId);//减一(方便统计用户后期获得赞的总数)operations.opsForValue().decrement(userLikeKey);}else {operations.opsForSet().add(entityLikeKey,userId);//加一(方便统计用户后期获得赞的总数)operations.opsForValue().increment(userLikeKey);}// 提交事务return operations.exec();}});}

然后在前端局部刷新页面的时候统计一下被点赞的实体对应redis的k中包含的vue的数量就是点赞数,同时判断一下vue列表是否包含当前登录的用户id就能知道当前用户是否已经对某个帖子进行过点赞:

 // 查询某实体点赞的数量public long findEntityLikeCount(int entityType,int entityId){String entityLikeKey = "like:entity:entityType:entityId";return redisTemplate.opsForSet().size(entityLikeKey);}//查询某人对某实体的点赞状态public int findEntityLikeStatus(int userId,int entityType,int entityId){String entityLikeKey = "like:entity:entityType:entityId";return redisTemplate.opsForSet().isMember(entityLikeKey,userId) ? 1 : 0;}

一般情况下,内存中的数据一般在磁盘上也要有一份作为数据持久化存储,网站的运营人员可以依据数据库中的数据来更好的分析网站的用户使用情况,用户爱看哪一类文章等等,如果只是存储在内存中很容易丢失,市面上的做法一般都是数据库与内存中都保持一份,但是如果直接更新内存之后在去操作数据库,在并发量大的时候,对数据库的压力来讲是很大的,大量请求进来造成大量行锁,对于使用者来讲体验肯定是不好的,一般这种情况下可以引入消息队列来进行异步处理数据库,达到更好的响应效果

1.将用户出发的点赞,关注啥的操作封装成一个事件

/***  事件(将用户触发的事件封装成一个对象)*/
public class Event {private String topic;    // 事件的主题private int userId;  // 事件的来源,触发的人private int entityType; // 事件发生在哪种类型上private int entityId;  // 事件发生在的实体的idprivate int entityUserId; //事件发生的实体对应的作者的idprivate Map data = new HashMap<>();public String getTopic() {return topic;}public Event setTopic(String topic) {this.topic = topic;return this;}public int getUserId() {return userId;}public Event setUserId(int userId) {this.userId = userId;return this;}public int getEntityType() {return entityType;}public Event setEntityType(int entityType) {this.entityType = entityType;return this;}public int getEntityId() {return entityId;}public Event setEntityId(int entityId) {this.entityId = entityId;return this;}public int getEntityUserId() {return entityUserId;}public Event setEntityUserId(int entityUserId) {this.entityUserId = entityUserId;return this;}public Map getData() {return data;}public Event setData(String key, Object value) {this.data.put(key, value);return this;}@Overridepublic String toString() {return "Event{" +"topic='" + topic + '\'' +", userId=" + userId +", entityType=" + entityType +", entityId=" + entityId +", entityUserId=" + entityUserId +", data=" + data +'}';}
}

2.获取当前登陆的用户对某个帖子的点赞状态,也就是是否已经点过赞

 //查询某人对某实体的点赞状态public int findEntityLikeStatus(int userId,int entityType,int entityId){String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);return redisTemplate.opsForSet().isMember(entityLikeKey,userId) ? 1 : 0;}
  1. 如果当前登录的用户该对帖子是没有点过赞的,那摩就可以通过消息队列往数据库存一份数据,
    如果是取消点赞,只需要清除redis中的数据即可,数据库的可以不用管,防止频繁点赞取消对数据库和队列造成过大压力,查询的时候只需要去redis中获取就行,如果追求完美,其实也可以删除数据库对应的记录,但是为了防止流量过大,这里应该做限流,比如一秒钟只允许发送多少个请求,redis可以实现这种功能,这里不细说,可以翻阅我的博客查找相关文章

新建一张表用于存储发生的点赞,关注事件,表实体类如下:

/**
*  会话列表对应实体类
*/@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class Message {private int id;private int fromId;   // 此条消息来源于谁 1-来源于系统通知(点赞关注操作这里都放置于系统通知,两个用户评论或者聊天才放置对应的id)private int toId;  // 此条消息发送给谁private String conversationId;  // 两者之间会话的idprivate String content;  // 会话的内容private int status; // 消息的状态,0-未读,1-已读,2-删除private Date createTime;  // 创建的时间}

完整代码如下:可以只看核心部分

  @Controller
public class LikeController implements EmailStatus {@Autowiredprivate LikeService likeService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate EventProducer eventProducer;@Autowiredprivate RedisTemplate redisTemplate;@PostMapping("/like")@ResponseBodypublic String like(@RequestParam("entityType") int entityType,@RequestParam("entityId") int entityId,@RequestParam("entityUserId") int entityUserId,@RequestParam("postId") int postId){// 获取到当前用户User user = hostHolder.getUser();// 点赞likeService.like(user.getId(),entityType,entityId,entityUserId);// 获取点赞的数量long likeCount = likeService.findEntityLikeCount(entityType, entityId);// 获取当前用户点赞的状态int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);// 返回的结果,封装成一个Map集合Map map = new HashMap<>();map.put("likeCount",likeCount);map.put("likeStatus",likeStatus);//触发点赞事件if (likeStatus==1){Event event = new Event().setTopic(TOPIC_LIKE).setEntityId(entityId).setEntityType(entityType).setUserId(hostHolder.getUser().getId()).setEntityUserId(entityUserId).setData("postId",postId);eventProducer.fireEvent(event);}if (entityType == ENTITY_TYPE_POST){// 计算帖子分数String redisKey = RedisKeyUtil.getPostScoreKey();redisTemplate.opsForSet().add(redisKey,postId) ;}return StringUtil.getJsonString(0,null,map);}       

消费者端:

@RabbitListener(bindings = {@QueueBinding(value = @Queue(value = "aa",durable ="true"),  // 声明临时队列exchange = @Exchange(name = "messageExchange",type = "topic"),key = {TOPIC_COMMENT,TOPIC_LIKE,TOPIC_FOLLOW})})public void handleCommentMessage(@Payload String msg) {if (msg == null ) {logger.error("消息的内容为空!");return;}Event event = JSONObject.parseObject(msg, Event.class);if (event == null) {logger.error("消息格式错误!");return;}// 发送站内通知Message message = new Message();message.setFromId(SYSTEM_USER_ID);message.setToId(event.getEntityUserId());message.setConversationId(event.getTopic());message.setCreateTime(new Date());Map content = new HashMap<>();content.put("userId", event.getUserId());content.put("entityType", event.getEntityType());content.put("entityId", event.getEntityId());if(!event.getData().isEmpty()) {for (Map.Entry entry : event.getData().entrySet()) {content.put(entry.getKey(), entry.getValue());}}message.setContent(JSONObject.toJSONString(content));messageService.addMessage(message);}

以上这种方式应对高并发场景是比较好的,当然也有为了简单直接将点赞数存在mysql的,将点赞数当作帖子的一个属性,这种在面对高并发时修改点赞数会触发大量行锁是性能是不太好的,不太推荐

关注

同点赞差不多,这里使用zset

实现思路:

  • 1.获取当前登录的用户
  • 2.使用redis的zset类型的数据结构,k-被关注的用户对应的id,v-对该实体进行点赞操作的用户的id
  • 3
/***   关注* @param userId 执行关注操作的用户的id* @param entityType  被关注的实体类型* @param entityId  被关注的实体的id* @return*/public void follow(int userId,int entityType,int entityId){redisTemplate.execute(new SessionCallback() {@Overridepublic Object execute(RedisOperations operations) throws DataAccessException {// 构造目标keyString followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);// 构造粉丝keyString followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);// 开启事务operations.multi();// 构建当前登录用户的关注列表operations.opsForZSet().add(followeeKey,entityId,System.currentTimeMillis());// 构建被关注用户的粉丝列表operations.opsForZSet().add(followerKey,userId,System.currentTimeMillis());// 提交事务return operations.exec();}});}

完整代码:

 //关注@PostMapping("/follow")@ResponseBodypublic String follow(int entityType,int entityId){// 获取到当前登录的用户User user = hostHolder.getUser();if(user == null){throw new RuntimeException("当前用户未登录");}//   关注followService.follow(user.getId(),entityType,entityId);// 触发关注事件Event event = new Event().setTopic(TOPIC_FOLLOW).setUserId(hostHolder.getUser().getId()).setEntityType(entityType).setEntityId(entityId).setEntityUserId(entityId);eventProducer.fireEvent(event);return StringUtil.getJsonString(0,"已关注");}

消费者端同上一样

取消关注:

 /***  取消关注* @param userId 执行取消关注操作的用户的id* @param entityType 被取消关注的实体类型* @param entityId  被取消关注的实体的id*/public void unfollow(int userId,int entityType,int entityId){redisTemplate.execute(new SessionCallback() {@Overridepublic Object execute(RedisOperations operations) throws DataAccessException {// 构造目标keyString followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);// 构造粉丝keyString followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);// 开启事务operations.multi();operations.opsForZSet().remove(followeeKey,entityId);operations.opsForZSet().remove(followerKey,userId);// 提交事务return operations.exec();}});}

完整代码:

 //取消关注@PostMapping("/unfollow")@ResponseBodypublic String unfollow(int entityType,int entityId){// 获取到当前登录的用户User user = hostHolder.getUser();if(user == null){throw new RuntimeException("当前用户未登录");}//   关注followService.unfollow(user.getId(),entityType,entityId);return StringUtil.getJsonString(0,"已取消关注");}

取top榜单排名前五:

这里大家应该想到使用redis的zset类型了,

思路:

  • 1.构建k-随意取常量字符串,见名知意就行,v-帖子的id,score 这里可以根据点赞量,阅读量啥的某种算法去做运算,也可以直接存点赞量
  • 2.代码实现:
String key = BLOG_LIKED_KEY + "like";// 1.查询top5的点赞帖子或者用户id, zrange key 0 4Set top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);if (top5 == null || top5.isEmpty()) {return Result.ok(Collections.emptyList());}// 2.解析出其中的用户idList ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());String idStr = StrUtil.join(",", ids);// 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)List userDTOS = userService.query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list().stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());// 4.返回return Result.ok(userDTOS);

相关内容

热门资讯

飞天茅台,又涨了100块,陈华... 作者:王一行 不到四个月,飞天茅台又涨价了。 加上3月31日那次涨价,今年飞天茅台的出厂价和零售价累...
银行理财收益缩水,机构集体喊话... 【大河财立方 记者 吴海舒 杨萨】“我自己买股票都没它能亏”,某社交平台上,一位网友晒出了自己购买的...
蒙商银行行长牛冠荣拟任内蒙古自... 蒙商银行行长牛冠荣拟任内蒙古自治区党委管理领导班子企业正职 人民财讯7月25日电,内蒙古自治区党委组...
首发经济破局 激活消费新动能 在昆明顺城购物中心,占地1800平方米的蜜雪冰城旗舰店人气爆棚,门口排满了前来打卡的消费者;蜡笔小新...
原创 谁... 坐在深圳南山的写字楼里往窗外看,无人机送外卖、机器人巡逻、满街的新能源车,很多外地人第一次来都会愣一...
“硬件创新基础设施”嘉立创今日... 7月24日,深圳嘉立创科技集团股份有限公司(以下简称“嘉立创”)正式启动网上网下发行申购,申购简称为...
深化产教融合 推进数智育人 哈... 7月23日,由阿里国际人工智能人才孵化中心(以下简称“阿里国际AITIC”)主办的“智启未来·数智赋...
陈春玉够“稳”,但魔法原子还“... 今年上半年,魔法原子获得了春晚的热度,但是也受到了人事和商业化的质疑。面对外界疑问,陈春玉依靠扎实的...
实物黄金和纸黄金的交易成本如何... 在黄金投资领域,实物黄金和纸黄金是较为常见的两种投资方式,而了解它们的交易成本计算方法对于投资者来说...
这些绩优股发布拟增持计划(附股... 7月以来,上市公司密集发布拟增持计划。与此同时,德明利、广钢气体、柯力传感等多家公司还发布了承诺不减...
特斯拉一周跌没18%,马斯克自... 马斯克这周不好过——特斯拉周五跌超2%,本周累跌近18%,创2022年以来最大单周跌幅;SpaceX...
原创 世... 文|江月白 编辑|江月白 近期中东局势再度掀起波澜,也门胡塞武装突然宣布封锁红海的曼德海峡,这一举...
农业农村部:乡村消费韧性持续凸... 本报记者 刘萌 7月24日,国新办举行新闻发布会介绍2026年上半年农业农村经济运行情况。农业农村部...
一杯鲜啤引爆夏夜狂欢 如东啤酒... 扬子晚报讯(记者 郭小川 通讯员 王军)如火的夏夜,怎能少了一杯清凉爽口的鲜啤?连日来,夜色中的如东...
原创 通... 时间定了,下周油价大涨!2026年汽柴油第10次上涨在即,时间将于7月31日24时准时调价,倒计时仅...
Waymo计划独立进入两地Ro... 7 月 25 日消息,据《金融时报》报道,Alphabet 旗下自动驾驶出租车企业 Waymo 在一...
日均狂赚2.39亿!宁德时代拿... 图片来源:图虫 7月24日晚,宁德时代(300750.SZ)披露2026年半年报,报告期内,公司实现...
长鑫科技下周一上市:合肥产投集... 长鑫科技下周一上市,大股东 合肥产投 都有哪些布局? 长鑫上市,合肥产投能赚多少? 作为长鑫科技发起...
ETF市场周报 | 市场回升趋... 市场回顾: 本周(2026年7月20日-7月24日),A股市场触底反弹,前4日整体走势强劲,周五略有...
美股开盘:指数涨跌不一 ,存储... 7月24日晚间,美股三大指数开盘后涨跌不一。截至发稿,标普500指数涨0.24%,道指涨0.32%,...