一、达人探店1.点赞功能1问题分析GetMapping(/likes/{id}) public Result queryBlogLikes(PathVariable(id) Long id) { //修改点赞数量 blogService.update().setSql(liked liked 1 ).eq(id,id).update(); return Result.ok(); }当前的点赞功能由于单纯的设计了set liked liked 1这导致用户可以无限点击点赞按钮刷赞这对我们来说肯定是不想看到的合理的业务需求应该是用户点赞点赞数1点赞按钮高亮显示提示用户已经点赞过了再次点击按钮点赞数-1并取消高亮显示提示用户未点赞。2技术选型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集合移除 if(isSuccess){ stringRedisTemplate.opsForSet().remove(key,userId.toString()); } }第一个想法是点赞和取消点赞做两个接口但是事实上这俩功能在一个接口里面。因此我们需要一个存储空间来记录用户点赞状态继而发现表中存在字段isLiked那就想我们是否可以先查字段isLiked再判断点赞数是1还是-1进行update操作update不仅仅要更新点赞数还得更新isLiked字段。方法显然是可行的但是我们最少也得进行两次数据库的操作那可以怎么优化呢。那就可以用到我们的Reids更新操作涉及到数据库变化肯定不能用Redis代替但查询是可以的。我们可以使用Set数据结构Value集合里面的数是唯一的来记录某个优惠卷Key有哪些用户用户ID放在Value集合中。这样查询用户点赞状态我们只需要调用Set的isMember方法判断用户ID是否在Set集合中即可。当然更新完数据库后需要在Redis中同步添入或移除用户ID。2.点赞排行榜1需求分析在探店笔记的详情页面应该把给该笔记点赞的人显示出来比如最早点赞的TOP5形成点赞排行榜。这是一个单纯的查询操作我们就可以想到使用Redis的缓存提速。在实现点赞功能板块我们只是把用户ID存到了Value集合中如果同时想要实现排序功能就得使用ZSet在特定Key中集合中的每个用户ID都有对应的权重score相当于多了个字段我们可以利用其作为排序标准也就是把当前系统时间填入。不幸的是ZSet数据结构并未为我们提供isMember方法判断某个用户ID是否存在于ZSet中但我们可以利用score方法进行查询该用户ID的权重如果为null说明它没有点赞反之点赞了执行各自业务逻辑即可。2代码实现修改原业务逻辑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集合移除 if (isSuccess) { stringRedisTemplate.opsForZSet().remove(key, userId.toString()); } } return Result.ok(); } private void isBlogLiked(Blog blog) { // 1.获取登录用户 UserDTO user UserHolder.getUser(); if (user null) { // 用户未登录无需查询是否点赞 return; } Long userId user.getId(); // 2.判断当前登录用户是否已经点赞 String key blog:liked: blog.getId(); Double score stringRedisTemplate.opsForZSet().score(key, userId.toString()); blog.setIsLike(score ! null); }添加查询最早点赞Top5逻辑Override public Result queryBlogLikes(Long id) { String key BLOG_LIKED_KEY id; // 1.查询top5的点赞用户 zrange key 0 4 SetString top5 stringRedisTemplate.opsForZSet().range(key, 0, 4); if (top5 null || top5.isEmpty()) { return Result.ok(Collections.emptyList()); } // 2.解析出其中的用户id ListLong ids top5.stream().map(Long::valueOf).collect(Collectors.toList()); String idStr StrUtil.join(,, ids); // 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1) ListUserDTO 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); }上述代码值得一提的是虽然我们从Redis中得到的top5集合是有序的但是在执行SQL语句时如果不加ORDER BY FIELD关键词进行排序单纯的用WHERE id IN #{ids}就会导致查询出来的数据是无序的我们先前的努力就白费了因为IN关键词本身就是无序的。二、好友关注1.关注和取消关注1数据库表设计这里的用户id就是登录用户的id关联的用户id就是指当前登录用户关注的其他用户id两个字段是一对一关系一行数据代表一个粉丝对应一个主播。2接口设计该业务要求实现两个功能接口一个就是查询当前用户是否关注该主播的接口另一个是实现关注或取关的接口。// 判断是否关注Service Override public Result isFollow(Long followUserId) { // 1.获取登录用户 Long userId UserHolder.getUser().getId(); // 2.查询是否关注 select count(*) from tb_follow where user_id ? and follow_user_id ? Integer count query().eq(user_id, userId).eq(follow_user_id, followUserId).count(); // 3.判断 return Result.ok(count 0); } // 关注或取关Service Override public Result follow(Long followUserId, Boolean isFollow) { // 1.获取登录用户 Long userId UserHolder.getUser().getId(); String key follows: userId; // 1.判断到底是关注还是取关 if (isFollow) { // 2.关注新增数据 Follow follow new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); boolean isSuccess save(follow); } else { // 3.取关删除 delete from tb_follow where user_id ? and follow_user_id ? remove(new QueryWrapperFollow() .eq(user_id, userId).eq(follow_user_id, followUserId)); } return Result.ok(); }查询用户是否关注其实就是在给定用户ID与关联者ID两个字段的条件下看看表中有没有符合这一条件的一行数据如果有就返回true没有就返回false给前端这就是isFollow逻辑它存在的必要性一是根据返回结果显示关注按钮还是取消关注按钮必要性二就是可以直接把关注状态在调用Follow接口时传给后端。关注和取消关注接口就是根据前端传来的关注状态决定是关注新增数据还是取关删除数据。2.共同关注1需求分析与技术选型我们需要在给定的两个用户ID下得出他们的共同关注有哪些。这种求交集的功能第一时间想到了我们Redis中Set结构求交集并集差集等相关API。为此我们需要对关注和取消关注接口进行一定的修改关注则把关联者ID加入以用户ID为Key的Set集合中取关则从中删除。修改代码如下Override public Result follow(Long followUserId, Boolean isFollow) { // 1.获取登录用户 Long userId UserHolder.getUser().getId(); String key follows: userId; // 1.判断到底是关注还是取关 if (isFollow) { // 2.关注新增数据 Follow follow new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); boolean isSuccess save(follow); if (isSuccess) { // 把关注用户的id放入redis的set集合 sadd userId followerUserId stringRedisTemplate.opsForSet().add(key, followUserId.toString()); } } else { // 3.取关删除 delete from tb_follow where user_id ? and follow_user_id ? boolean isSuccess remove(new QueryWrapperFollow() .eq(user_id, userId).eq(follow_user_id, followUserId)); if (isSuccess) { // 把关注用户的id从Redis集合中移除 stringRedisTemplate.opsForSet().remove(key, followUserId.toString()); } } return Result.ok(); }然后就是我们具体的求交集并查询返回的代码底层是利用Set集合的交集运算SINTER快速计算并展示两个用户的共同关注好友Override public Result followCommons(Long id) { // 1.获取当前用户 Long userId UserHolder.getUser().getId(); String key follows: userId; // 2.求交集 String key2 follows: id; SetString intersect stringRedisTemplate.opsForSet().intersect(key, key2); if (intersect null || intersect.isEmpty()) { // 无交集 return Result.ok(Collections.emptyList()); } // 3.解析id集合 ListLong ids intersect.stream().map(Long::valueOf).collect(Collectors.toList()); // 4.查询用户 ListUserDTO users userService.listByIds(ids) .stream() .map(user - BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); return Result.ok(users); }3.Feed流推送1Feed流引入当我们关注了用户后这个用户发了动态那么我们应该把这些数据推送给用户这个需求其实我们又把他叫做Feed流关注推送也叫做Feed流直译为投喂。为用户持续的提供“沉浸式”的体验通过无限下拉刷新获取新的信息。Feed流产品有两种常见模式Timeline不做内容筛选简单的按照内容发布时间排序常用于好友或关注。例如朋友圈优点信息全面不会有缺失。并且实现也相对简单缺点信息噪音较多用户不一定感兴趣内容获取效率低智能排序利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户优点投喂用户感兴趣信息用户粘度很高容易沉迷缺点如果算法不精准可能起到反作用本例中的个人页面是基于关注的好友来做Feed流只需要拿到我们关注用户的信息然后按照时间排序即可。2技术选型该模式的实现方案有三种拉模式推模式推拉结合拉模式也叫做读扩散该模式的核心含义就是当张三和李四和王五发了消息后都会保存在自己的邮箱中假设赵六要读取信息那么他会从读取他自己的收件箱此时系统会从他关注的人群中把他关注人的信息全部都进行拉取然后在进行排序优点比较节约空间因为赵六在读信息时并没有重复读取而且读取完之后可以把他的收件箱进行清除。缺点比较延迟当用户读取数据时才去关注的人里边去读取数据假设用户关注了大量的用户那么此时就会拉取海量的内容对服务器压力巨大。推模式也叫做写扩散。推模式是没有写邮箱的当张三写了一个内容此时会主动的把张三写的内容发送到他的粉丝收件箱中去假设此时李四再来读取就不用再去临时拉取了优点时效快不用临时拉取缺点内存压力大假设一个大V写信息很多人关注他 就会写很多分数据到粉丝那边去推拉结合模式也叫做读写混合兼具推和拉两种模式的优点。推拉模式是一个折中的方案站在发件人这一端如果是个普通的人那么我们采用写扩散的方式直接把数据写入到他的粉丝中去因为普通的人他的粉丝关注量比较小所以这样做没有压力如果是大V那么他是直接将数据先写入到一份到发件箱里边去然后再直接写一份到活跃粉丝收件箱里边去现在站在收件人这端来看如果是活跃粉丝那么大V和普通的人发的都会直接写入到自己收件箱里边来而如果是普通的粉丝由于他们上线不是很频繁所以等他们上线时再从发件箱里边去拉信息。3需求分析需求修改新增探店笔记的业务在保存blog到数据库的同时推送到粉丝的收件箱收件箱满足可以根据时间戳排序必须用Redis的数据结构实现查询收件箱数据时可以实现分页查询Feed流中的数据会不断更新所以数据的角标也在变化因此不能采用传统的分页模式。原因如下假设在t1 时刻我们去读取第一页此时page 1 size 5 那么我们拿到的就是10~6 这几条记录假设现在t2时候又发布了一条记录此时t3 时刻我们来读取第二页读取第二页传入的参数是page2 size5 那么此时读取到的第二页实际上是从6 开始然后是6~2 那么我们就读取到了重复的数据所以feed流的分页不能采用原始方案来做。4解决方案主播保存博客的时候推送给粉丝Override public Result saveBlog(Blog blog) { // 1.获取登录用户 UserDTO user UserHolder.getUser(); blog.setUserId(user.getId()); // 2.保存探店笔记 boolean isSuccess save(blog); if(!isSuccess){ return Result.fail(新增笔记失败!); } // 3.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id ? ListFollow 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()); } // 5.返回id return Result.ok(blog.getId()); }Feed流的滚动分页我们需要记录每次操作的最后一条然后从这个位置开始去读取数据举个例子我们从t1时刻开始拿第一页数据拿到了10~6然后记录下当前最后一次拿取的记录就是6t2时刻发布了新的记录此时这个11放到最顶上但是不会影响我们之前记录的6此时t3时刻来拿第二页第二页这个时候拿数据还是从6后一点的5去拿就拿到了5-1的记录。我们这个地方可以采用sortedSet来做可以进行范围查询并且还可以记录当前获取数据时间戳最小值就可以实现滚动分页了。当然上述实现还有一个小BUG如果集合里面有多个6呢那即使我们记录下最后一次拿取的是6也不知道从哪个6开始下一页查找。所以这里不仅要记录最后一次拿取的数字还要记录这次查询跟这个数字相同大小的有几个记为offset到时候第二次查需要跳过。第一次查询offset默认给0就行。实现分页查询收邮箱需求在个人主页的“关注”卡片中查询并展示推送的Blog信息具体操作如下1、每次查询完成后我们要分析出查询出数据的最小时间戳这个值会作为下一次查询的条件2、我们需要找到与上一次查询相同的查询个数作为偏移量下次查询时跳过这些查询过的数据拿到我们需要的数据综上我们的请求参数中就需要携带 lastId上一次查询的最小时间戳 和偏移量这两个参数。这两个参数第一次会由前端来指定以后的查询就根据后台结果作为条件再次传递到后台。Override public Result queryBlogOfFollow(Long max, Integer offset) { // 1.获取当前用户 Long userId UserHolder.getUser().getId(); // 2.查询收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count String key FEED_KEY userId; SetZSetOperations.TypedTupleString typedTuples stringRedisTemplate.opsForZSet() .reverseRangeByScoreWithScores(key, 0, max, offset, 2); // 3.非空判断 if (typedTuples null || typedTuples.isEmpty()) { return Result.ok(); } // 4.解析数据blogId、minTime时间戳、offset ListLong ids new ArrayList(typedTuples.size()); long minTime 0; // 2 int os 1; // 2 for (ZSetOperations.TypedTupleString tuple : typedTuples) { // 5 4 4 2 2 // 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; } } os minTime max ? os : os offset; // 5.根据id查询blog String idStr StrUtil.join(,, ids); ListBlog 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 r new ScrollResult(); r.setList(blogs); r.setOffset(os); r.setMinTime(minTime); return Result.ok(r); }三、附近商户1.需求分析具体场景就是点开某一个分类例如美食商户然后有许多筛选标准这里我们聚焦“距离”这个标准实现距离从短到近的商户排序。这个地方就需要使用到我们的GEO向后台传入当前app收集的地址(我们此处是写死的) 以当前坐标作为圆心同时绑定相同的店家类型type以及分页信息把这几个条件传入后台后台查询出对应的数据再返回。2.GEO数据结构介绍GEO就是Geolocation的简写形式代表地理坐标。Redis在3.2版本中加入了对GEO的支持允许存储地理坐标信息帮助我们根据经纬度来检索数据。常见的命令有GEOADD添加一个地理空间信息包含经度longitude、纬度latitude、值memberGEODIST计算指定的两个点之间的距离并返回GEOHASH将指定member的坐标转为hash字符串形式并返回GEOPOS返回指定member的坐标GEORADIUS指定圆心、半径找到该圆内包含的所有member并按照与圆心之间的距离排序后返回。6.以后已废弃GEOSEARCH在指定范围内搜索member并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能GEOSEARCHSTORE与GEOSEARCH功能一致不过可以把结果存储到一个指定的key。 6.2.新功能3.解决方案我们要做的事情是将数据库表中的数据导入到redis中去redis中的GEOGEO在redis中就一个menber和一个经纬度我们把x和y轴传入到redis做的经纬度位置去但我们不能把所有的数据都放入到menber中去毕竟作为redis是一个内存级数据库如果存海量数据redis还是力不从心所以我们在这个地方存储他的id即可。但是这个时候还有一个问题就是在redis中并没有存储type所以我们无法根据type来对数据进行筛选所以我们可以按照商户类型做分组类型相同的商户作为同一组以typeId为key存入同一个GEO集合中即可。载入商户代码如下Test void loadShopData() { // 1.查询店铺信息 ListShop list shopService.list(); // 2.把店铺分组按照typeId分组typeId一致的放到一个集合 MapLong, ListShop map list.stream().collect(Collectors.groupingBy(Shop::getTypeId)); // 3.分批完成写入Redis for (Map.EntryLong, ListShop entry : map.entrySet()) { // 3.1.获取类型id Long typeId entry.getKey(); String key SHOP_GEO_KEY typeId; // 3.2.获取同类型的店铺的集合 ListShop value entry.getValue(); ListRedisGeoCommands.GeoLocationString locations new ArrayList(value.size()); // 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); } }实现附近商户功能代码如下Override public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) { // 1.判断是否需要根据坐标查询 if (x null || y null) { // 不需要坐标查询按数据库查询 PageShop 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、按照距离排序、分页。结果shopId、distance String key SHOP_GEO_KEY typeId; GeoResultsRedisGeoCommands.GeoLocationString results stringRedisTemplate.opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE .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()); } ListGeoResultRedisGeoCommands.GeoLocationString list results.getContent(); if (list.size() from) { // 没有下一页了结束 return Result.ok(Collections.emptyList()); } // 4.1.截取 from ~ end的部分 ListLong ids new ArrayList(list.size()); MapString, 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查询Shop String idStr StrUtil.join(,, ids); ListShop shops query().in(id, ids).last(ORDER BY FIELD(id, idStr )).list(); for (Shop shop : shops) { shop.setDistance(distanceMap.get(shop.getId().toString()).getValue()); } // 6.返回 return Result.ok(shops); }四、用户签到1.需求分析与技术选型我们想要记录用户的签到情况第一时间想到用户只要一签到我们就把这条数据存入数据库就行了嘛。表字段设计如下用户一次签到就是一条记录假如有1000万用户平均每人每年签到次数为10次则这张表一年的数据量为 1亿条有没有更节约内存的办法呢用户签到无非两种情况签到与没签到就像0和1那能不能用二进制表示呢如果我们要记录一个月用户签到情况用一个32位的二进制表示不就可以了这也就引出了位图BitMap。Redis中是利用string类型数据结构实现BitMap因此最大上限是512M转换为bit则是 2^32个bit位。2.BitMap简介BitMap的操作命令有SETBIT向指定位置offset存入一个0或1GETBIT 获取指定位置offset的bit值BITCOUNT 统计BitMap中值为1的bit位的数量BITFIELD 操作查询、修改、自增BitMap中bit数组中的指定位置offset的值BITFIELD_RO 获取BitMap中bit数组并以十进制形式返回BITOP 将多个BitMap的结果做位运算与 、或、异或BITPOS 查找bit数组中指定范围内第一个0或1出现的位置3.签到功能实现需求实现签到接口将当前用户当天签到信息保存到Redis中思路我们可以把年和月作为bitMap的key然后保存到一个bitMap中每次签到就到对应的位上把数字从0变成1只要对应是1就表明说明这一天已经签到了反之则没有签到。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(); }4.相关功能与代码技巧问题1什么叫做连续签到天数 从最后一次签到开始向前统计直到遇到第一次未签到为止计算总的签到次数就是连续签到天数。Java逻辑代码获得当前这个月的最后一次签到数据定义一个计数器然后不停的向前统计直到获得第一个非0的数字即可每得到一个非0的数字计数器1直到遍历完所有的数据就可以获得当前月的签到总天数了问题2如何得到本月到今天为止的所有签到数据BITFIELD key GET u[dayOfMonth] 0假设今天是10号那么我们就可以从当前月的第一天开始获得到当前这一天的位数是10号那么就是10位去拿这段时间的数据就能拿到所有的数据了那么这10天里边签到了多少次呢统计有多少个1即可。问题3如何从后向前遍历每个bit位注意bitMap返回的数据是10进制哪假如说返回一个数字8那么我哪儿知道到底哪些是0哪些是1呢我们只需要让得到的10进制数字和1做与运算就可以了因为1只有遇见1 才是1其他数字都是0我们把签到结果和1进行与操作每与一次就把签到结果向右移动一位以此类推我们就能完成逐个遍历的效果了。需求实现下面接口统计当前用户截止当前时间在本月的连续签到天数有用户有时间我们就可以组织出对应的key此时就能找到这个用户截止这天的所有签到记录再根据这套算法就能统计出来他连续签到的次数了。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:5:202203 GET u14 0 ListLong result stringRedisTemplate.opsForValue().bitField( key, BitFieldSubCommands.create() .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0) ); 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); }5.BitMap解决缓存穿透回顾缓存穿透发起了一个数据库不存在的redis里边也不存在的数据通常你可以把他看成一个攻击解决方案判断id0如果数据库是空那么就可以直接往redis里边把这个空数据缓存起来第一种解决方案遇到的问题是如果用户访问的是id不存在的数据则此时就无法生效第二种解决方案遇到的问题是如果是不同的id那就可以防止下次过来直击数据所以我们如何解决呢我们可以将数据库的数据所对应的id写入到一个list集合中当用户过来访问的时候我们直接去判断list中是否包含当前的要查询的数据如果说用户要查询的id数据并不在list集合中则直接返回如果list中包含对应查询的id数据则说明不是一次缓存穿透数据则直接放行。现在的问题是这个主键其实并没有那么短而是很长的一个主键哪怕你单独去提取这个主键但是在11年左右淘宝的商品总量就已经超过10亿个所以如果采用以上方案这个list也会很大所以我们可以使用BitMap来减少list的存储空间。我们可以把list数据抽象成一个非常大的BitMap我们不再使用list而是将db中的id数据利用哈希思想比如id % bitmap.size 算出当前这个id对应应该落在bitmap的哪个索引上然后将这个值从0变成1然后当用户来查询数据时此时已经没有了list让用户用他查询的id去用相同的哈希算法 算出来当前这个id应当落在bitmap的哪一位然后判断这一位是0还是1如果是0则表明这一位上的数据一定不存在 采用这种方式来处理需要重点考虑一个事情就是误差率所谓的误差率就是指当发生哈希冲突的时候产生的误差。五、UV统计1.前置概念首先我们搞懂两个概念UV全称Unique Visitor也叫独立访客量是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站只记录1次。PV全称Page View也叫页面访问量或点击量用户每访问网站的一个页面记录1次PV用户多次打开页面则记录多次PV。往往用来衡量网站的流量。通常来说UV会比PV大很多所以衡量同一个网站的访问量我们需要综合考虑很多因素所以我们只是单纯的把这两个值作为一个参考。2.解决方案UV统计在服务端做会比较麻烦因为要判断该用户是否已经统计过了需要将统计过的用户信息保存。但是如果每个访问的用户都保存到Redis中数据量会非常恐怖那怎么处理呢Hyperloglog(HLL)是从Loglog算法派生的概率算法用于确定非常大的集合的基数而不需要存储其所有值。相关算法原理大家可以参考HyperLogLog算法原理Redis中的HLL是基于string结构实现的单个HLL的内存永远小于16kb内存占用低的令人发指作为代价其测量结果是概率性的有小于0.81的误差。不过对于UV统计来说这完全可以忽略。