黑马点评项目笔记

文章目录

一、短信登录

1. Session实现

在这里插入图片描述
登录验证功能:三个点,一个是拦截器 HandlerInterceptor 一个是ThreadLocal线程,隐藏用户敏感信息

  • 拦截器 HandlerInterceptor
    HandlerInterceptor WebMvcConfigurer
  • ThreadLocal线程
  • 隐藏用户敏感信息

存在问题:Session共享问题,多台Tomcat并不共享Session

2. Redis缓存替代Session

在这里插入图片描述

  • 保存对象选择hash结构还是string结构
    答案是hash结构,内存占用少在这里插入图片描述
    user对象转换成hashmap存储(代码如何实现?)
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
            CopyOptions.create()
                    .setIgnoreNullValue(true)
                    .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
  • token的设置
    (用于登录校验,需要不断刷新,只要用户在操作就会不断刷新过期时间,从而不会失效) -
  • 拦截器优化:
    存在的问题
    token的作用在于可以根据token查询当前用户存在,并不断刷新token的过期时间,但问题是拦截器只拦截了部分路径,如果用户一直访问那些没有被拦截的网站,那么token就有可能过期
    在这里插入图片描述

二、商户查询缓存

1. 给商品添加缓存

在这里插入图片描述
视频只给了商品页面设置了缓存,其他的页面没有缓存 可自己补充

2. 缓存与数据库一致性问题

2.1 理论部分

  • 缓存更新策略
    在这里插入图片描述
    在这里插入图片描述
    问题1: 删除缓存优势在于,比如我对数据库进行100次操作,只要没人来查询,我的缓存就不会更新,这样可以减少无效写操作
    在这里插入图片描述
    先操作数据库再删除缓存 比较安全一点!!!!!
    在这里插入图片描述

2.2 代码实现

根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间(超时剔除)

根据id修改店铺时,先修改数据库,再删除缓存(主动更新,注意的是数据库和缓存的操作顺序)

3. 缓存穿透

3.1 理论

在这里插入图片描述
布隆过滤器:存在不一定真的存在,不存在就一定是不存在

3.2 业务实现

选择了缓存空对象
如果这个数据不存在,我们不会返回404 ,还是会把这个数据写入到Redis中,并且将value设置为空,欧当再次发起查询时,我们如果发现命中之后,判断这个value是否是null,如果是null,则是之前写入的数据,证明是缓存穿透数据,如果不是,则直接返回数据。
在这里插入图片描述
下面几种的没学过,限流在黑马微服务教程里

4. 缓存雪崩

4.1 理论

在这里插入图片描述

5. 缓存击穿

5.1 理论

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
互斥锁选择的是一致性
逻辑过期选择的是可用性

5.2 互斥锁业务实现

redis的默认命令 setnx
代码逻辑比较复杂 建议多看

  • 锁的代码

private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

private void unlock(String key) {
    stringRedisTemplate.delete(key);
}
  • 理解逻辑
    在这里插入图片描述

5.3 逻辑过期业务实现

  • 封装好逻辑过期时间
    新建一个类,没有侵入性
  • 注意代码逻辑
    在这里插入图片描述

6.缓存封装工具类

就是对前面的几种方法写成工具类,利用的技巧是泛型,调用函数。
逻辑跟前面一致,主要是代码实现的技巧

7. 总结

视频P47

三、优惠券秒杀

1. 全局唯一ID

1.1 理论基础

在这里插入图片描述
其他几种办法,需要了解自我感觉面试可以问
在这里插入图片描述

1.2 代码实现

timestamp << COUNT_BITS | count; 左移X位 进行或运算,由于后面都是0 所以也就相当于拼接

2. 实现秒杀下单 (存在并发超卖)

在这里插入图片描述

2.2 存在问题

在这里插入图片描述

3. 乐观锁解决超卖问题

3.1 理论

在这里插入图片描述
在这里插入图片描述
没必要用版本号 直接用库存就好了
但是仍然存在问题
只要我扣减库存时的库存和之前我查询到的库存是一样的,就意味着没有人在中间修改过库存,那么此时就是安全的,但是以上这种方式通过测试发现会有很多失败的情况,失败的原因在于:在使用乐观锁过程中假设100个线程同时都拿到了100的库存,然后大家一起去进行扣减,但是100个人中只有1个人能扣减成功,其他的人在处理时,他们在扣减时,库存已经被修改过了,所以此时其他线程都会失败
改进二
之前的方式要修改前后都保持一致,但是这样我们分析过,成功的概率太低,所以我们的乐观锁需要变一下,改成stock大于0 即可

3.2 代码实现

改查询条件就好 比较简单

4. 一人一单

4.1 逻辑理论

在这里插入图片描述

4.2 改进方案 (比较难)**** 视频P54

  • 改进1:用count计数表示用户已经下过单
    Long userId = UserHolder.getUser().getId();
    int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    // 5.2.判断是否存在
    if (count > 0) {
        // 用户已经购买过了
        return Result.fail("用户已经购买过一次!");
    }

问题: 现在的问题还是和之前一样,并发过来,查询数据库,都不存在订单,所以我们还是需要加锁。但是乐观锁比较适合更新数据,而现在是插入数据,所以我们需要使用悲观锁操作

  • 改进2:给整个方法加一个synchronized锁
    首先我们的初始方案是封装了一个createVoucherOrder方法,同时为了确保他线程安全,在方法上添加了一把synchronized 锁
    存在问题:锁的粒度太粗了,在使用锁过程中,控制锁粒度 是一个非常重要的事情,因为如果锁的粒度太大,会导致每个线程进来都会锁住,所以我们需要去控制锁的粒度
  • 改进3 控制锁的粒度,根据用户id来上锁
    intern() 这个方法是从常量池中拿到数据,如果我们直接使用userId.toString() 他拿到的对象实际上是不同的对象,new出来的对象,我们使用锁必须保证锁必须是同一把
public  Result createVoucherOrder(Long voucherId) {
	Long userId = UserHolder.getUser().getId();
	synchronized(userId.toString().intern()){
         // 5.1.查询订单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否存在
        if (count > 0) {
            // 用户已经购买过了
            return Result.fail("用户已经购买过一次!");
        }

当前方法被spring的事务控制,如果你在方法内部加锁,可能会导致当前方法事务还没有提交,但是锁已经释放也会导致问题,问题如下:
锁释放了,其他线程就可以进来查询数据库,而此时事务还没有提交,新增的订单可能还没写入数据库,导致查询结果异常
也即此时锁的范围太小了,必须做到 先获取锁-提交事务-释放锁

  • 改进4 先获取锁-提交事务-释放锁
    分析如上,代码实现如下
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()){
	return this.createVoucherOrder(voucherId)}
  • 改进5 事务失效的问题(第一次遇到)
    因为你调用的方法,其实是this.的方式调用的,事务想要生效,还得利用代理来生效,所以这个地方,我们需要获得原始的事务对象, 来操作事务
    在这里插入图片描述
    还必须添加依赖和启动类注解

5.一人一单存在的问题

5.1 模拟集群

见视频

5.2 问题存在

在这里插入图片描述
由于现在我们部署了多个tomcat,每个tomcat都有一个属于自己的jvm,那么假设在服务器A的tomcat内部,有两个线程,这两个线程由于使用的是同一份代码,那么他们的锁对象是同一个,是可以实现互斥的,但是如果现在是服务器B的tomcat内部,又有两个线程,但是他们的锁对象写的虽然和服务器A一样,但是锁对象却不是同一个,所以线程3和线程4可以实现互斥,但是却无法和线程1和线程2实现互斥,这就是 集群环境下,syn锁失效的原因,在这种情况下,我们就需要使用分布式锁来解决这个问题。

四、分布式锁

1. Redis实现分布式锁

在这里插入图片描述

  • 代码实现
    注意:利用setnx方法进行加锁,同时增加过期时间,防止死锁,此方法可以保证加锁和增加过期时间具有原子性
//线程标识id前缀
private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"_";
//锁前缀名
private static final String KEY_PREFIX="lock:"

private StringRedisTemplate stringRedisTemplate;
//锁名
private String name;
//构造方法初始化
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
}
    @Override
public boolean tryLock(long timeoutSec) {
    // 获取线程标示
    String threadId =ID_PREFIX + Thread.currentThread().getId()
    // 获取锁
    Boolean success = stringRedisTemplate.opsForValue()
            .setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
}
public void unlock() {
    //通过del删除锁
    stringRedisTemplate.delete(KEY_PREFIX + name);
}

业务实现
在源代码中 利用订单orderid 加锁

2. 分布式锁误删

2.1 误删情况说明

在这里插入图片描述

  • 逻辑说明:
    线程1发生了业务阻塞,后面正常执行业务后,把线程2的锁给释放了
  • 解决方案:
    解决方案就是在每个线程释放锁的时候,去判断一下当前这把锁是否属于自己,如果不属于自己,则不进行锁的删除

2.2 解决误删

核心逻辑:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
      //获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
   // 获取锁
   Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
   return Boolean.TRUE.equals(success);
}

3. 原子性问题

3.1 更为极端的误删

判断锁和释放锁是两个不同的动作!需要让判断和释放锁变成一个原子操作
在这里插入图片描述

3.2 Lua脚本解决多条命令原子性问题

Lua脚本命令形式略

3.3 利用Java代码调用Lua脚本改造分布式锁

stringRedisTemplate.execute
在这里插入图片描述

五、分布式锁redission

1. 前面基于setnx的锁存在的问题

  • 不可重入
    在同一线程无法多次获取同一把锁
    在一个线程中,方法A去调方法b,需要先获取锁,再调方法b,而方法b需要先获取该锁,此时会失败,就是死锁
  • 不可重试
  • 超时释放
  • 主从一致性
    在这里插入图片描述

2. Redisson(原理多看)**

可以方便帮助我们实现分布式锁的 导入依赖和包就可以用

2.1 可重入锁

在这里插入图片描述
利用hash结构 多了一个 value的值,如果是同一线程,value就给+1,释放锁就-1.并且如果value=0,那就可以删了

  • 逻辑图
    在这里插入图片描述

2.2 锁重试原理

获取—得到它何时释放的时间(信号量)订阅它----在此期间就不会去重新获取锁,浪费cpu

2.3 锁超时

为什么要设置超时就释放锁? 如果redis宕机了,这时候他才能自己释放锁,避免死锁
存在哪些问题? 如果业务执行时间太长了,那这个锁它就自己会释放了,存在隐患
Redisson怎么解决呢? 利用一个watchdog看门狗的东西,进行超时续约,每隔一段时间(默认10s)就进行依次时间刷新,也就是能一直保证这个锁不过期。如果redis宕机了,那他就不会续约,时间到了还是自己释放。

总结(可重入和锁超时)

在这里插入图片描述
##

2.4 Multilock 主从一致性

  • 问题
    在这里插入图片描述
  • 解决方案
    redission提出来了MutiLock锁,使用这把锁咱们就不使用主从了,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性

看成多个 可重入锁的 集合,一旦一个节点失败就失败,缺点就在于成本高!

3. 分布式锁总结

在这里插入图片描述

六、秒杀优化

1. 思路分析

在这里插入图片描述

  • 问题
    这是个串行操作,耗时大,尤其是减库存和创建订单这种对数据库的进行写操作
  • 解决方案
    两步走
    我们考虑把判断秒杀库存和校验一人一单交给redis(用lua脚本实现),当这两个完成,也就意味着用户一定能够完成下单。完成后把相关信息,移交给一个阻塞队列,并让程序从队列中拿出信息去完成 减库存、创建订单
    在这里插入图片描述
  • 细节1:数据结构的选择
    在这里插入图片描述
    库存就用string类型,订单id用set(去重,只能一个)
  • 细节2:为什么选择lua脚本
    当用户下单之后,判断库存是否充足只需要导redis中去根据key找对应的value是否大于0即可,如果不充足,则直接结束,如果充足,继续在redis中判断用户是否可以下单,如果set集合中没有这条数据,说明他可以下单,如果set集合中没有这条记录,则将userId和优惠卷存入到redis中,并且返回0,整个过程需要保证是原子性的,我们可以使用lua来操作
  • 细节3:需要新建一个线程
    当以上判断逻辑走完之后,我们可以判断当前redis中返回的结果是否是0 ,如果是0,则表示可以下单,则将之前说的信息存入到到queue中去,然后返回,然后再来个线程异步的下单,前端可以通过返回的订单id来判断是否下单成功。注意,该线程在项目启动的时候就必须启动,用到一个注解**@PostConstruct**

2.秒杀优化-Redis完成秒杀资格判断

  • 具体需求
    新增秒杀优惠券的同时,将优惠券信息保存到Redis中
    基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
    如果抢购成功,将优惠券id和用户id封装后存入阻塞队列
    开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

3. 秒杀优化-基于阻塞队列实现秒杀优化

4. 总结

在这里插入图片描述

七、消息队列

在这里插入图片描述

1. 基于List实现消息队列(了解)

在这里插入图片描述
在这里插入图片描述

2. PubSub实现消息队列(了解)

在这里插入图片描述
在这里插入图片描述

3. 基于Stream实现消息队列

3.1 理论:单消费模式

他是一种数据类型

  • 发送消息:
    在这里插入图片描述
  • 读取消息
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

3.2 理论:消费者模式

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
转换为java代码:思路如下,
while死循环一直去读,读到信息返回,没有信息就等待一段时间重新读,如果没有就停止;
拿到消息,一定要做ack,将消息从pending list移除;
出现异常,要捕获异常后,修改读取语句中的>改成0,读取pending-list中的第一个消息
在这里插入图片描述

  • 总结

在这里插入图片描述

3.3 业务实现

创建一个Stream类型的消息队列,名为stream.orders (直接redis客户端命令行实现)
修改之前的秒杀下单Lua脚本,在认定有抢购资格后,直接向stream.orders中添加消息,内容包含voucherId、userId、orderId(写Lua脚本,给消息队列中加入信息)
项目启动时,开启一个线程任务,尝试获取stream.orders中的消息,完成下单 (用Java代码消息队列中的信息),出现异常需要读取Pending List的

八、达人探店

1. 发布探店笔记

上传图片功能的实现
save到数据库

2. 查询笔记

数据库查询功能

3. 点赞功能

update数据库

  • 难点:直接点赞,会存在同一用户无限点赞
    采用的数据结构是Set在这里插入图片描述
    其中3和4是为了提供给前端isLike属性

4. 点赞排行榜

把点赞的人展示出来,比如最早点赞的TOP5,采取的是SortedSet数据结构
第一步,把前面点赞功能的代码修改,原来的Set数据结构变成SortedSet结构,利用Score来做各种操作
第二步,显示点赞前五个

  • 此处有个sql语句的问题
    WHERE id IN ( 5 , 1 ) 显示的结果不会是 5,1,原因在于in
    WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1),必须在后面添加上此语句
@Override
public Result queryBlogLikes(Long id) {
    String key = BLOG_LIKED_KEY + id;
    // 1.查询top5的点赞用户 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
    List<Long> 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)
    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);
}

九、好友关注

1. 关注和取关

需求:基于该表数据结构,实现两个接口:
关注和取关接口
判断是否关注的接口

2. 共同关注

利用redis数据结构set求两个key的交集

//设置key:当前登录用户,关注的对象
   stringRedisTemplate.opsForSet().add(key, followUserId.toString());
 //求交集
   Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);

3.关注推送Feed流

  • 分类
    TimeLine
    智能排序
    拉模式、推模式、推拉结合
    在这里插入图片描述
  • 本项目用户量不多,采用推模式
  • 不采用传统分页,采用滚动分页
    list不支持滚动分页,sorted Set支持
  • 滚动分页查询的参数
    在这里插入图片描述
    max: 时间戳,每次都是上一次查询的最小时间戳
    min: 默认0
    offset: 偏移量,排除掉时间戳相同的
    count:每一页的数量
    主要是redis分页查询的命令,详见视频
    在这里插入图片描述
  • 业务实现
    难点在于如何获取四个参数
    容易漏的点是,查询到blog后还需要对是否点赞的功能进行一个判断
@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;
    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; // 2
    int os = 1; // 2
    for (ZSetOperations.TypedTuple<String> 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);
    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 r = new ScrollResult();
    r.setList(blogs);
    r.setOffset(os);
    r.setMinTime(minTime);

    return Result.ok(r);
}

十、附近商户

  1. 了解Geo这种数据结构
  2. 把商铺的地理坐标导入到Geo
    在这里插入图片描述
  3. 代码逻辑
@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、按照距离排序、分页。结果:shopId、distance
        String key = SHOP_GEO_KEY + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> 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());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
        if (list.size() <= from) {
            // 没有下一页了,结束
            return Result.ok(Collections.emptyList());
        }
        // 4.1.截取 from ~ end的部分
        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查询Shop
        String idStr = StrUtil.join(",", ids);
        List<Shop> 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. BitMap数据结构

在这里插入图片描述

2. 签到功能

在这里插入图片描述

@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; //user:sign:5:202302
    // 4.获取今天是本月的第几天
    int dayOfMonth = now.getDayOfMonth();
    // 5.写入Redis SETBIT key offset 1
    stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
    return Result.ok();
}

3. 签到统计

在这里插入图片描述
主要是处理十进制的数

@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
    List<Long> 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);
}

十二、UV统计

  • 概念
    UV:全称Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。
    PV:全称Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量
  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
这篇笔记是关于黑马点评项目中使用Redis的学习笔记笔记中的图片来源于黑马ppt,并提供了联系方式,如果有侵权问题可以联系删除。笔记内容包括了Redis的安装配置以及一些相关的知识点。需要注意的是,笔记中的配置是按照黑马2022的Redis进行的,仅供学习参考,并可以自由转载。另外,作者使用的是云服务器,所以IP配置不是127.0.0.1,大家需要根据自己的实际情况进行配置。在笔记中还对一些知识进行了补充,例如设置RedisSerializer来解决乱码问题。此外,笔记还提到了Redis的5种常见数据结构,包括String、List、Set、Hash和ZSet。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [redis项目-黑马点评 项目笔记](https://blog.csdn.net/qq_48617775/article/details/127497077)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Redis黑马2022笔记(基础篇)](https://blog.csdn.net/m0_56079407/article/details/123453958)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值