仿牛客论坛项目05-Redis
点赞功能、我收到的赞、关注和取消关注、关注列表和粉丝列表、Redis优化
Redis 入门
- NoSQL(not only):关系型数据库之外的数据库
- Redis 是基于键值对的 NoSQL 数据库,key 是 String 类型,值支持多种数据结构:字符串、哈希、列表、集合、有序集合
- Redis 将所有数据都存放在内存,读写性能很强;并将内存中的数据以快照或日志的形式保存到硬盘上,保证数据安全性。
- 典型应用场景:缓存、排行榜、计数器(帖子浏览量)、社交网络(点赞)、消息队列。
安装方法:brew install redis
启动 redis 服务器:redis-server
连接到 redis 服务器:redis-cli
Spring 整合 Redis
- 点赞
- 针对帖子、评论点赞
- 第一次点赞、第二次点赞取消
- 首页显示点赞数量
- 统计帖子点赞数量
- 详情页显示点赞数量
- 统计点赞数量
- 显示点赞状态
用 redis 提高性能。
直接在业务层,在redis中存数据
点赞
- 点赞
- 对帖子、评论点赞
- 第一次点赞、第二次取消点赞
- 首页点赞数量
- 统计帖子点赞数
- 详情页点赞数量
- 统计点赞数量
- 显示点赞状态
redis中存储格式:like:entity:entityType:entityId -> set(userId)
like:user:userId -> int
我收到的赞
重构点赞功能
- 以用户为key,记录点赞数量
开发个人主页 - 以用户为key,查询点赞数量
点赞业务中加上维护用户收到的赞,一个业务中有两个更新,要用 redis 的事务操作。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public void like(int userId, int entityType, int entityId, int entityUserId) {
// String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
// System.out.println(entityLikeKey);
// // 判断当前用户是否点过赞,userId是否在redis集合里
// boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);
// if (isMember) {
// // 如果点过赞,就取消点赞
// redisTemplate.opsForSet().remove(entityLikeKey, userId);
// } else {
// // 如果没点过赞,就点赞
// redisTemplate.opsForSet().add(entityLikeKey, userId);
// }
redisTemplate.execute(new SessionCallback() {
public Object execute(RedisOperations operations) throws DataAccessException {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
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();
}
});
}
关注、取消关注
- 需求
- 实现关注、取消关注
- 统计用户关注数、粉丝数
- 关键
- 关系:A关注B,A是B的粉丝(follower),B是A的关注目标(followee)
- 关注的目标可以用用户、帖子、题目,实现时抽象为实体(存在redis)
关注和取消关注是异步请求。(页面不会刷新)
使用redis有序列表zset,新建两个主键:某个用户关注的实体,某个实体拥有的粉丝。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public void follow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
public Object execute(RedisOperations operations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getPrefixFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getPrefixFollowerKey(entityType, entityId);
operations.multi();
// 某个用户关注的实体
operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
// 某个实体拥有的粉丝
operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());
return operations;
}
});
}
redis 中存储格式:followee:userId:entityType -> zset(entityId, now)
follower:entityType:entityId -> zset(userId, now)
关注列表、粉丝列表
- 业务层
- 查询某个用户关注的人,分页
- 查询某个用户的粉丝,分页
- 表现层
- 查询关注的人、查询粉丝请求
- 查询关注的人、查询粉丝模版
查询关注的人,需要分页信息、关注的人列表、当前用户是否关注过这个人1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public String getFollowees(int userId, Model model, Page page) {
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";
}
优化登录模块
- 使用 Redis 存储验证码
- 验证码要频繁访问和刷新,对性能要求高
- 验证码不需要永久保存,很短时间后失效
- 目前存在了 session 中,用户量大时,存储大量验证码会占用内容,影响性能;分布式部署时,不同服务器上的 session 无法共享,导致无法验证。
- 使用 Redis 存储登录凭证
- 目前登录凭证存在 Mysql 中,每次登录都要查询用户凭证,访问频率很高
- 使用 Redis 缓存用户信息
- 处理每次请求,都要根据凭证查询用户信息,访问频率高
总结适合用 Redis 存储的数据:
- 需要频繁读写,但对一致性要求不高的数据,如:热点数据、排行版、计数器
- 需要缓存的数据,如用户信息、文章内容
- 需要短时存储的数据,如验证码
- 需要实时统计的数据,如点赞数、浏览量、粉丝数
Redis 作为缓存 + MySQL 作为持久化存储
验证码使用 redis 存储
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 存入redis
// 验证码的归属,用cookie保存
String kapthcaOwner = CommunityUtil.generateUUID();
Cookie cookie = new Cookie("kaptchaOwner", kapthcaOwner);
cookie.setMaxAge(60);
cookie.setPath(contextPath);
response.addCookie(cookie);
// 从redis中取出验证码
String kaptcha = null;
if(StringUtils.isNoneBlank(kaptchaOwner)) {
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
kaptcha = redisTemplate.opsForValue().get(redisKey).toString();
}登录凭证使用 redis 存储
登录时存入,登出时删掉1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());
// redis 将对象序列化为JSON格式的字符串
redisTemplate.opsForValue().set(redisKey, loginTicket, expiredSeconds, TimeUnit.SECONDS);
// 退出
public void logout(String ticket) {
// loginTicketMapper.updateStatus(ticket, 1);
String redisKey = RedisKeyUtil.getTicketKey(ticket);
// 将对象的status改为1
LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
loginTicket.setStatus(1);
// 改完再存进去
redisTemplate.opsForValue().set(redisKey, loginTicket);
}
// 根据ticket查询凭证
public LoginTicket findLoginTicket(String ticket) {
// return loginTicketMapper.selectByTicket(ticket);
String redisKey = RedisKeyUtil.getTicketKey(ticket);
return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
}redis 缓存用户信息
调用最多的方法是public User findUserById(int id)
查询 User 时,先尝试从缓存中取值,能取到就用,取不到就做初始化存入。
更改用户信息时,把缓存删除。如果更新缓存,会有并发的问题。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 1. 优先从缓存中取值
private User getCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
return (User) redisTemplate.opsForValue().get(redisKey);
}
// 2. 取不到时初始化缓存数据
private User initCache(int userId) {
// 从 MySQL 中查到数据
User user = userMapper.selectById(userId);
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);
return user;
}
// 3. 数据变更时,清楚缓存数据
public void clearCache(int userId) {
String redisKey = RedisKeyUtil.getUserKey(userId);
redisTemplate.delete(redisKey);
}
问题
- 为什么用 redis
1、Redis是⼀种基于键值对的NoSQL数据库,它⽀持多种数据结构:
字符串(String)、哈希(hashs)、列表(lists)、集合(sets)、有序集合(sorted sets)等
2、Redis将所有的数据都存在内存中,所以它的读写性能⼗分惊⼈。
同时,Redis还可以将内存中的数据以快照或者⽇志的形式保存到硬盘上,以保证数据的安全性。
3、缓存、排⾏榜(热⻔帖⼦)、计数器、社交⽹络(点赞数)、消息队列等。 - 如何优化登陆模块?
当⽤户点击刷新验证码时,服务端⾸先给当前需要登陆的游客,设置⼀个随机字符串(kaptchaOwner),⽤于标识当前这个游客,然后将随机字符串存⼊到cookie中,返回给浏览器,然后服务端的 redis 保存 key:随机字符串,接着⽤户输⼊⽤户名,密码,验证码,再次点击登陆时,服务端会从 cookie 中拿到 kaptchaOwner ,通过它可以从 Redis 中得到正确的验证码,然后与⽤户输⼊的验证码做⽐较,看是否⼀致。