👨?🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:Redis:原理速成+项目实战——Redis实战10(Redis消息队列实现异步秒杀)
📚订阅专栏:Redis:原理速成+项目实战
希望文章对你们有所帮助
之前一直在用Redis做缓存,并利用Redis的性能、利用Redis的一些数据结构去不断的做业务的优化,耗时很长,每次功能做完总得啃一点源码,梳理一下知识,并查缺补漏,还是挺有收获的。
其实走到现在,不光是Redis的很多操作都会了,中途也为了这个项目学了很多东西,例如docker,mybatis-plus,nginx的负载均衡、反向代理等等,但没去做总结。
现在要用Redis继续给项目增加功能了。
探店笔记类似于网站评价,图文结合,对应2张表:
tb_blog:探店笔记表,包含笔记的标题、文字、图片等
tb_blog_comments:其他用户对探店笔记的评价
点击首页的“+”按钮,即可进入该页面:
发布探店笔记的业务主要分为两部分,第一部分是上传图片,第二部分是发布,应该将这两个步骤分离,因为上传照片不光是这里有,在其他地方也会用到,需要写到特定controller层中。
上传图片成功以后,应当返还这个照片的地址,作为表单的参数,点击发布的时候提交到后台。
UploadController,图片上传后保存到前端服务器中:
@PostMapping("blog")
public Result uploadImage(@RequestParam("file") MultipartFile image) {
try {
// 获取原始文件名称
String originalFilename = image.getOriginalFilename();
// 生成新文件名
String fileName = createNewFileName(originalFilename);
// 保存文件到nginx目录下,IMAGE_UPLOAD_DIR = "D:\\nginx-1.18.0\\html\\hmdp\\imgs"
image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));
// 返回结果
log.debug("文件上传成功,{}", fileName);
return Result.ok(fileName);
} catch (IOException e) {
throw new RuntimeException("文件上传失败", e);
}
}
BlogController接口:
@Resource
private IBlogService blogService;
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
//标题、图片、内容、店铺id都已经在前端提交了,所以只需要保存用户id
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
blogService.save(blog);
// 返回id
return Result.ok(blog.getId());
}
启动服务,上传照片,可以直接看到上传的地址:
点击上传,就会跳转回主页。
需求:点击首页中的探店笔记,会进入到详情页,需要实现该页面的查询接口。
请求路径定义为/blog/{id},利用GET请求,获取博客信息以及用户用户信息返还到前端,业务的流程我全部交给了service层(下面代码也包括了查看多个笔记的分页查询):
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;
@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(this::queryBlogUser);
return Result.ok(records);
}
@Override
public Result queryBlogById(Long id) {
//查看blog
Blog blog = getById(id);
if(blog == null){
return Result.fail("笔记不存在!");
}
//查询blog有关的用户
queryBlogUser(blog);
return Result.ok(blog);
}
private void queryBlogUser(Blog blog) {
//查询用户直接封装成通用方法
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}
当我们在首页的笔记或者笔记详情页点击点赞,将会带上这篇笔记的id,发一个请求,实现点赞功能,同时我们需要满足一个用户,对于一个笔记只能进行一次点赞,因此我们不可以每次发起请求就直接操作数据库,这之间应当加上一些处理。
需求:
1、一个用户只能点赞一次,再次点击则要取消点赞
2、若当前用户已经点赞,则点赞按钮高亮显示(由前端来判断Blog类的isLike属性)
实现:
1、给Blog类增加一个isLike字段,标示是否被该用户点赞
Blog实体类:
@TableField(exist = false)
private Boolean isLike;
2、修改点赞功能,利用Redis的set集合判断是够点赞过,未点赞过的则点赞数+1,否则点赞数-1。
根据业务需求可以看出,这里用Redis的set结构是很适合的,key为笔记的id,并记录点赞过的所有用户(集合、唯一性),修改BlogServiceImpl:
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result likeBlog(Long id) {
//获取当前用户
Long userId = UserHolder.getUser().getId();
//判读当前登录用户是否已经点赞
String key = "blog:liked:" + id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
if(BooleanUtil.isFalse(isMember)) {//包装类,不要直接判断,防止拆箱操作
//未点赞,可以点赞
//数据库点赞数+1
boolean isSuccess = update().setSql("liked = liked + 1")
.eq("id", id)
.update();
//保存到Redis的set集合
if(isSuccess){
stringRedisTemplate.opsForSet().add(key, userId.toString());
}
}else{
//已点赞,取消点赞
//数据库点赞数-1
boolean isSuccess = update().setSql("liked = liked - 1")
.eq("id", id)
.update();
//把用户从Redis的set集合中移除
if(isSuccess){
stringRedisTemplate.opsForSet().remove(key, userId.toString());
}
}
return Result.ok();
}
3、根据id查询Blog的业务(笔记详情页),判断当前登录用户是否点赞过,赋值给isLike字段(只需要给isLike字段赋值即可)
4、修改分页查询Blog业务(首页笔记列表),判断当前登录用户是否点赞过,赋值给isLike字段(只需修改forEach内的逻辑即可)
上面的isBlogLiked标示对isLike字段进行赋值,进行封装:
private void isBlogLiked(Blog blog) {
//获取当前用户
Long userId = UserHolder.getUser().getId();
//判读当前登录用户是否已经点赞
String key = "blog:liked:" + blog.getId();
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
blog.setIsLike(BooleanUtil.isTrue(isMember));
}
测试点赞功能,成功高亮显示,Redis也新增了数据:
再次点击即可取消点赞,再次点赞就会取消。
在探店笔记详情页,应该要将给笔记点赞的人显示出来,而且只需要显示出点赞的前几个人,点赞的排行榜是按时间来进行排序的。
分析一下这个需求,我们需要用GET请求去根据id进行查询,并且返还List<UserDTO>。
由于我们要进行排序,因此set不适合了,可以比较一下下面3个数据结构:
List | Set | SortedSet | |
---|---|---|---|
排序方式 | 按添加顺序排序 | 无法排序 | 根据score值排序 |
唯一性 | 不唯一 | 唯一 | 唯一 |
查找方式 | 按索引查找,或首尾查找 | 根据元素查找 | 根据元素查找 |
因此,SortedSet是最符合业务需求的,查找的效率也更高。
所以我们需要修改之前的点赞业务,在此之前我们需要熟悉一下SortedSet的操作:
增加元素:ZADD
查找元素:用ZSCORE,元素存在即可返回分数,否则返回空
另外一个比较重要的点,我们除了要保证存入SortedSet后取出来的用户Id的顺序是按时间顺序正确排列的,还要保证查询数据库后返还到前端也得是正确的。
但是如果有2个用户点赞,id=5的用户先查询,id=1的用户后查询,简单使用数据库查询结果顺序就会反了,因为SQL代码为:
select id,phone,password,...
from tb_user
where id in(5, 1)
这样的话底层会先查询id为1的用户
应当将SQL改为:
select id,phone,password,...
from tb_user
where id in(5, 1)
order by field(id, 5, 1)
所以查询的逻辑需要改造(当然查询完以后把数组倒转一下也是阔以滴)。
所有代码如下:
BlogController:
@RestController
@RequestMapping("/blog")
public class BlogController {
@Resource
private IBlogService blogService;
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
//标题、图片、内容、店铺id都已经在前端提交了,所以只需要保存用户id
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
blogService.save(blog);
// 返回id
return Result.ok(blog.getId());
}
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
return blogService.likeBlog(id);
}
@GetMapping("/of/me")
public Result queryMyBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
// 根据用户查询
Page<Blog> page = blogService.query()
.eq("user_id", user.getId()).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
return Result.ok(records);
}
@GetMapping("/hot")
public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {
return blogService.queryHotBlog(current);
}
@GetMapping("/{id}")
public Result queryBlogById(@PathVariable("id") Long id){
return blogService.queryBlogById(id);
}
@GetMapping("/likes/{id}")
public Result queryBlogByLikes(@PathVariable("id") Long id){
return blogService.queryBlogByLikes(id);
}
}
BlogServiceImpl:
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(blog -> {
this.queryBlogUser(blog);
this.isBlogLiked(blog);
});
return Result.ok(records);
}
@Override
public Result queryBlogById(Long id) {
//查看blog
Blog blog = getById(id);
if(blog == null){
return Result.fail("笔记不存在!");
}
//查询blog有关的用户
queryBlogUser(blog);
//查询blog是否被点赞过
isBlogLiked(blog);
return Result.ok(blog);
}
private void isBlogLiked(Blog blog) {
//获取当前用户
UserDTO user = UserHolder.getUser();
if(user == null){
//若用户未登录,无须查询其是否点赞
return;
}
Long userId = user.getId();
//判读当前登录用户是否已经点赞BLOG_LIKED_KEY = "blog:liked:"
String key = BLOG_LIKED_KEY + blog.getId();
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
blog.setIsLike(score != null);
}
@Override
public Result likeBlog(Long id) {
//获取当前用户Id
Long userId = UserHolder.getUser().getId();
//判读当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + id;
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
if(score == null) {
//未点赞,可以点赞
//数据库点赞数+1
boolean isSuccess = update().setSql("liked = liked + 1")
.eq("id", id)
.update();
//保存到Redis的SortedSet集合,score为时间戳
if(isSuccess){
stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
}
}else{
//已点赞,取消点赞
//数据库点赞数-1
boolean isSuccess = update().setSql("liked = liked - 1")
.eq("id", id)
.update();
//把用户从Redis的SortedSet集合中移除
if(isSuccess){
stringRedisTemplate.opsForZSet().remove(key, userId.toString());
}
}
return Result.ok();
}
@Override
public Result queryBlogByLikes(Long id) {
//查询点赞用户取出前五名
String key = BLOG_LIKED_KEY + id;
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if(top5 == null || top5.isEmpty()){
return Result.ok(Collections.emptyList());
}
//解析其中的用户Id
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
//根据Id查询用户
//拼接一下字符串,表示ORDER BY的顺序
String idStr = StrUtil.join(",", ids);
List<UserDTO> userDTOS = userService.query().in("id", ids).
last("ORDER BY FIELD(id," + idStr +")")//last表示在背后拼接
.list()
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(userDTOS);
}
private void queryBlogUser(Blog blog) {
//查询用户直接封装成通用方法
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
}