【 Redis | 实战篇 扩展 】

目录

前言:

1.达人探店功能

1.1.点赞功能

1.2.排行榜

2.关注

2.1.共同关注

2.2.关注推送

3.附近商铺

3.1.GEO基本用法

3.2.获取附近商铺

4.签到

4.1.BitMap

4.2.实现签到

4.3.统计连续签到

5.UV统计


前言:

实现达人探店,好友关注,附件商铺,用户签到,UV统计

1.达人探店功能

1.1.点赞功能

分析:实现点赞功能,那么一个用户是不是只能点赞一次,如果能进行多次点赞(这不刷起来了吗)

思路:一个用户只能点赞一次,点亮了,赞图标就高亮显示,再次点亮就是取消点赞

实现方案:

  • 数据库:数据库中创建一张表,每次用户点赞时,先访问数据库,数据库有对应数据,代表已经点赞,因此实现取消点赞功能(删除数据库对应数据),没有数据,则代表没有点赞,添加对应数据到数据库中
  • Redis:一人一赞,那么我们使用set集合(不可重复性),key为发笔记人的id,value为点赞用户id
  • 同时采用数据库和Redis:Redis做缓存实现快速查询,数据库做数据持久化

**优化:**由于前端需要后端返回一个布尔值,来判断是否点赞(是否显示高亮),而老师采用的是每次查询操作是先查询数据再判断是否点赞,但是由于我们笔记实体类中已经有了这个属性(布尔),我们可以直接在调用点赞业务代码时(你都在这设置了是否点赞了,那你直接将值赋值给属性即可)

**问题:**如果我们使用老师的方案,在每次查询业务时都需要查询数据库,根据登录用户id查,如果用户没有登录呢?你的查询操作就进行不下去了(要判断id是否为空),没有登录的用户连界面都访问不了

java 复制代码
    @Override
    public Result likeBlog(Long id) {
        //设置Redis的key
        String key = RedisConstants.BLOG_LIKED_KEY + id;
        //查询Redis
        Long userId = UserHolder.getUser().getId();
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
        //判断是否存在
        if (score != null) {
            //Redis中已经存在
            //更新数据库
            boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
            if (isSuccess) {
                //数据库更新成功后,才能删除Redis
                stringRedisTemplate.opsForZSet().remove(key, userId.toString());
            }
        } else {
            //不存在
            //更新数据库
            boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
            if (isSuccess) {
                //数据库更新成功后,才能更新Redis
                stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
            }
        }
        Blog blog = getById(id);
        blog.setIsLike(score == null);
        return Result.ok();
    }

1.2.排行榜

**分析:**由于我们是要对点赞时间用户的排序(统计前五名先点赞的用户),因为是点赞用户肯定不止一个,因此我们需要使用集合的方式,并且是一个查询操作,可以采取Redis

集合分析:

set集合:由于我们是要统计排名,需要对数据进行排序,而set集合是无序的(不能使用


list集合:一个用户只能点赞一次,那么就代表数据不能重复对吧,list集合是按照添加顺序来排序,但是它的原理是基于链表(可以重复),并且由于是链表代表它的查询速度慢(索引查询,首尾查),如果有大量数据需要查询,等待时间太长了


sortedset集合:该集合不允许重复(保证唯一性),并且它可以根据你给定的score(分数)进行排序(默认从小到大)

**思路:**使用sortedset集合,我们需要统计前五名点赞的用户,是不是就是统计先点赞的用户,那么在用户点赞时会调用点赞业务,我们在业务中将原先的使用的存入Redis采用的set集合代码转变成sortedset集合代码即可(我们的score可以设置为时间戳,直接根据时间戳来排序)

查询排行榜:

Redis查询数据

==》判断数据是否存在

==》不存在(没人点赞),直接返回空


==》存在

==》将数据过滤,留下用户id

==》根据用户id查询数据库

==》将数据再次转变成前端要的数据

==》返回数据

java 复制代码
 @Override
    public Result queryLikes(Long id) {
        //查询Redis
        String key = RedisConstants.BLOG_LIKED_KEY + id;
        Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
        //判断是否存在
        if (top5 == null || top5.isEmpty()){
            return Result.ok();
        }
        //转换格式
        List<Long> ids = top5.stream().map(Long::valueOf).toList();
        //根据ids查询
        List<UserDTO> userDTOS = userService
                .listByIds(ids)
                .stream()
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                .toList();
        String join = StrUtil.join(",", ids);

        return Result.ok(userDTOS);
    }

**问题:**虽然我们从Redis中取出的数据是按照时间顺序排序的,但是由于我们使用的是mp的list查询方式,它会默认根据id的顺序查询(从小到大),并不会按照我们指定的顺序查询,最终我们查询出来的数据依然是排序错误(数据库原理:in()查询默认使用id正序查询)

**解决:**不使用这个查询方式,自己写sql语句即可

java 复制代码
    @Override
    public Result queryLikes(Long id) {
        //查询Redis
        String key = RedisConstants.BLOG_LIKED_KEY + id;
        Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
        //判断是否存在
        if (top5 == null || top5.isEmpty()){
            return Result.ok();
        }
        //转换格式
        List<Long> ids = top5.stream().map(Long::valueOf).toList();
        //根据ids查询
        String join = StrUtil.join(",", ids);
        List<UserDTO> userDTOS = userService
                .query()
                .in("id", ids)
                .last("ORDER BY FIELD(id," + join + ")").list()
                .stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).toList();

        return Result.ok(userDTOS);
    }

**解释:**因为我们设置score时设置的是每次点赞的时间戳,因此我们可以直接根据时间戳来查询排行榜(默认正序),所以我们只需要根据索引查询5个就行(索引默认从0开始:0到4即可)

2.关注

2.1.共同关注

分析:共同关注代表的是不是就是两个人关注的交集,并且我们依然是查询判断操作,使用Redis,使用哪个数据结构呢?求交集,使用set集合

java 复制代码
    @Override
    public Result followCommon(Long followId) {
        //1.获取用户id
        Long userId = UserHolder.getUser().getId();
        //2.设置key
        String key1 = "follows:" + userId;
        String key2 = "follows:" + followId;

        //3.Redis中查询交集
        Set<String> set = stringRedisTemplate.opsForSet().intersect(key1, key2);
        //4.转换格式
        if (set == null || set.isEmpty()){
            return Result.ok(Collections.emptyList());
        }
        List<Long> ids = set.stream().map(Long::valueOf).toList();
        //5.查询数据库
        List<UserDTO> userDTOS = userService.listByIds(ids)
                .stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).toList();

        return Result.ok(userDTOS);
    }

2.2.关注推送

**介绍:**关注推送也叫Feed流(投喂),通过无限下拉刷新数据,获取信息(刷视频)

模式:

  • 传统模式:用户需要自己去寻找信息,自己鉴别这个信息是不是自己想要的(用户寻找内容)
  • Feed流:自动根据用户的行为来匹配数据,直接推送你想要的数据(大数据)(刷视频根据你的停留时间来给你推送该类型的视频)(内容匹配用户)

Feed流的模式:

Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
优点:信息全面,不会有缺失。并且实现也相对简单
缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低

智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
缺点:如果算法不精准,可能起到反作用

总结:由于我们是个人空间的笔记展示,使用第一种即可

Feed流的实现方案:

  • 拉模式(读扩散)
  • 推模式(写扩散)
  • 推拉结合

拉模式:

介绍:只有当用户读这篇笔记时才会加载(发件箱--收件箱)

例子:每个发信的用户(作者)都有对应的发件箱(存储笔记的空间),当张三发布了一篇笔记,李四发布了两篇笔记,他们各自的笔记会发送到自己的发件箱中(并且按照时间戳排序(互通,共享)从小到大不会重复),而王五关注了张三,但是王五平时不会访问张三,所以王五的收件箱是空的,只有当王五去访问这篇笔记时(收件箱才会加载),才会拉取张三的消息(笔记)(一个个拉),拉取完成后会进行时间排序(从小到大)

  • 优点:节省内存空间(因为一般是空的)
  • 缺点:每次都需要重新拉取并且进行排序(耗时长,延迟长)

总结:拉模式(只有当你读的时候才会加载)(读扩散)

拉模式:

介绍:用户发了消息直接就传递到收件箱中(写扩散)

例子:没有发件箱了,只有收件箱,当张三发送消息,会直接将消息推送到他的所以粉丝的收件箱中,然后进行排序

  • 优点:延迟低
  • 缺点:有多少粉丝就需要发送多少份(内存占用高)

推拉结合:

介绍:读写混合

例子:有两个作者,一个作者张三是一个拥有千万粉丝的大V博主,另一个作者李四是一个粉丝量少的普通博主,当李四发消息时,采用推模式(粉丝少,直接发),大V发消息时,根据粉丝的活跃度做处理,由于活跃的粉丝相较于是少的,那么活跃的粉丝采用推模式,普通粉丝采用拉模式(不经常访问)

  • 优点:节省内存空间,延迟低
  • 缺点:实现复杂

分析:由于我们用户少,所以采用推模式,我们要展示的是笔记,肯定是先展示最新发布的笔记,然后依次展示,所以我们需要进行根据发布时间进行排序,我们还是进行查找操作,依然选择Redis来进行缓存,使用哪个集合呢?感觉list和sortedset都可以

**思考:**我们使用的是Feed流,我们的数据是不是在不断更新,因此索引也一直在变化(首尾一样),而List不支持流动处理,因此排除,并且由于我们数据不断变化的,我们能才用线性分页查询吗?

**例子:**如果采用线性查询,每次查询五条数据,第一次查询完后,更新了两条数据(按时间发布排序,他们会挤掉先前的第一第二),所以当我们第二次查询时会查询到重复的数据

实现:使用滚动分页查询,每次记录查询的最后一个数据的score值即可,下次就从这查询(要保证有序)

**问题:**那么第一次查询呢?我们没有记录数据啊

解决:由于我们是按从大到小排序,我们直接使用一个无限大的值即可,并且我们不能使用sortedset的按排名查(排名会变),只能使用范围查询(score值不变)

**细节:**如果我们记录查询的最后一个数据的score值时,它有重复值(多个),那么你怎么知道你是从哪个查询,因此我们需要记录最后一个值的重复值有几个(跳过即可)

java 复制代码
@Override
    public Result queryBlogFollow(Long max, Integer offset) {
        //1.获取用户id
        Long userId = UserHolder.getUser().getId();
        //2.根据id获取Redis中的数据
        String key = "feed:" + userId;
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0F, max, offset, 3);
        //3.判断是否为空
        if (typedTuples == null || typedTuples.isEmpty()){
            return Result.ok();
        }
        //4.获取查询博客的id
        List<Long> ids = new ArrayList<>(typedTuples.size());
        //5.设置时间初始值
        int os = 1;
        long minTime = 0;
        for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
            ids.add(Long.valueOf(typedTuple.getValue()));
            long time = typedTuple.getScore().longValue();
            if(time == minTime){
                os++;
            }else {
                minTime = time;
                os = 1;
            }
        }
        //6.查询数据库
        String join = StrUtil.join(",", ids);
        List<Blog> blogs = query()
                .in("id", ids)
                .last("ORDER BY FIELD(id," + join + ")").list();
        BlogData blogData = new BlogData();
        blogData.setList(blogs);
        blogData.setOffset(os);
        blogData.setMinTime(minTime);
        return Result.ok(blogData);
    }

3.附近商铺

3.1.GEO基本用法

GEO就是Geolocation的简写形式,代表地理坐标。Redis在3.2版本中加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。

常见的命令有:

  • GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)
  • GEODIST:计算指定的两个点之间的距离并返回
  • GEOHASH:将指定member的坐标转为hash字符串形式并返回
  • GEOPOS:返回指定member的坐标
  • GEORADIUS:指定圆心、半径,找到该圆内包含的所有member,并按照与圆心之间的距离排序后返回。6.2以后已废弃
  • GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能
  • GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key

3.2.获取附近商铺

分析:还是查询,使用Redis

注意:由于我们用户可以选择不同的分类来进行查询店铺,不一定就是距离查询,因此我们需要判断

java 复制代码
 @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //1.判断是否是距离查询
        if (x == null || y == null) {
            // 根据类型分页查询
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page.getRecords());
        }
        //2.是距离查询
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

        //3.查询Redis,按照距离排序,分页
        String key = RedisConstants.SHOP_GEO_KEY + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(key,
                GeoReference.fromCoordinate(x, y),
                new Distance(5000),
                RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));
        //4.解析出id
        if (results == null) {
            return Result.ok(Collections.emptyList());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        //4.1截取from - end
        if (list.size() <= from) {
            return Result.ok(Collections.emptyList());
        }
        List<Long> ids = new ArrayList<>(list.size());
        Map<String,Distance> distanceMap = new HashMap<>(list.size());
        list.stream().skip(from).forEach(result -> {
            //4.2.获取店铺id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            //4.3.获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr,distance);
        });
        //5.根据id查询店铺信息
        String idsStr = StrUtil.join(",", ids);
        List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idsStr + ")").list();
        for (Shop shop : shops) {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        return Result.ok(shops);
    }

4.签到

4.1.BitMap

分析:如果我们使用数据库将每天用户签到的数据一一记录下来,那么我们将要存储大量数据,成本大,因此我们需要使用一个简单的方式来实现签到记录数据的实现

**简单方法:**签到只有两个选择:签到还是没有签到,我们二进制是不是只有0和1,那么我们可以使用1代表签到的,0代表未签到的,怎么统计每一月,每一年的数据呢,使用Redis key为时间前缀,value为签到值

实现:一般是统计一个月的签到情况,因此我们key前缀具体到月,value的二进制位数为32位即可,因为类型简单,使用Redis的String类型(BitMap)即可

BitMap用法

介绍:Redis中是利用string类型数据结构实现BitMap,因此最大上限是512M,转换为bit则是2A32个bit位。

BitMap的操作命令有:

  • SETBIT:向指定位置(offset)存入一个0或1
  • GETBIT:获取指定位置(offset)的bit值
  • BITCOUNT:统计BitMap中值为1的bit位的数量
  • BITFIELD:操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值
  • BITFIELD_RQ:获取BitMap中bit数组,并以十进制形式返回
  • BITOP:将多个BitMap的结果做位运算(与、或、异或)
  • BITPOS:查找bit数组中指定范围内第一个0或1出现的位置

4.2.实现签到

java 复制代码
@Override
    public Result sign() {
        //1.获取用户id
        Long userId = UserHolder.getUser().getId();
        //2.获取当前时间
        LocalDateTime now = LocalDateTime.now();
        //3.设置时间
        String keyStrTime = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = USER_SIGN_KEY + userId + keyStrTime;
        //4.获取天数
        int dayOfMonth = now.getDayOfMonth();
        //5.写入Redis
        stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
        return Result.ok();
    }

4.3.统计连续签到

java 复制代码
@Override
    public Result signCount() {
        //1.获取用户id
        Long userId = UserHolder.getUser().getId();
        //2.获取当前时间
        LocalDateTime now = LocalDateTime.now();
        //3.设置时间
        String keyStrTime = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = USER_SIGN_KEY + userId + keyStrTime;
        //4.获取天数
        int dayOfMonth = now.getDayOfMonth();
        //5.查询Redis,查询本月数据
        List<Long> list = stringRedisTemplate.opsForValue().bitField(
                key,
                BitFieldSubCommands.create()
                        .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));

        if(list == null || list.isEmpty()){
            return Result.ok(0);
        }
        Long aLong = list.get(0);
        if(aLong == null || aLong == 0){
            return Result.ok(0);
        }
        //6.循环遍历
        int count = 0;
        while(true){
            //做与运算
            if((aLong & 1) == 0){
                //跳出循环
                break;
            }else {
                //计数器加一
                count++;
            }
            //向右移动一位
            aLong >>>= 1;
        }
        return Result.ok(count);
    }

5.UV统计

UV: 全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。
PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。

**分析:**UV统计在服务端·实现很困难,因为要判断该用户是否已经统计过了,那么就需要查询数据,因此我们的已经访问的用户信息都需要保存到Redis中,如果用户量大,需要考虑内存

解决:

相关推荐
昭阳~25 分钟前
PostgreSQL日常维护
数据库·postgresql
数据库幼崽33 分钟前
MySQL 8.0 OCP 1Z0-908 171-180题
数据库·mysql·ocp
zyq~38 分钟前
【课堂笔记】核方法和Mercer定理
笔记·机器学习·核方法·mercer定理
wusixuan1310041 小时前
图论学习笔记 3
笔记·学习·图论
FBI HackerHarry浩1 小时前
Linux云计算训练营笔记day13[CentOS 7 find、vim、vimdiff、ping、wget、curl、RPM、YUM]]
linux·运维·笔记·centos·云计算
we199898981 小时前
Spring Boot中的分布式缓存方案
spring boot·分布式·缓存
Мартин.1 小时前
[Meachines] [Hard] Dab Enumerate+memcached+ldconfig-Lib-Hijack特權升級+Tyrant
数据库·缓存·memcached
weixin_446260851 小时前
现代化SQLite的构建之旅——解析开源项目Limbo
数据库·sqlite·开源
nenchoumi31191 小时前
Model 速通系列(一)nanoGPT
笔记·深度学习·学习·语言模型
想你依然心痛1 小时前
Spark大数据分与实践笔记(第五章 HBase分布式数据库-02)
大数据·数据库·分布式·hbase