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);

相关内容

热门资讯

贷款也“拼团” 银行抢单忙 购物能“拼团”,贷款也能! 近日,一场“拼团融资”的银企对接活动在省工业和信息化厅拉开帷幕。 “贷款...
逛花展、赶市集、嗨直播!202... 5月23日 “2026北京直播电商购物月” 在丰台区丽泽金融商务区·2026北京国际花展 正式拉开帷...
2026中关村毕业季|AI“吃... “上帝会掷骰子吗?” 在联想未来中心的“与智者同场”展区,一位海淀学子对着屏幕问道。 爱因斯坦微微前...
原创 今... 今日为5月23日,国际现货黄金价格在4500美元/盎司整数关口附近徘徊不前,日内最低触及4480美元...
三连亏后变为“无主”状态,农尚... 从吴亮手中接盘农尚环境(300536)不足三年后,林峰如今让出了公司控制权,上市公司进入“无主”状态...
55岁湖南女首富出手!豪掷13... 快科技5月24日消息,与马斯克、库克并肩而坐,刚参加完国宴的湖南女首富周群飞就买了家上市企业。 近日...
外资加仓A股,岂是跟风这么简单... 熬过忙碌的交易日,在周末安静时段,理清接下来布局方向。本篇为大家准备了5条要闻,涵盖市场动态、行业变...
原创 俄... 在全球能源的残酷牌桌上,手里攥着石油,腰杆子才能硬气。长期以来,中东的沙漠、俄罗斯的冰原、美国的页岩...
喜力啤酒有产品将涨价,华润啤酒... 来源:红星新闻 红星资本局5月22日消息,今日,红星资本局从雪花啤酒(厦门)有限公司、华润啤酒方面获...
原创 金... 心理预期调整刻不容缓,五月二十二日,黄金价格或将重现十五年前的历史性低迷。 近期若您密切关注着黄金市...
原创 马... 埃隆·马斯克如果能让SpaceX实现“科幻小说”级别的目标,他可能获得1万亿美元的收入。 埃隆·马斯...
涨涨涨!放开限制、可加杠杆!这... 韩国股市站在风口上! 据最新消息,为吸引更多海外资金进入股市,韩国政府计划放开限制,允许境外投资者直...
下周9家上会丨科创板首单IPO... IPO及再融资上会预告 据交易所官网审核动态信息,下周(5.25-5.29)IPO上会审核6家企业,...
富途、老虎市值蒸发1/4!或被... 来源:金融时报 5月22日,中国证监会宣布依法对Tiger Brokers (NZ) Limited...
马爸爸的好兄弟钱多多搞了杀猪盘... *此图由AI生成 作者| 史大郎&猫哥 来源| 是史大郎&大猫财经Pro 上周四,港股经纬天地大崩盘...
原创 壳... 编辑:XL 国际能源圈最近炸开了锅,壳牌这家百年石油巨头在2026年3月与委内瑞拉政府正式签署多项油...
存储热潮愈演愈烈!奖金拿到手软... 财联社5月24日讯(编辑 卞纯)在席卷全球的存储芯片热潮中,韩国“存储芯片双雄”SK海力士和三星无疑...
揽牌、合作、生态,跨境支付头部... 近日,国内头部跨境支付机构密集落地海外重要布局,一方面,连连数字、PingPong两家公司相继在中东...
原创 帮... 老铁们,周末好!我是帮主郑重。刚扫了一眼下周的财经日历,好家伙,事件一个接一个,堪称“消息面轰炸周”...
海南省住建厅与中国石化海南石油... 5月22日,中国石化海南石油分公司代表、党委书记李新强、总经理蔡文东一行赴海南省住建厅拜访交流。省住...