达人探店
发布探店笔记
改一下,图片保存路径就可以直接运行测试了。
查看探店笔记
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;
@Override
public Result queryBlogById(Long id) {
//1.查询blog
Blog blog = getById(id);
if(blog==null){
return Result.fail("笔记不存在");
}
//2.查询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());
}
@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);
}
}
点赞
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@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);
return Result.ok(blog);
}
private void isBlogLiked(Blog blog) {
//1.判获取登录用户
Long userId = UserHolder.getUser().getId();
//2.判断当前登录用户是否已经点赞
String key=BLOG_LIKED_KEY+blog.getId();
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
blog.setIsLike(BooleanUtil.isTrue(isMember));
}
private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
@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 likeBlog(Long id) {
//1.判获取登录用户
Long userId = UserHolder.getUser().getId();
//2.判断当前登录用户是否已经点赞
String key=BLOG_LIKED_KEY+id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
if(BooleanUtil.isFalse(isMember)) {
//3.如果未点赞
//3.1数据库点赞数+1
boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
//3.2保存用户到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把用户从redis的set集合删除
stringRedisTemplate.opsForSet().remove(key,userId.toString());
}
return Result.ok();
}
}
点赞排行榜
为了将最早点赞的人摆在最前面,需要按照时间先后存储分数,这里使用zset存储时间戳作为zset的score作为排序的依据。
然后针对mysql的排序会乱序的问题需要自定义排序规则,将数据按照指定顺序展示.
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IUserService userService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@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);
return Result.ok(blog);
}
private void isBlogLiked(Blog blog) {
//1.判获取登录用户
UserDTO user = UserHolder.getUser();
if(user==null){
//用户未登录,无需查询是否点赞
return;
}
Long userId = user.getId();
//2.判断当前登录用户是否已经点赞
String key=BLOG_LIKED_KEY+blog.getId();
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
blog.setIsLike(score!=null);
}
private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
@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 likeBlog(Long id) {
//1.判获取登录用户
Long userId = UserHolder.getUser().getId();
//2.判断当前登录用户是否已经点赞
String key=BLOG_LIKED_KEY+id;
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保存用户到Redis的set集合 zadd key value score
if(isSuccess){
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把用户从redis的set集合删除
stringRedisTemplate.opsForZSet().remove(key,userId.toString());
}
return Result.ok();
}
@Override
public Result queryBlogLikes(Long id) {
//1.查询top5的点赞用户 zrange key 0 4
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());
}
//2.解析出用户id
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
//将id拼成字符串
String idStr = StrUtil.join(",", ids);
//3.根绝用户id查询用户 必须自定义排序规则进行查询,否则原本是有序的数据查完之后就变成无序的了
List<UserDTO> userDTOS = userService.query()
.in("id",ids).last("ORDER BY FIELD(id,"+idStr+")")
.list()
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
//4.返回
return Result.ok(userDTOS);
}
}
好友关注
关注和取关
一个接口实现关注和取关
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
/**
* 关注取关功能
* @param followUserId
* @param isFollow
* @return
*/
@Override
public Result follow(Long followUserId, Boolean isFollow) {
//1.获取登录用户
Long userId = UserHolder.getUser().getId();
//1.判断到底是关注还是取关
if (isFollow) {
//2.关注,新增数据
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
save(follow);
}else{
//3.取关,删除 delete from tb_follow where userId = ? and follow_user_id = ?
remove(new QueryWrapper<Follow>()
.eq("user_id",userId)
.eq("follow_user_id",followUserId));
}
return Result.ok();
}
@Override
public Result isFollow(Long followUserId) {
//1.获取登录用户
Long userId = UserHolder.getUser().getId();
//2.查询是否关注 select count(*) from tb_follow where userId = ? and follow_user_id = ?
Integer count = query().eq("user_id", userId)
.eq("follow_user_id", followUserId).count();
//3.判断
return Result.ok(count>0);
}
}
共同关注
这两个接口直接使用资料里的代码片段
// UserController 根据id查询用户
@GetMapping("/{id}")
public Result queryUserById(@PathVariable("id") Long userId){
// 查询详情
User user = userService.getById(userId);
if (user == null) {
return Result.ok();
}
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 返回
return Result.ok(userDTO);
}
// BlogController
@GetMapping("/of/user")
public Result queryBlogByUserId(
@RequestParam(value = "current", defaultValue = "1") Integer current,
@RequestParam("id") Long id) {
// 根据用户查询
Page<Blog> page = blogService.query()
.eq("user_id", id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
return Result.ok(records);
}
为了能用redis的set集合取交集求得共同关注的好友,这里要将一个用户关注的所有人都存储到redis里的set集合,,取关了从set集合里删除。
代码实现
@Override
public Result followCommons(Long id) {
//1.获取当前用户
Long userId = UserHolder.getUser().getId();
String key="follows:"+userId;
//2.求交集
String key2="follows:"+id;
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
if(intersect==null||intersect.isEmpty()){
//无交集
return Result.ok(Collections.emptyList());
}
//3.解析id集合
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
//4.查询用户
List<UserDTO> users = userService
.listByIds(ids)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
return Result.ok(users);
}
关注推送
有点像观察者模式,观察者给监听者集体发送消息。
feed流实现方案分析
拉模式下会将用户关注的人的消息全都拉取到收件箱里面按照时间进行排序。 这个模式下,消息只会在作者的发件箱持久保存一份,用户的收件箱每次打开都会去拉取一份新的,用完就删。每次都读取一堆,性能消耗大。
推模式下会将消息发送到用户的收件箱持久化保存。但是这个模式下一个消息会写n份,内存占用高。
普通v直接用推模式,大v的活跃粉丝用推模式,普通粉丝数量多用拉模式。
基于推模式实现关注推送功能
这里选择推送blog的id到用户的收件箱,用户要看时再现查。
这里使用Redis的Zset进行保存,基于时间戳进行排序,要进行分页查询时利用角标0~n。
这里传统分页好像是有个问题,会出现相同商品重复出现的情况。然后利用滚动分页就不会有这种情况,但是这样话新数据又去哪里了?
使用sortedSet实现的时候,每次分页记住时间戳最小的那个,下次查就从更小的开始,就不会出现数据重复了。
推送代码实现
改造新增blog的代码,在新增成功的时候获取所有粉丝id进行blog推送。
@Override
public Result saveBlog(Blog blog) {
// 1.获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 2.保存探店博文
boolean isSucess = save(blog);
if(!isSucess){
return Result.fail("新增笔记失败");
}
//3.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id=?
List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
//4.推送笔记id给所有粉丝
for(Follow follow:follows){
//4.1获取粉丝id
Long userId = follow.getUserId();
//4.2推送
String key=FEED_KEY+userId;
stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
}
// 3.返回id
return Result.ok(blog.getId());
}
滚动分页查询收件箱思路
使用的是zrange,不能按照角标查询,要按照score进行查询.
所以这里使用按照分数查询的命令. 但是这里也会存在问题,如果存在两个相同score的话,可能也会出现重复,因为命令是按照小于等于。
滚动分页查询实现
这个代码好像还是有点问题,这里查询了blog有关用户和是否点赞却没有用上
@Data
public class ScrollResult {
private List<?> list;
private Long minTime;
private Integer offset;
}
@GetMapping("/of/follow")
public Result queryBlogOfFollow(
@RequestParam("lastId") Long max, @RequestParam(value = "offset",defaultValue = "0") Integer offset){
return blogService.queryBlogOfFollow(max,offset);
}
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
//1.获取当前用户
Long userId = UserHolder.getUser().getId();
//2.查询收件箱 ZREVRANGBYSCORE key MAX MIN WITHSCORES LIMIT offset count
String key=FEED_KEY+userId;
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 2);
//3.非空判断
if(typedTuples==null||typedTuples.isEmpty()){
return Result.ok();
}
//4.解析数据: blogId ,mintime(时间戳),offset
List<Long> ids=new ArrayList<>(typedTuples.size());
long minTime=0;
int os=1;
for (ZSetOperations.TypedTuple<String> tuple : typedTuples) { //
//4.1获取id
ids.add(Long.valueOf(tuple.getValue()));
//4.2获取分数(时间戳)
long time=tuple.getScore().longValue();
if(time==minTime){
os++;
}else{
minTime=time;
os=1;
}
}
//5.根据id查询blog 基于in语句查询会打乱顺序
String idStr = StrUtil.join(",", ids);
List<Blog> blogs =query()
.in("id",ids).last("ORDER BY FIELD(id,"+idStr+")").list();
for (Blog blog : blogs) {
//5.1.查询blog有关的用户
queryBlogUser(blog);
//5.2.查询blog是否被点赞
isBlogLiked(blog);
}
//6.封装并返回
ScrollResult scrollResult = new ScrollResult();
scrollResult.setList(blogs);
scrollResult.setMinTime(minTime);
scrollResult.setOffset(os);
return Result.ok(scrollResult);
}
附近商铺
GEO数据结构的基本用法
导入店铺数据到GEO
写一个数据导入的测试类
@Test
void loadShopDate(){
//1.查询店铺信息
List<Shop> list = shopService.list();
//2.把店铺分组,按照typeId分组,typeId一致的放到一个集合
Map<Long,List<Shop>> map=list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
//3.分批完成写入Redis
for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
//3.1获取类型id
Long typeId = entry.getKey();
String key= SHOP_GEO_KEY+typeId;
//3.2获取同类型的店铺的集合
List<Shop> value = entry.getValue();
List<RedisGeoCommands.GeoLocation<String>> locations=new ArrayList<>();
//3.3写入redis geoadd key 经度 维度 member
for (Shop shop : value) {
//写入一条数据
//stringRedisTemplate.opsForGeo().add(key,new Point(shop.getX(),shop.getY()),shop.getId().toString());
locations.add(new RedisGeoCommands.GeoLocation<>(
shop.getId().toString(),
new Point(shop.getX(),shop.getY())));
}
//一次写入多条数据
stringRedisTemplate.opsForGeo().add(key,locations);
}
}
实现附近商户功能
用户签到
BitMap功能演示
这个表格里的每一行数据就是用户的一条签到记录,现在使用二进制压缩来存储.
实现签到功能
代码实现
@Override
public Result sign() {
//1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
//2.获取日期
LocalDateTime now = LocalDateTime.now();
//3.拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
String key=USER_SIGN_KEY+userId+keySuffix;
//4.获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
//5.写入Redis SETBIT key offset 1
stringRedisTemplate.opsForValue().setBit(key, dayOfMonth-1,true);
return Result.ok();
}
统计连续签到
代码实现
@Override
public Result signCount() {
//1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
//2.获取日期
LocalDateTime now = LocalDateTime.now();
//3.拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
String key=USER_SIGN_KEY+userId+keySuffix;
//4.获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
//5.获取本月截止今天为止的所有的签到记录,要返回的是一个十进制的数字 BITFIELD sign:8:202312 GET u14 0
List<Long> result = stringRedisTemplate.opsForValue().bitField(
key,
BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType
.unsigned(dayOfMonth))
.valueAt(0) //今天多少号就多少个bit位
);
if(result==null||result.isEmpty())
{
//没有任何签到结果
return Result.ok(0);
}
Long num=result.get(0);
if(num==null||num==0){
return Result.ok(0);
}
//6.循环遍历
int count=0;
while(true){
//6.1让这个数字与1做与运算,得到数字的最后一个bit位
//判断这个bit位是否为0
if ((num&1)==0) {
//如果为0,说明未签到,结束
break;
}else{
//如果不为0,说明已签到,计数器+1
count++;
}
//把数字右移一位,抛弃最后一个bit位,继续下一个bit位
num>>>=1;
}
return Result.ok(count);
}
UV统计
HyperLogLog的用法
测试百万数据的统计
@Test
void testHyperLogLog(){
//准备数组,装用户数据
String[] users = new String[1000];
//数据角标记
int index=0;
for(int i=1;i<=1000000;i++){
//赋值
users[index++]="user_"+i;
//每1000条发送一次
if(i%1000==0){
index=0;
stringRedisTemplate.opsForHyperLogLog().add("hl2",users);
}
}
//统计数量
Long hl2 = stringRedisTemplate.opsForHyperLogLog().size("hl2");
System.out.println(hl2);
}
这个测试方法一直运行失败,我真的吐了.