Redis 基础 - 点赞/点赞排行榜

点赞

需求
  • 同一个用户只能点赞一次,再次点击则取消点赞
  • 如果当前用户已经点赞,则点赞按钮高亮显示(判断字段Blog类的isLike是true或false)
实现步骤
  • 给Blog类添加isLike属性,表示是否被当前用户点赞(这个问题是难点所在,也和需求1一样的需求)
  • 用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1。
  • 修改根据id查询blog的业务(以及分页查询blog的业务),判断当前登陆用户是否点赞过,赋值给isLike字段。

判断用户有没有点过赞,实现方案非常多,比如数据库里建一张表,记录blogId和给这个blog点赞的userId,机每点赞,这张表里就会记录一次,下次再来就判断下是否存在记录。但用数据库实现,太重了,首先数据库的性能不太好,而且这种点赞的判断可能会比较多,所以对数据库的压力就会比较大。可以使用轻量级的办法,比如redis。

那如果要判断用户有没有点赞过,其实就是记录一下当前blog被谁点赞过,所以可以在Redis里以blog的id为key,用集合去记录给这个blog点过赞的所有用户id。下次再来时,判断用户id在集合中是否存在。由于一个用户只能点赞一次,所以这个集合中用户id是不能重复,所以可以用set集合。

Blog.java

...
// 由于数据库中没这个字段,所以加 TableField(exist = false)注解
@TableField(exist = false)
private Boolean isLike; // 当前用户是否点赞过
...

BlogController.java

...
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
	return blogService.likeBlog(id);// 点赞功能,参数id是blog的id
}
...

IBlogService.java

public interface IBlogService extends IService<Blog> {
	...
	Result likeBlog(Long id);
	...
}

BlogServiceImpl.java

public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
	...
	@Resource
	private StringRedisTemplate stringRedisTemplate;
	...
	@Override
	public Result likeBlog(Long id) {
		// 1,获取登陆用户
		Long userId = UserHolder.getUser().getId();

		// 2,判断当前登陆用户是否已经点过赞
		String key = "blog:liked:" + id;// 文章id

		Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());

		// 由于isMember是包装类,所以直接判断可能为空,可以用hutool的工具类(BooleanUtil)判断
		if (BooleanUtil.isFalse(isMember)) {
			// 3,如果没点过赞,可以点赞
			// 3.1 数据库点赞数+1
			boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();

			// 3.2 保存用户id到Redis的set集合
			if (isSuccess) {
				stringRedisTemplate.opsForSet().add(key, userId.toString());
			}
		}else {
			// 4,如果已经点赞,取消点赞
			// 4.1 数据库点赞数-1
			boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();

			// 4.2 把用户id从Redis的set集合里移除
			if (isSuccess) {
				stringRedisTemplate.opsForSet().remove(key, userId.toString());
			}
		}
		return Result.ok();
	}
	...

	// 查询blog的函数也要修改
	@Override
	public Result queryBlogById(Long id) {
		// 1,查询blog
		Blog blog = getById(id);
		if (blog == null) return Result.fail("文章不存在");
		// 2,查询blog有关用户
		queryBlogUser(blog);

		// 【添加的部分】3,查询blog是否被点赞
		isBlogLiked(blog);// 因为List列表页里也要用到,所以封装到一个函数里
	}

	// 判断blog是否被当前用户点赞
	private void isBlogLiked(Blog blog) {
		// 1,获取登陆用户
		Long userId = UserHolder.getUser().getId();

		// 2,判断当前登陆用户是否已经点过赞
		String key = "blog:liked:" + blog.getId();// 文章id

		Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());

		// 由于isMember是包装类,所以直接判断可能为空,可以用hutool的工具类(BooleanUtil)判断
		blog.setIsLike(BooleanUtil.isTrue(isMember));// 给blog的相关字段赋值,如果是true,说明点过赞,前端可以根据这个字段,把点赞图标改为高亮
	}
}

点赞排行榜

需求

在文章详情页面,按照点赞时间先后排序,返回TOP5的用户。要排序、唯一,可以使用SortedSet

List Set SortedSet
排序方式 按添加顺序排序 无法排序 根据score值(比如可以用时间戳)排序
唯一性 不唯一 唯一 唯一
查找方式 按索引查找或首尾查找(即想查找元素就得遍历一遍) 根据元素查找 根据元素查找
修改代码

Blog.java

...
// 由于数据库中没这个字段,所以加 TableField(exist = false)注解
@TableField(exist = false)
private Boolean isLike; // 当前用户是否点赞过
...

BlogController.java

...
@PutMapping("/like/{id}")
public Result likeBlog(@PathVariable("id") Long id) {
	return blogService.likeBlog(id);// 点赞功能,参数id是blog的id
}

// 查询点赞的人
@GetMapping("/likes/{id}")
public Result queryBlogLikes(@PathVariable("id") Long id) {
	return blogService.queryBlogLikes(id);// 点赞功能,参数id是blog的id
}
...

IBlogService.java

public interface IBlogService extends IService<Blog> {
	...
	Result likeBlog(Long id);
	Result queryBlogLikes(Long id);
	...
}

BlogServiceImpl.java

public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
	...
	@Resource
	private StringRedisTemplate stringRedisTemplate;
	...

	@Override
	public Result likeBlog(Long id) {
		// 1,获取登陆用户
		Long userId = UserHolder.getUser().getId();

		// 2,判断当前登陆用户是否已经点过赞
		String key = "blog:liked:" + id;// 文章id

		// 查当前用户的score
		/*
		由于SortedSet里没有ismember命令,所以可以用zscore来返回元素的score值来判断有没有值。
		由于她天然的做从小到大的排序,所以可以用zrange命令查前五个(zrange key 0 4)。
		*/
		Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
		if (score == null) {
			// 3,如果没点过赞,可以点赞
			// 3.1 数据库点赞数+1
			boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();

			// 3.2 保存用户id到Redis的set集合
			if (isSuccess) {
				// sorted set是opsForZSet(),还需要第三个score参数(这里用时间戳就行,因为会以时间戳排序)
				// 相当于命令:zadd key value score
				stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
			}
		}else {
			// 4,如果已经点赞,取消点赞
			// 4.1 数据库点赞数-1
			boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();

			// 4.2 把用户id从Redis的set集合里移除
			if (isSuccess) {
				stringRedisTemplate.opsForZSet().remove(key, userId.toString());
			}
		}
		return Result.ok();
	}
	...

	// 查询blog的函数也要修改
	@Override
	public Result queryBlogById(Long id) {
		// 1,查询blog
		Blog blog = getById(id);
		if (blog == null) return Result.fail("文章不存在");
		// 2,查询blog有关用户
		queryBlogUser(blog);

		// 【添加的部分】3,查询blog是否被点赞
		isBlogLiked(blog);// 因为List列表页里也要用到,所以封装到一个函数里
	}

	// 判断blog是否被当前用户点赞
	private void isBlogLiked(Blog blog) {
		// 1,获取登陆用户(当然,这里需要判断用户是否已登陆,这里假设已登录)
		Long userId = UserHolder.getUser().getId();

		// 2,判断当前登陆用户是否已经点过赞
		String key = "blog:liked:" + blog.getId();// 文章id

		Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());

		// score不等于null,就说明该用户点赞了
		blog.setIsLike(score != null);// 给blog的相关字段赋值,如果是true,说明点过赞,前端可以根据这个字段,把点赞图标改为高亮
	}

	@Override
	public Result queryBlogLikes(Long id) {
		String key = "blog:liked:" + id;// 文章id
		// 1,查询top的点赞用户 zrange key 0 4
		Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
		if (top5 == null || top5.isEmpty()) {
			return Result.ok(Collections.emptyList());// 给个空集合
		}
		// 2,解析其中的用户id,把top5中的id集合变成long后存到list中
		List<Long> ids = top5.stream().map((Long::valueOf).collect(Collectors.toList()));
		// 3,根据用户id查询用户
		/*
		List users = userService.listByIds(ids);
		这边使用mp的listByIds方法时,由于数据库的问题,你给的id顺序和查出来的id顺序不一致。
		因为数据库会根据id排序,而不是in里的id顺序。
		所以就算Redis中根据顺序查出来了用户id,但如果在SQL语句使用in语法
		来查用户list的话,数据库查询并存到list的顺序可能和Redis里的顺序不一致。比如SQL语句如下:
		select id,phone from user where in id(2,1),这里的“2,1”是Redis中的用户id顺序,所以结果
		本应该是先查出2的用户,然后再查出1的用户,但执行后可能会是先查出1的用户之后2的用户,
		这样在前端显示时顺序可能变反,所以后面加上order by FIELD(id, 2, 1)强制指定顺序。
		*/

		String idStr = StrUtil.join(",", ids);// 拼接成字符串
		// in("id", ids)等于in id(2,1),last是手动在sql语句后面拼接sql语句。
		List<User> users = userService.query().in("id", ids).last("order by FIELD(id, "+ids+")").list();

		// 4,返回
		return Result.ok(users);
	}
}

其他参考

Redis基础 - 基本类型及常用命令
Redis基础 - Java客户端
Redis 基础 - 短信验证码登录
Redis 基础 - 用Redis查询商户信息
Redis 基础 - 优惠券秒杀《非集群》
Redis 基础 - 优惠券秒杀《分布式锁(初级)》
Redis 基础 - 优惠券秒杀《分布式锁(使用Redisson)》
Redis 基础 - 优惠券秒杀《初步优化(异步秒杀)》
Redis 基础 - 优惠券秒杀《基于Redis消息队列实现》

你可能感兴趣的:(Redis,Redis基础,Redis点赞,Redis点赞排行,Redis,sort)