第三章 高性能存储方案

Spring 整合 Redis

第三章 高性能存储方案_项目

引入依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Redis 相关配置类

@Configuration
public class RedisConfig {

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);

// 设置key的序列化方式
template.setKeySerializer(RedisSerializer.string());

// 设置value的序列化方式
template.setValueSerializer(RedisSerializer.json());

// 设置hash的key的序列化方式
template.setHashKeySerializer(RedisSerializer.string());

// 设置hash的value的序列化方式
template.setHashValueSerializer(RedisSerializer.json());

// 让设置生效
template.afterPropertiesSet();

return template;
}

}

测试

@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTests {

@Autowired
private RedisTemplate redisTemplate;

@Test
public void testStrings() {
String redisKey = "test:count";

redisTemplate.opsForValue().set(redisKey, 1);

System.out.println(redisTemplate.opsForValue().get(redisKey));
System.out.println(redisTemplate.opsForValue().increment(redisKey));
System.out.println(redisTemplate.opsForValue().decrement(redisKey));
}

@Test
public void testHashes() {
String redisKey = "test:user";

redisTemplate.opsForHash().put(redisKey, "id", 1);
redisTemplate.opsForHash().put(redisKey, "username", "zhangsan");

System.out.println(redisTemplate.opsForHash().get(redisKey, "id"));
System.out.println(redisTemplate.opsForHash().get(redisKey, "username"));
}

@Test
public void testLists() {
String redisKey = "test:ids";

redisTemplate.opsForList().leftPush(redisKey, 101);
redisTemplate.opsForList().leftPush(redisKey, 102);
redisTemplate.opsForList().leftPush(redisKey, 103);

System.out.println(redisTemplate.opsForList().size(redisKey));
System.out.println(redisTemplate.opsForList().index(redisKey, 0));
System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2));

System.out.println(redisTemplate.opsForList().leftPop(redisKey));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
System.out.println(redisTemplate.opsForList().leftPop(redisKey));
}

@Test
public void testSets() {
String redisKey = "test:teachers";

redisTemplate.opsForSet().add(redisKey, "刘备", "关羽", "张飞", "赵云", "诸葛亮");

System.out.println(redisTemplate.opsForSet().size(redisKey));
System.out.println(redisTemplate.opsForSet().pop(redisKey));
System.out.println(redisTemplate.opsForSet().members(redisKey));
}

@Test
public void testSortedSets() {
String redisKey = "test:students";

redisTemplate.opsForZSet().add(redisKey, "唐僧", 80);
redisTemplate.opsForZSet().add(redisKey, "悟空", 90);
redisTemplate.opsForZSet().add(redisKey, "八戒", 50);
redisTemplate.opsForZSet().add(redisKey, "沙僧", 70);
redisTemplate.opsForZSet().add(redisKey, "白龙马", 60);

System.out.println(redisTemplate.opsForZSet().zCard(redisKey));
System.out.println(redisTemplate.opsForZSet().score(redisKey, "八戒"));
System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "八戒"));
System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2));
}

@Test
public void testKeys() {
redisTemplate.delete("test:user");

System.out.println(redisTemplate.hasKey("test:user"));

redisTemplate.expire("test:students", 10, TimeUnit.SECONDS);
}

// 多次访问同一个key
@Test
public void testBoundOperations() {
String redisKey = "test:count";
BoundValueOperations operations = redisTemplate.boundValueOps(redisKey);
operations.increment();
operations.increment();
operations.increment();
operations.increment();
operations.increment();
System.out.println(operations.get());
}

// 编程式事务
@Test
public void testTransactional() {
Object obj = redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String redisKey = "test:tx";

operations.multi(); // 启动事务

operations.opsForSet().add(redisKey, "zhangsan");
operations.opsForSet().add(redisKey, "lisi");
operations.opsForSet().add(redisKey, "wangwu");

System.out.println(operations.opsForSet().members(redisKey));

return operations.exec(); // 提交事务
}
});
System.out.println(obj);
}

}

整合点赞功能

第三章 高性能存储方案_redis_02

工具类

  • RedisKeyUtil.java
public class RedisKeyUtil {

private static final String SPLIT = ":";
private static final String PREFIX_ENTITY_LIKE = "like:entity";

// 某个实体的赞
// like:entity:entityType:entityId -> set(userId)
public static String getEntityLikeKey(int entityType, int entityId) {
return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
}

}

业务层

  • LikeService.java
@Service
public class LikeService {

@Autowired
private RedisTemplate redisTemplate;

/**
* 点赞
*
* @param userId 要给哪个帖子或者评论点赞的当前用户
* @param entityType 实体类型,帖子或者评论,1表示帖子,2表示评论或回复
* @param entityId 实体的id,帖子id或评论id
*/
public void like(int userId, int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
// 判断当前实体是否被点过赞
Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);
if (isMember) { // 点过赞就需要取消点赞
redisTemplate.opsForSet().remove(entityLikeKey, userId);
} else { // 没点过赞就需要进行点赞操作
redisTemplate.opsForSet().add(entityLikeKey, userId);
}
}

// 查询实体被点赞的数量
public long findEntityLikeCount(int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().size(entityLikeKey);
}

// 查询某个人对实体的点赞状态(有没有点过赞),1表示点过赞,0表示没点过赞
public int findEntityLikeStatus(int userId, int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;
}

}

控制层

  • HomeController.java
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model, Page page) {
// 方法调用前,SpringMVC会自动实例化Model和Page,并将Page注入Model
// 所以,在thymeleaf中可以直接访问Page对象中的数据
page.setRows(discussPostService.findDiscussPostRows(0));
page.setPath("/index");

List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());
List<Map<String, Object>> discussPosts = new ArrayList<>();
if (list != null) {
for (DiscussPost post : list) {
Map<String, Object> map = new HashMap<>();
map.put("post", post);
User user = userService.findUserById(post.getUserId());
map.put("user", user);

// 给帖子显示点赞数量
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId());
map.put("likeCount", likeCount);

discussPosts.add(map);
}
}
model.addAttribute("discussPosts", discussPosts);
return "/index";
}
  • LikeController.java
@Controller
public class LikeController {

@Autowired
private LikeService likeService;

@Autowired
private HostHolder hostHolder;

@RequestMapping(path = "/like", method = RequestMethod.POST)
@ResponseBody
public String like(int entityType, int entityId) {
User user = hostHolder.getUser();

// 点赞
likeService.like(user.getId(), entityType, entityId);
// 数量
long likeCount = likeService.findEntityLikeCount(entityType, entityId);
// 状态
int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
// 返回的结果
HashMap<String, Object> map = new HashMap<>();
map.put("likeCount", likeCount);
map.put("likeStatus", likeStatus);

return CommunityUtil.getJSONString(0, null, map);
}

}
  • DiscussPostController.java
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
// 查询帖子
DiscussPost post = discussPostService.findDiscusspostById(discussPostId);
model.addAttribute("post", post);
// 作者
User user = userService.findUserById(post.getUserId());
model.addAttribute("user", user);
// 帖子的点赞数量
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, discussPostId);
model.addAttribute("likeCount", likeCount);
// 帖子的点赞状态
int likeStatus = hostHolder.getUser() == null ? 0 : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_POST, discussPostId);
model.addAttribute("likeStatus", likeStatus);

// 查询评论信息
// 评论分页信息
page.setLimit(5);
page.setPath("/discuss/detail/" + discussPostId);
page.setRows(post.getCommentCount());

/**
* 说明:
* 评论:给帖子的评论
* 回复:给评论的评论
* vo:view object
*/
// 查询当前帖子的所有评论,下面这个commentList是评论列表
List<Comment> commentList = commentService.findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());

// 评论的vo列表
List<Map<String, Object>> commentVoList = new ArrayList<>();
if (commentList != null) {
for (Comment comment : commentList) {
// 一个评论的vo
Map<String, Object> commentVo = new HashMap<>();
// 往vo里添加评论
commentVo.put("comment", comment);
// 往vo里添加评论的作者
commentVo.put("user", userService.findUserById(comment.getUserId()));

// 评论的点赞数量
likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("likeCount", likeCount);
// 评论的点赞状态
likeStatus = hostHolder.getUser() == null ? 0 : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("likeStatus", likeStatus);

// 下面这个replyList是回复列表,(offset:0,limit:Integer.MAX_VALUE 表示有多少评论就查多少评论,不分页)
List<Comment> replyList = commentService.findCommentsByEntity(ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);

// 回复的vo列表
List<Map<String, Object>> replyVoList = new ArrayList<>();
if (replyList != null) {
for (Comment reply : replyList) {
// 一个回复的vo
Map<String, Object> replyVo = new HashMap<>();
// 往vo里添加回复
replyVo.put("reply", reply);
// 往vo里添加回复的作者
replyVo.put("user", userService.findUserById(reply.getUserId()));
/**
* 往vo里添加回复的目标
* target:0表示普通回复
* target:非0 表示回复作者的评论
*/
User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
replyVo.put("target", target);

// 回复的点赞数量
likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, reply.getId());
replyVo.put("likeCount", likeCount);
// 回复的点赞状态
likeStatus = hostHolder.getUser() == null ? 0 : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, reply.getId());
replyVo.put("likeStatus", likeStatus);

replyVoList.add(replyVo);
}
}
commentVo.put("replys", replyVoList);

// 回复的数量
int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("replyCount", replyCount);

commentVoList.add(commentVo);
}
}

model.addAttribute("comments", commentVoList);

return "/site/discuss-detail";
}

重构点赞功能及开发个人主页

第三章 高性能存储方案_spring_03

工具类

  • RedisKeyUtil.java
public class RedisKeyUtil {

private static final String SPLIT = ":";
private static final String PREFIX_ENTITY_LIKE = "like:entity";
private static final String PREFIX_USER_LIKE = "like:user";

// 某个实体的赞
// like:entity:entityType:entityId -> set(userId)
public static String getEntityLikeKey(int entityType, int entityId) {
return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
}

// 某个用户的赞
// like:user:userId -> int
public static String getUserLikeKey(int userId) {
return PREFIX_USER_LIKE + SPLIT + userId;
}

}

业务层

  • LikeService.java
@Service
public class LikeService {

@Autowired
private RedisTemplate redisTemplate;

/**
* 点赞
*
* @param userId 要给哪个帖子或者评论点赞的当前用户
* @param entityType 实体类型,帖子或者评论,1表示帖子,2表示评论或回复
* @param entityId 实体的id,帖子id或评论id
* @param entityUserId 实体的用户id
*/
public void like(int userId, int entityType, int entityId, int entityUserId) {
// 编程式事务
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
// 这里的entityUserId是被赞的那个用户
String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId);
// 判断当前用户有没有对这个实体点过赞
boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);

// 开启事务
operations.multi();

if (isMember) { // 已经点过赞
// 需要取消点赞
operations.opsForSet().remove(entityLikeKey, userId);
operations.opsForValue().decrement(userLikeKey);
} else {
// 没点过赞就需要增加点赞
operations.opsForSet().add(entityLikeKey, userId);
operations.opsForValue().increment(userLikeKey);
}

// 提交事务
return operations.exec();
}
});
}

// 查询实体被点赞的数量
public long findEntityLikeCount(int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().size(entityLikeKey);
}

// 查询某个人对实体的点赞状态(有没有点过赞),1表示点过赞,0表示没点过赞
public int findEntityLikeStatus(int userId, int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;
}

// 查询某个用户获得的赞的数量
public int findUserLikeCount(int userId) {
String userLikeKey = RedisKeyUtil.getUserLikeKey(userId);
Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey);
return count == null ? 0 : count.intValue();
}

}

控制层

  • LikeController.java
@Controller
public class LikeController {

@Autowired
private LikeService likeService;

@Autowired
private HostHolder hostHolder;

@RequestMapping(path = "/like", method = RequestMethod.POST)
@ResponseBody
public String like(int entityType, int entityId, int entityUserId) {
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);
// 返回的结果
HashMap<String, Object> map = new HashMap<>();
map.put("likeCount", likeCount);
map.put("likeStatus", likeStatus);

return CommunityUtil.getJSONString(0, null, map);
}

}
  • UserController.java
// 个人主页
@RequestMapping(path = "/profile/{userId}", method = RequestMethod.GET)
public String getProfilePage(@PathVariable("userId") int userId, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}

// 用户
model.addAttribute("user", user);
// 用户的获赞数量
int likeCount = likeService.findUserLikeCount(userId);
model.addAttribute("likeCount", likeCount);

return "/site/profile";
}

整合关注功能

第三章 高性能存储方案_redis缓存_04

工具类

  • RedisKeyUtil.java
public class RedisKeyUtil {

private static final String SPLIT = ":";
// 关注的目标
private static final String PREFIX_FOLLOWEE = "followee";
// 该用户的粉丝
private static final String PREFIX_FOLLOWER = "follower";

// 某个用户关注的实体
// followee:userId:entityType -> zset(entityId,now)
public static String getFolloweeKey(int userId, int entityType) {
return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType;
}

// 某个实体拥有的粉丝
// follower:entityType:entityId -> zset(userId,now)
public static String getFollowerKey(int entityType, int entityId) {
return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId;
}

}
  • CommunityConstant.java
public interface CommunityConstant {

/**
* 实体类型: 帖子
*/
int ENTITY_TYPE_POST = 1;

/**
* 实体类型: 评论
*/
int ENTITY_TYPE_COMMENT = 2;

/**
* 实体类型: 用户
*/
int ENTITY_TYPE_USER = 3;

}

业务层

  • FollowService.java
@Service
public class FollowService {

@Autowired
private RedisTemplate redisTemplate;

// 关注
public void follow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);

// 开启事务
operations.multi();

operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());

// 提交事务
return operations.exec();
}
});
}

// 取消关注
public void unfollow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);

// 开启事务
operations.multi();

operations.opsForZSet().remove(followeeKey, entityId);
operations.opsForZSet().remove(followerKey, userId);

// 提交事务
return operations.exec();
}
});
}

// 查询某个用户关注的实体的数量
public long findFolloweeCount(int userId, int entityType) {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
return redisTemplate.opsForZSet().zCard(followeeKey);
}

// 查询实体的粉丝数量
public long findFollowerCount(int entityType, int entityId) {
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
return redisTemplate.opsForZSet().zCard(followerKey);
}

// 判断当前用户是否已关注该实体
public boolean hasFollowed(int userId, int entityType, int entityId) {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
return redisTemplate.opsForZSet().score(followeeKey, entityId) != null;
}

}

控制层

  • FollowController.java
@Controller
public class FollowController {

@Autowired
private FollowService followService;

@Autowired
private HostHolder hostHolder;

// 关注
@RequestMapping(path = "/follow", method = RequestMethod.POST)
@ResponseBody
public String follow(int entityType, int entityId) {
User user = hostHolder.getUser();
followService.follow(user.getId(), entityType, entityId);

return CommunityUtil.getJSONString(0, "已关注!");
}

// 取消关注
@RequestMapping(path = "/unfollow", method = RequestMethod.POST)
@ResponseBody
public String unfollow(int entityType, int entityId) {
User user = hostHolder.getUser();
followService.unfollow(user.getId(), entityType, entityId);

return CommunityUtil.getJSONString(0, "已取消关注!");
}

}
  • UserController.java
// 个人主页
@RequestMapping(path = "/profile/{userId}", method = RequestMethod.GET)
public String getProfilePage(@PathVariable("userId") int userId, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}

// 用户
model.addAttribute("user", user);
// 用户的获赞数量
int likeCount = likeService.findUserLikeCount(userId);
model.addAttribute("likeCount", likeCount);

// 用户的关注数量
long followeeCount = followService.findFolloweeCount(userId, ENTITY_TYPE_USER);
model.addAttribute("followeeCount", followeeCount);

// 用户的粉丝数量
long followerCount = followService.findFollowerCount(ENTITY_TYPE_USER, userId);
model.addAttribute("followerCount", followerCount);

// 当前用户对该用户是否已关注
boolean hasFollowed = false;
if (hostHolder.getUser() != null) {
hasFollowed = followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
model.addAttribute("hasFollowed", hasFollowed);

return "/site/profile";
}

关注列表、粉丝列表

第三章 高性能存储方案_spring_05

业务层

  • FollowService.java
// 查询某个用户关注的人
public List<Map<String, Object>> findFollowees(int userId, int offset, int limit) {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER);
// 倒序查询,按照score(时间)从大到小
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1);
if (targetIds == null) {
return null;
}
List<Map<String, Object>> list = new ArrayList<>();
for (Integer targetId : targetIds) {
Map<String, Object> map = new HashMap<>();
User user = userService.findUserById(targetId);
map.put("user", user);
Double score = redisTemplate.opsForZSet().score(followeeKey, targetId);
// 将毫秒数转成时间
map.put("followTime", new Date(score.longValue()));
list.add(map);
}
return list;
}

// 查询某个用户的粉丝
public List<Map<String, Object>> findFollowers(int userId, int offset, int limit) {
String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId);
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followerKey, offset, offset + limit - 1);
if (targetIds == null) {
return null;
}
List<Map<String, Object>> list = new ArrayList<>();
for (Integer targetId : targetIds) {
Map<String, Object> map = new HashMap<>();
User user = userService.findUserById(targetId);
map.put("user", user);
Double score = redisTemplate.opsForZSet().score(followerKey, targetId);
// 将毫秒数转成时间
map.put("followTime", new Date(score.longValue()));
list.add(map);
}
return list;
}

控制层

  • FollowController.java
@RequestMapping(path = "/followees/{userId}", method = RequestMethod.GET)
public String getFollowees(@PathVariable("userId") int userId, Page page, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user", user);

// 设置分页信息
page.setLimit(5);
page.setPath("/followees/" + userId);
page.setRows((int) followService.findFolloweeCount(userId, ENTITY_TYPE_USER));

// 当前用户的关注列表
List<Map<String, Object>> userList = followService.findFollowees(userId, page.getOffset(), page.getLimit());
if (userList != null) {
// 遍历关注列表
for (Map<String, Object> map : userList) {
// 关注列表的每一个用户
User u = (User) map.get("user");
// 当前用户对关注列表的每个用户的关注状态
map.put("hasFollowed", hasFollowed(u.getId()));
}
}
model.addAttribute("users", userList);

return "/site/followee";
}

// 判断当前用户对这个用户(userId)的关注状态
private boolean hasFollowed(int userId) {
// 如果当前用户没有登录直接返回false就行
if (hostHolder.getUser() == null) {
return false;
}
return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}

@RequestMapping(path = "/followers/{userId}", method = RequestMethod.GET)
public String getFollowers(@PathVariable("userId") int userId, Page page, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user", user);

// 设置分页信息
page.setLimit(5);
page.setPath("/followers/" + userId);
page.setRows((int) followService.findFollowerCount(ENTITY_TYPE_USER, userId));

// 当前用户的粉丝列表
List<Map<String, Object>> userList = followService.findFollowers(userId, page.getOffset(), page.getLimit());
if (userList != null) {
// 遍历粉丝列表
for (Map<String, Object> map : userList) {
// 粉丝列表的每一个用户
User u = (User) map.get("user");
// 当前用户对粉丝列表的每个用户的关注状态
map.put("hasFollowed", hasFollowed(u.getId()));
}
}
model.addAttribute("users", userList);

return "/site/follower";
}

优化登录模块

第三章 高性能存储方案_spring_06

使用 Redis 存储验证码

工具类

  • RedisKeyUtil.java
public class RedisKeyUtil {

// 验证码
private static final String PREFIX_KAPTCHA = "kaptcha";

// 登录验证码,owner代表临时令牌
public static String getKaptchaKey(String owner) {
return PREFIX_KAPTCHA + SPLIT + owner;
}

}

控制层

  • LoginController.java
// 生成验证码的方法
@RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
public void getKaptcha(HttpServletResponse response /*, HttpSession session*/) { // 因为验证码属于敏感数据,所以存在session比较合适
// 生成验证码文本
String text = kaptchaProducer.createText();
// 通过验证码文本生成验证码图片
BufferedImage image = kaptchaProducer.createImage(text);

// 将验证码存入session
//session.setAttribute("kaptcha", text); 替换成下面的redis

// 验证码的归属者
String kaptchaOwner = CommunityUtil.generateUUID();
Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
cookie.setMaxAge(60); // cookie失效时间为60秒
cookie.setPath(contextPath); // cookie在整个路径范围都有效
response.addCookie(cookie);
// 将验证码存入redis
String redKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(redKey, text, 60, TimeUnit.SECONDS); // 验证码过期时间60s

// 将验证码图片输出给浏览器
response.setContentType("image/png"); // 设置传输的格式
try {
OutputStream os = response.getOutputStream(); // 获取输出字节流
ImageIO.write(image, "png", os);
} catch (IOException e) {
logger.error("响应验证码失败:" + e.getMessage());
}
}

// 登录
@RequestMapping(path = "/login", method = RequestMethod.POST)
public String login(String username, String password, String code, boolean rememberme, Model model, /* HttpSession session, */ HttpServletResponse response, @CookieValue("kaptchaOwner") String kaptchaOwner) {

// 检查验证码
// String kaptcha = (String) session.getAttribute("kaptcha"); 替换成下面的redis
String kaptcha = null;
if (StringUtils.isNoneBlank(kaptchaOwner)) {
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
kaptcha = (String) redisTemplate.opsForValue().get(redisKey);
}

// equalsIgnoreCase表示验证码忽略大小写
if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
model.addAttribute("codeMsg", "验证码不正确!");
// 跳转到登录页面
return "/site/login";
}

// 检查账号和密码
int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
Map<String, Object> map = userService.login(username, password, expiredSeconds);
if (map.containsKey("ticket")) {
// cookie的key和value都必须是字符串类型
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
// 设置cookie在什么范围内有效,这里设置整个项目的访问路径都有效
cookie.setPath(contextPath);
// 设置cookie有效时间
cookie.setMaxAge(expiredSeconds);
// 发送cookie到浏览器
response.addCookie(cookie);

// 拿到ticket,说明登录成功,重定向到首页
return "redirect:/index";
} else {
model.addAttribute("usernameMsg", map.get("usernameMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));

// 登录失败,跳转到登录页面
return "/site/login";
}
}

使用 Redis 存储登录凭证

工具类

  • RedisKeyUtil.java
public class RedisKeyUtil {

// 登录凭证
private static final String PREFIX_TICKET = "ticket";

// 登录的凭证
public static String getTicketKey(String ticket) {
return PREFIX_TICKET + SPLIT + ticket;
}

}

数据层

  • UserService.java
/**
* 登录方法
*
* @param username
* @param password
* @param expiredSeconds 单位是毫秒
* @return
*/
public Map<String, Object> login(String username, String password, int expiredSeconds) {
Map<String, Object> map = new HashMap<>();

// 空值处理
if (StringUtils.isBlank(username)) {
map.put("usernameMsg", "账号不能为空!");
return map;
}
if (StringUtils.isBlank(password)) {
map.put("passwordMsg", "密码不能为空!");
return map;
}

// 验证账号
User user = userMapper.selectByName(username);
if (user == null) {
map.put("usernameMsg", "该账号不存在!");
return map;
}
// 验证状态
if (user.getStatus() == 0) { // 等于0表示该账号没有激活
map.put("usernameMsg", "该账号未激活!");
return map;
}
// 验证密码
password = CommunityUtil.md5(password + user.getSalt());
if (!user.getPassword().equals(password)) {
map.put("passwordMsg", "密码不正确!");
return map;
}

// 登录成功,需要生成登录凭证
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(user.getId());
loginTicket.setTicket(CommunityUtil.generateUUID());
loginTicket.setStatus(0); // 0表示该凭证有效
loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
// loginTicketMapper.insertLoginTicket(loginTicket); 替换成下面的redis

String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
redisTemplate.opsForValue().set(redisKey, loginTicket); // redis会把loginTicket对象保存为json格式的字符串

map.put("ticket", loginTicket.getTicket()); // 将ticket数据存放到map中
return map;
}

/**
* 退出登录的方法
*
* @param ticket
*/
public void loginOut(String ticket) {
// loginTicketMapper.updateStatus(ticket, 1); // 1表示该凭证无效 替换成下面的redis
String redisKey = RedisKeyUtil.getTicketKey(ticket);
LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
loginTicket.setStatus(1); // 1表示该凭证无效
redisTemplate.opsForValue().set(redisKey, loginTicket);
}

/**
* 通过ticket查询用户信息
*
* @param ticket
* @return
*/
public LoginTicket findLoginTicket(String ticket) {
// return loginTicketMapper.selectByTicket(ticket); 替换成下面的redis
String redisKey = RedisKeyUtil.getTicketKey(ticket);
return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
}

使用 Redis 缓存用户信息

工具类

  • RedisKeyUtil.java
public class RedisKeyUtil {

// 用户信息
private static final String PREFIX_USER = "user";

// 用户信息
public static String getUserKey(int userId) {
return PREFIX_USER + SPLIT + userId;
}
}

业务层

  • UserService.java
public User findUserById(int id) {
// return userMapper.selectById(id);
User user = getCache(id);
if (user == null) {
user = initCache(id);
}
return user;
}

/**
* 激活邮件的方法
*
* @param userId 用户id
* @param code 激活码
* @return
*/
public int activation(int userId, String code) {
User user = userMapper.selectById(userId);
if (user.getStatus() == 1) { // 用户状态已激活:1,未激活:0
return ACTIVATION_REPEAT; // 重复激活
} else if (user.getActivationCode().equals(code)) { // 激活码匹配上了
// 将用户的status状态修改为1
userMapper.updateStatus(userId, 1);
// 清理缓存
clearCache(userId);
return ACTIVATION_SUCCESS; // 激活成功
} else {
return ACTIVATION_FAILURE; // 激活失败
}
}

/**
* 更新用户头像的路径
*
* @param userId
* @param headerUrl
* @return
*/
public int updateHeader(int userId, String headerUrl) {
// return userMapper.updateHeader(userId, headerUrl);
int rows = userMapper.updateHeader(userId, headerUrl);
// 清理缓存
clearCache(userId);
return rows;
}

/**
* 修改密码
*
* @param userId 要修改密码的用户id
* @param oldPassword 原始密码
* @param newPassword 新密码
*/
public boolean updatePassword(int userId, String oldPassword, String newPassword) {
User user = userMapper.selectById(userId);
// 获取原密码的盐值
String salt = user.getSalt();
// 先校验原密码是否匹配
if (CommunityUtil.md5(oldPassword + salt).equals(user.getPassword())) {
// 设置新密码
newPassword = CommunityUtil.md5(newPassword + salt);
userMapper.updatePassword(userId, newPassword);
// 清理缓存
clearCache(userId);
return true;
}
return false;
}

// 重点:保证数据库和缓存的数据一致性
// 1. 优先从缓存中取值
private User getCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
return (User) redisTemplate.opsForValue().get(redisKey);
}

// 2. 取不到时初始化缓存数据
private User initCache(int userId) {
User user = userMapper.selectById(userId);
String redisKey = RedisKeyUtil.getUserKey(userId);
// 设置缓存过期时间一小时
redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);
return user;
}

// 3. 当数据发生变更时清除缓存数据
private void clearCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
// 删除对应的key
redisTemplate.delete(redisKey);
}