Redis
NoSQL数据库:除了关系型数据库以外的数据库
Redis:基于键值对的NoSQL型数据库,key只是String,value支持多种数据结构:字符串(String)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等
Redis将所有的数据存到内存中,读写性能很好,同时Redis可以以快照或日志的形式把内存中的数据保存到硬盘上,以保证数据的安全
Redis的典型的应用场景:缓存、排行榜、计数器、社交网络、消息队列等。
常用命令
redis-cli:命令行中开启redis
flushdb:刷新库,库里内容删除
字符串类型
set test:count 1
get test:count
incr test:count 变量加一
decr test:count 变量减一
哈希类型
hset test:user id 1
hset test:user username zhangsan
hget test:user id
list列表类型(横向容器:支持左右进,左右出)
lpush test:ids 101 102 103 左边进
llen test:ids 列表的长度
lindex test:ids 0 看索引为0的值
lrange test:ids 0 2 看索引为0到2的值
rpop test:ids 从右边弹出一个值
set集合类型(无序,不重复)
sadd test:students aaa bbb ccc 向集合添加元素
scard test:students 统计集合中有多少元素
spop test:students 从集合中随机弹出元素 (eg:抽奖)
smember test:students 统计集合中还有多少元素
sortedset 有序集合(提供按分数排序的功能)
zadd test:students 10 aaa 20 bbb 30 ccc 40 ddd 向集合添加元素,(10,20,30是分数)
zcard test:students 统计集合中有多少元素
zscore test:students ccc 查ccc的分数
zrank test:students ccc 查ccc的排名(从小到大,0开始)
zrange test:students 0 2 从小到大排序,取0-2范围的数
全局的命令,对所有的数据类型都有效
keys * 查库里key有什么
keys test * 查库里以test开头的可以有什么
type test:user 看这个key的值的类型
exists test:user 看这个key是否存在
del test:user 删掉这个key
expire test:user 10 设置key的过期时间,单位秒
Spring整合Redis
- 引入依赖
- 配置Redis
配置数据库参数
编写配置类,构造RedisTemplate(Spring Boot自动实现的key是object类型,变成String类型) - 访问Redis
RedisTemplate.opsForValue()
RedisTemplate.opsForHash()
RedisTemplate.opsForList()
RedisTemplate.opsForSet()
RedisTemplate.opsForzSet()
// 编程式事务
// 把操作放到一个队列中去,提交时统一提交给redis,统一处理
@Test
public void testTransactional() {
Object obj = redisTemplate.execute(new SessionCallback() {
@Override
// operations执行命令对象,这个方法调用时会把执行命令对象传进来,用其去管理事务
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(); //提交事务
}
});
点赞
- 点赞 (异步请求)
支持对帖子、评论点赞
第一次点赞,第二次点取消点赞 - 首页点赞数量
统计帖子的点赞数量 - 详情页点赞数量
统计点赞数量
显示点赞状态
Redis根据key进行数据的存储和获得,指定key的格式,有利于查询和存储
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";
private static final String PREFIX_FOLLOWEE = "followee";
private static final String PREFIX_FOLLOWER = "follower";
private static final String PREFIX_KAPTCHA = "kaptcha";
private static final String PREFIX_TICKET = "ticket";
private static final String PREFIX_USER = "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;
}
// 某个用户关注的实体
// 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;
}
// 登录验证码
public static String getKaptchaKey(String owner) {
return PREFIX_KAPTCHA + SPLIT + owner;
}
// 登录的凭证
public static String getTicketKey(String ticket) {
return PREFIX_TICKET + SPLIT + ticket;
}
// 用户
public static String getUserKey(int userId) {
return PREFIX_USER + SPLIT + userId;
@Service
public class LikeService {
@Autowired
private RedisTemplate redisTemplate;
// 点赞
// 事务管理,点赞状态和用户收到的赞的总改变要同时进行
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);
String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId);
//userid判断是否已经在entityLikeKey对应的set中了
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);
}
// 查询某人对某实体的点赞状态
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();
}
@LoginRequired //自定义注解,不登录不能点赞
@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);
// 返回的结果
Map<String, Object> map = new HashMap<>();
map.put("likeCount", likeCount);
map.put("likeStatus", likeStatus);
return CommunityUtil.getJSONString(0, null, map); //异步请求
}
关注、取消关注
- 需求
开发关注、取消关注功能。
统计用户的关注数、粉丝数 - 关键
若A关注了B,则A是B的Follower(粉丝),B是A的Followee(目标)
关注的目标可以是用户、帖子、题目等,在实现时将这些目标抽象为实体
@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
// 个人主页
@LoginRequired
@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";
}
关注列表、粉丝列表
- 业务层
查询某个用户关注的人, 支持分页。
查询某个用户的粉丝,支持分页 - 表现层
处理“查询关注的人”,“查询粉丝”请求
编写“查询关注的人”,“查询粉丝”模板
关注列表查询对方关注的用户,并将其user,关注时间,以及我是否关注这几个信息返回
@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";
}
@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";
}
private boolean hasFollowed(int userId) {
if (hostHolder.getUser() == null) {
return false;
}
return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
优化登录模块
- 使用Redis存储验证码
验证码需要频繁的访问和刷新,对性能要求较高
验证码不需要永久保存,在较短时间内会失效
分布式部署Session共享问题 - 使用Redis存储登录凭证
处理每次请求时,都要查询用户的登录凭证,访问频率高 - 使用Redis缓存用户信息
处理每次请求时,都要根据凭证查询用户信息,访问频率高