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

相关内容

热门资讯

国民性创新,越来越阳春白雪 问一个问题,最近两年爆火的创新,从生成式AI到人形机器人,到底是离大众越来越近,还是离大众越来越远?...
男子被显示欠银行1000万亿索... 男子被显示欠银行1000万亿索赔200万遭拒,银行仅愿赔偿3万元
研究显示美国散户投资者推动杠杆... 来源:环球市场播报 Direxion公司联合Vanda Research与The Compound ...
高瓴、李录、巴菲特最新持仓披露... 最近,随着美股13F文件的披露,多家私募机构2025Q4最新调仓情况浮出水面。之前的文章,证星研究院...
原创 老... 四十年代的北京珠市口路口,正处于从民国向新中国过渡的时期,它既保留着清末民初形成的鲜明社会分层特征,...
原创 相... 在金融圈,流行一个词叫“估值修复”。意思是股价跌狠了,总会涨回来。 但阿睿发现,自己在相亲市场的估值...
上海楼市重磅新政,非沪籍大松绑... wumiancaijing.com / 最热的泛财经新闻,都在这儿了。 重要提醒!!!为防失联,请“...
春节白酒消费:高端产品热度升高... 来源:新京报 春节是酒水消费传统旺季,马年春节期间,白酒动销稳健复苏。多家机构调研发现,白酒消费呈现...
黄金暴涨的秘密找到了!不是散户... 过去两年,黄金市场最容易被忽视的一条主线,并不是价格本身的起伏,而是一个更为深层的结构性变化——全球...
刘强东投资50亿进军游艇产业,... 极目新闻记者 陈红 刘强东近日创立了独立游艇品牌Sea Expandary,进入游艇产业,计划从研...
韩国驻美大使:密切关注美方新关... 据韩联社报道,韩国驻美国大使康京和2月24日就美国总统特朗普在联邦最高法院裁定“对等关税”违法后宣布...
现货黄金刚刚涨破5200美元关... 25日,现货黄金持续拉涨,盘中再次突破5200美元大关,涨超1.3%。 瑞银(UBS)分析师此前在...
【银行业展望系列】五篇大文章:... 当前银行息差持续承压、规模扩张的增长红利逐渐消退,已经陷入内卷式的同质化竞争。“规模为王”的旧模式将...
原创 一... 美国总统唐纳德·特朗普迅速恢复了之前被最高法院废除的关税政策,这一急转弯让本就面临成本激增压力的美国...
收盘:上证指数、深证成指涨1.... 上证指数(000001)涨0.72%,深证成指(399001)涨1.29%,创业板指(399006)...
蔡宏波、毛健:美国贸易逆差“转... 近年来,美国政府反复将“贸易失衡”描述为事关国家安全的核心问题。从政策实践看,自2018年以来,美国...
五粮液集团入股四川三江汇海融资... 天眼查显示,近日,四川三江汇海融资租赁有限公司发生工商变更,新增四川省宜宾五粮液集团有限公司为股东,...
A股高开高走:周期股延续强势,... A股三大股指2月25日集体高开。早盘震荡走高,午后震荡回落,全天呈现高位震荡走势。 从盘面上看,周期...