假设读者现在已经对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);
}
上述方法是 RedisTemplate 中 delete 方法的源码,它就是使用 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
使用 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
下面代码演示各大社交网站常见功能
点赞
实现思路:
代码实现:
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;}
新建一张表用于存储发生的点赞,关注事件,表实体类如下:
/**
* 会话列表对应实体类
*/@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
实现思路:
/*** 关注* @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类型了,
思路:
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);