redis: zset(不使用zlist,主要是考虑查询效率的原因)
有序集合对象的编码可以是ziplist或者skiplist, 这里只介绍skiplist(详情参见《Redis设计与实现》P305 8.6) skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表
typedef struct zset{ //跳跃表(按score从小到大保存了所有集合的元素,每个跳跃表节点保存了一个集合的元素,跳表节点score属性保存元素分值,可进行范围操作:ZRANK,ZRANGE) zskiplist *zsl; //字典(为有序集合创建了一个从成员到分值的映射:键保存元素的成员,而值保存了元素的分值,O(1)时间复杂度可以查找给定成员分值ZSCORE) dict *dict; }zset;事务就是一种将多个命令一次性打包,按顺序执行的机制,并且在事务执行期间,服务器不会中断事务去执行其他客户端的命令请求,必须一次性全部执行完(原子性)。(详细参见《Redis设计与实现》P1279 第19章) 通常过程如下:
MULTI //事务开始的标志符号 ....... //一系列对redis数据库操作的命令 EXEC //这个命令会将上面的所有操作命令一次性提交给服务器执行也就是三个阶段: 1)事务开始 2)命令入队 3)事务执行
以MULTI为标志符号,将客户端从非事务状态切换到事务状态,通过在客户端状态的flags属性中打开REDIS_MULTI标识完成。
命令入队的判断流程如下图:
每个redis客户端都有自己的事务状态,事务状态保存在客户端状态的mastate属性中。 事务状态包含一个事务队列以及一个已入队命令的计数器,事务队列的保存方式是FIFO,所以在执行时可以按顺序执行。
以EXEC为标志,收到这个命令后,服务器端会遍历这个客户端的事务队列,执行事务队列中的所有命令,最后把执行结果全部返回给客户端。
Redis事务中的命令还有WATCH(监视)等,不再详细介绍。
使用jedis包来操作redis数据库。所有的命令封装在JedisAdapter.Java文件中。 基础封装如下:
对redis操作进行了封装以后,下面就可以开始向上封装为service层了,这一层中主要负责提供一个通用的关注服务,那么他所做的功能就大体可以分为: 1、关注某个对象(人or问题)follow 2、取消关注某个对象(人or问题)unfollow 3、获取某个对象的粉丝 getFollowers 4、获取粉丝数目 getFollowersCount 5、获取某个人关注对象的数目 getFolloeeCount 6、判断是否是粉丝 isFollower 在redis中所有的数据都是以{key:value}的形式存在,所以针对不同的业务,在写具体操作之前我们需要有一个key的生成器。 RedisKeyUtil.java
//粉丝 private static String BIZ_FOLLOWER = "FOLLOWER"; //关注的事物 private static String BIZ_FOLLOWEE = "FOLLOWEE"; //每个实体所有粉丝的key public static String getFollowerKey(int entityType, int entityId) { return BIZ_FOLLOWER + SPLIT + String.valueOf(entityType) + SPLIT + String.valueOf(entityId); } //某一个用户关注某一类事件(人or问题)的key public static String getFolloweeKey(int userId, int entityType) { return BIZ_FOLLOWEE + SPLIT + String.valueOf(userId) + SPLIT + String.valueOf(entityType); }有了key的生成器,我们的数据库就可以设计为按时间排序(score值为时间,键为对应的key,值为: 1、如果是follower,值为粉丝的id 2、如果为followee,值为被关注的事务(人或问题)的id 注意一个关注行为,对于不同的对象来说将同时产生follower和followee。
Eg. A关注问题1,对于问题1来说,A是问题1的follower;对于A来说,问题1是A的followee。 因此,follower的键值对创建,永远伴随着followee键值对的创建。取消也是一样的效果。是不可分开的2个操作,这也就是为什么要引入具有原子性的Redis事务。
基于以上分析,可以写出关注和不关注的代码如下:
原理和follow一样,不再重复。
这2个函数主要是为了方便在页面上显示出来粉丝列表和某用户的关注列表,这里可以利用zrang和zrevrange函数取出排好序的想要的条数,和mysql中的limit相似。 以getFollowers为例,返回的值虽然实际上是保存的被关注的人的userId,或被关注的问题的questionId, 但是在redis中是以string类型存储的,因此需要一个辅助函数,转化为interger类型方便后续操作和mysql数据类型的对接。
//把string装换为int的辅助函数 private List<Integer> getIdsFromSet(Set<String> idset){ List<Integer> ids = new ArrayList<>(); for(String str : idset){ ids.add(Integer.parseInt(str));//将字符串参数作为有符号的十进制整数进行解析 } return ids; }getFollowers getFollowees函数如下:
public List<Integer> getFollowers(int entityType,int entityId, int count){ String followerKey = RedisKeyUtil.getFollowerKey(entityType,entityId); return getIdsFromSet(jedisAdapter.zrevrange(followerKey,0,count)); } public List<Integer> getFollowers(int entityType,int entityId, int offset, int count){ String followerKey = RedisKeyUtil.getFollowerKey(entityType,entityId); return getIdsFromSet(jedisAdapter.zrevrange(followerKey,offset,count+offset)); } public List<Integer> getFollowees(int userId,int entityType, int count){ String followeeKey = RedisKeyUtil.getFolloweeKey(userId,entityType); return getIdsFromSet(jedisAdapter.zrevrange(followeeKey,0,count)); } public List<Integer> getFollowees(int userId,int entityType, int offset, int count){ String followeeKey = RedisKeyUtil.getFolloweeKey(userId,entityType); return getIdsFromSet(jedisAdapter.zrevrange(followeeKey,offset,count+offset)); }到这里,service基本就完成了
controller是最上层和web端直接的接口,可以指定访问的方法,URL等。 这里根据前端需要的信息,我们大概要3件事: 1、js触发的“关注”行为 2、js触发的“取消关注”行为 3、用户粉丝详情页和用户关注的人详情页 3、首页关注人数的显示 4、问题详情页的关注列表显示 其中1、2又分为关注对象为人和关注对象为问题2种情况,这里可以在前端显示的只有关注问题这种情况,但不阻碍我们写关注对象为人的后端代码。
粉丝详情页
@RequestMapping(path={"/user/{uid}/followers"},method = RequestMethod.GET) public String followers(Model model, @PathVariable("uid") int userId){ List<Integer> followerIds = followService.getFollowers(EntityType.ENTITY_USER,userId,0,10); if(hostHolder.getUser()!=null){ model.addAttribute("followers",getUsersInfo(hostHolder.getUser().getId(),followerIds)); }else{ model.addAttribute("followers", getUsersInfo(0, followerIds)); } model.addAttribute("followerCount",followService.getFollowerCount(EntityType.ENTITY_USER,userId)); model.addAttribute("curUser",userService.selectById(userId)); return "followers"; } 123456789101112用户关注的人详情页
@RequestMapping(path={"/user/{uid}/followees"},method = RequestMethod.GET) public String followees(Model model, @PathVariable("uid") int userId){ List<Integer> followeeIds = followService.getFollowees(userId,EntityType.ENTITY_USER,0,10); if(hostHolder.getUser()!=null){ model.addAttribute("followees",getUsersInfo(hostHolder.getUser().getId(),followeeIds)); }else{ model.addAttribute("followees", getUsersInfo(0, followeeIds)); } model.addAttribute("followeeCount",followService.getFolloweeCount(userId,EntityType.ENTITY_USER)); model.addAttribute("curUser",userService.selectById(userId)); return "followees"; } 123456789101112其中getUsersInfo是一个公用的函数,取了其中几个值:
private List<ViewObject> getUsersInfo(int localUserId, List<Integer> userIds) { List<ViewObject> userInfos = new ArrayList<ViewObject>(); for (Integer uid : userIds) { User user = userService.selectById(uid); if (user == null) { continue; } ViewObject vo = new ViewObject(); vo.set("user", user); vo.set("commentCount", commentService.getUserCommentCount(uid)); vo.set("followerCount", followService.getFollowerCount(EntityType.ENTITY_USER, uid)); vo.set("followeeCount", followService.getFolloweeCount(uid, EntityType.ENTITY_USER)); if (localUserId != 0) { vo.set("followed", followService.isFollower(localUserId, EntityType.ENTITY_USER, uid)); } else { vo.set("followed", false); } userInfos.add(vo); } return userInfos; }到这里FollowController的功能基本完成了.
FollowHandler
@Component public class FollowHandler implements EventHandler { @Autowired MessageService messageService; @Autowired UserService userService; @Override public void doHandle(EventModel model) { Message message = new Message(); message.setFromId(WendaUtil.SYSTEM_USERID); message.setToId(model.getEntityOwnerId()); message.setCreatedDate(new Date()); User user = userService.getUser(model.getActorId()); if (model.getEntityType() == EntityType.ENTITY_QUESTION) { message.setContent("用户" + user.getName() + "关注了你的问题,http://127.0.0.1:8080/question/" + model.getEntityId()); } else if (model.getEntityType() == EntityType.ENTITY_USER) { message.setContent("用户" + user.getName() + "关注了你,http://127.0.0.1:8080/user/" + model.getActorId()); } messageService.addMessage(message); } @Override public List<EventType> getSupportEventTypes() { return Arrays.asList(EventType.FOLLOW); } }下面主要是主页中关注人数的显示和问题详情页关注人列表的显示,这里使用viewObject类型(自定义的class,主要是{string: object})记录下这些需要的参数。
在HomeController.java中添加这部分代码 在getQuestion函数中添加:
private List<ViewObject> getQuestions(int userId, int offset, int limit) { List<Question> questionList = questionService.getLatestQuestions(userId, offset, limit); List<ViewObject> vos = new ArrayList<>(); for (Question question : questionList) { ViewObject vo = new ViewObject(); vo.set("question", question); //添加部分,取出关注人数---------- vo.set("followCount", followService.getFollowerCount(EntityType.ENTITY_QUESTION, question.getId())); //---------------------------- vo.set("user", userService.getUser(question.getUserId())); vos.add(vo); } return vos; }在QuestionController.java中的questionDetail函数中添加如下代码,添加在返回前面即可
List<ViewObject> followUsers = new ArrayList<ViewObject>(); // 获取关注的用户信息 List<Integer> users = followService.getFollowers(EntityType.ENTITY_QUESTION, qid, 20); for (Integer userId : users) { ViewObject vo = new ViewObject(); User u = userService.getUser(userId); if (u == null) { continue; } vo.set("name", u.getName()); vo.set("headUrl", u.getHeadUrl()); vo.set("id", u.getId()); followUsers.add(vo); } model.addAttribute("followUsers", followUsers); if (hostHolder.getUser() != null) { model.addAttribute("followed", followService.isFollower(hostHolder.getUser().getId(), EntityType.ENTITY_QUESTION, qid)); } else { model.addAttribute("followed", false); }到这里基本就结束了,关注功能基本完成。
参考:https://blog.csdn.net/weixin_43963656/article/details/84870103