黑马点评【Redis】

文章目录

  • 一、短信登录功能
    • 1、Session实现
    • 2、集群的session共享问题
  • 二、商户查询缓存
    • 1、根据id查询商品缓存的流程
    • 2、缓存更新策略
    • 3、缓存穿透
    • 4、缓存雪崩
    • 5、缓存击穿
    • 6、缓存工具封装
  • 三、优惠券秒杀
    • 1、全局唯一id生成策略
    • 2、下单功能
    • 3、超卖问题
    • 4、一人一单
    • 5、集群下的线程并发安全问题
    • 6、分布式锁实现版本1
    • 7、分布式锁误删问题
    • 8、Lua脚本解决多条命令原子性问题
    • 9、Redission
      • 9.1、基本介绍
      • 9.2、Redisson入门
      • 9.3、Redisson可重入锁原理
      • 9.4、Redisson的锁重试和WatchDog机制
      • 9.5、Redisson的multiLock原理
    • 10、Redis优化秒杀
    • 11、Redis消息队列实现异步秒杀
  • 四、达人探店
    • 1、发布探店笔记
    • 2、实现查看发布探店笔记的接口
    • 3、完善点赞功能
    • 4、点赞排行榜
    • 5、好友关注
    • 6、好友共同关注
    • 7、关注推送
    • 8、滚动分页查询收件箱
    • 9、附近商户
      • 9.1、GEO数据结构和用法
      • 9.2、导入店铺数据到GEO中
      • 9.3、实现附近商户功能
    • 10、用户签到
    • 11、HyperLogLog

网上都说黑马的redis的课好,我也来,简单记录一下重要的知识点和解决问题的思路,因为之前了解过一些redis的基本用法,这里就直接进入实战了

源码:https://gitee.com/lzy612/hmdp

黑马点评【Redis】_第1张图片

一、短信登录功能

1、Session实现

首先搞一个lower版本的,后面再用redis进行优化

黑马点评【Redis】_第2张图片

这个逻辑我也见到很多次了,希望下次可以不假思索,直接说出来,
这里的发送验证码,和登录就不记录了,具体写一写校验登录的方法

拦截器代码

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1、获取session
        HttpSession session = request.getSession();
        // 2、获取session中的用户
        UserDTO userDto = (UserDTO)session.getAttribute("user");
        // 3、判断用户是否存在
        if (userDto == null) {
            // 4、不存在就拦截
            response.setStatus(401);
            return false;
        }
        // 5、存在就将信息放到线程中去,并放行
        UserHolder.saveUser(userDto);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户,防止内存泄露
        UserHolder.removeUser();
    }
}

注册代码

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
                // 放行
                "/user/code", // 发送验证码
                "/user/login", // 登录
                "/shop/**",   // 商店所有的
                "blog/hot", // 热点评论的
                "/shop-type/**", // 商店类型的
                "/voucher/**"    // 优惠卷的
        );
    }
}

知识点

  1. 用户的请求会带着cookie,登录的凭证sessionId就在cookie中
  2. 使用springboot的拦截器来实现对登录校验的拦截,就不用多次在controller中写这个逻辑了
  3. 这里考虑到了,我们在后面的请求中可能要用到session中存的一些数据,但是如果要是每一个请求都要获取的session的,就要在方法的参数上多加一个,这样就有点造轮子的嫌疑了,所以这里使用了将信息放到线程中去,有人要用就从这里面取出来,没人用就算了
  4. 还有一个问题在使用完这个线程中的数据的时候,可以用拦截器中的after那个方法将线程中的数据删除掉,虽然我现在可能写不写这个东西无所谓,好习惯有头有尾

2、集群的session共享问题

黑马点评【Redis】_第3张图片

多台Tomcat并不共享Session,如果一通过nginx访问一个tomcat的集群,你刚在tomcat1机器里面输入了密码登录信息,然后当你下一个请求进入的tomcat2中,但是这个tomcat2并没有session,你还得重新登录,非常的不合理

这里使用的解决方案就是使用redis来取代session

满足的特点:

  • 数据共享
  • 内存数据
  • key,value结构

黑马点评【Redis】_第4张图片

修改发送验证码

将code存储到session中变成存储到redis中去,将手机号作为唯一值作为key,验证码code作为redis,并且同时设置好过期时间

修改登录部分

这里有点东西

登录过程验证完校验码后,要将得到的用户信息存储到redis中,这里用什么格式去存储,又是一个讲究,

  1. 首先是key,这里的话还是可以用手机号的,用手机号提取出用户的信息,看似没有什么问题,但是要用户信息做什么呢 ,用于后面的登录校验判断,还有一些要请求其他页面的时候需要使用该用户的信息,这就好像是一个人在大街上拿了一块金条去买东西,你说能不被人惦记吗,所以这里要使用一个随机的token来作为key,当这里业务处理完的时候,将这个token返回给前端,请求的时候,就携带上这个token,相当于一个人拿了张银行卡去买东西,虽然功能是相同的,但是更加的安全
  2. 然后是value,redis有5大数据结构,肯定不是瞎设计的,所以我们存储value的时候也要研究一下,如果是用string类型的话,我们就需要将用户的信息变成一个json格式的,json格式有什么,中间有{}、:等的符号,一个两个不说,但是如果好多的数据,那岂不是要浪费大量的存储空间,并且存储成json格式的话,你取只能整个取出来,不能要哪个取哪个,费劲。所以我们这里需要采用一个更加的方式,视频中给出的是使用Hash来存储,这就很好的解决了string的问题,但是这里我们怎么讲一个实体对象,变成一个key-value对呢,这里可以采用BeanUtil的beanToMap这个API,直接将实体封装成一个key-value结构的数据,这时千万不要忘记加过期时间,同时也要考虑到整个的逻辑,如果我们登录了以后这个过期时间就开始倒计时了,我们如果正在访问的页面好好的,突然过期时间到了,点击下一个要看的页面,直接就弹出去了,用户体验及其不好,所以过期时间应该在访问一个页面的时候进行更新,也就是登录验证的地方,也就是拦截器的位置
  3. 这个应该不算是一个逻辑问题,算是一个用法方面的问题
 Map<String, Object> map = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));

自定义key-value的类型,能设置好多的规则

修改登录拦截器部分

  1. 这部分涉及到了前端拿着token来获取用户信息,有了这个信息,就可以畅通无阻的访问页面了,通过访问头获取token

String token = request.getHeader(“authorization”);

  1. 然后这里应该也算是技巧,将key-value又转换成实体类,果然解铃还须系铃人

这里又会发现一个问题,说实话我以为写到这个程度就算是很完美了,想不到啊,太细了!!!!之前不是说逻辑方面,要进入到一个页面就要刷新token的有效期,但是有一些页面不会被拦截,也就是不会刷新token有效期,这里采用了一个拦截器链来解决这个问题

黑马点评【Redis】_第5张图片

  1. 相当于就是将之前那个拦截器拆开了
  2. 第一个拦截器的主要功能就是拦截所有的请求,同时将用户信息放到线程中,如果放到了线程中之后的话,就刷新token的有效期,反正无论发生怎么样的情况,这个拦截的请求都会放心
  3. 第二个拦截器就是通过判断线程中有没有东西,来判断是否通过登录验证,通过了就放行

二、商户查询缓存

黑马点评【Redis】_第6张图片
黑马点评【Redis】_第7张图片

黑马点评【Redis】_第8张图片

之前不知道这个步骤就叫做缓存命中了缓存命中就是请求到redis中的数据成功返回就是缓存命中

1、根据id查询商品缓存的流程

黑马点评【Redis】_第9张图片

这里感觉还可以没啥说的

2、缓存更新策略

黑马点评【Redis】_第10张图片
黑马点评【Redis】_第11张图片

这里提到删除缓存和操作数据库谁前谁后的线程安全问题,记录一下

第一组删除缓存,再操作数据库

理想情况:
黑马点评【Redis】_第12张图片

异常情况

黑马点评【Redis】_第13张图片

也就是在你删除缓存和更新数据库之间,被趁虚而入了,感觉就是这两个操作不是原子操作

第二组操作数据库,再删除缓存

正常情况

黑马点评【Redis】_第14张图片

异常情况
黑马点评【Redis】_第15张图片

比较两种异常情况的发生概率

这里目的就是查询到正确的数据

  1. 第一种当删除缓存完之后,要更新数据库,更新数据库需要一段时间来准备,所以说这里有一段时间差,这时候可能就会被趁虚而入,一个迅速的查询操作就会侵入,导致更新数据的这个命令没有执行完,那边就把你旧的数据弄走了,概率来说很大
  2. 第二种当如果一个缓存恰好过时了,然后你去查询没有命中,去查询数据库了,然后其他线程更新了你要查询的数据,然后你写入缓存,就是旧的数据,后者明显比前者概率更小
  3. 其实我觉得这件可以这样理解,我们把查询缓存未命中,查询数据库可以看做是一体的,中间被钻空的几率是很小的,然后删除缓存和更新数据库的执行顺序可以这样看,如果先执行删除缓存的话,再执行更新数据库中间的时间较长,也就是被侵入的几率打;如果先执行更新数据库的话,然后紧接着删除缓存,中间的时间较短,被侵入的几率有,但是很小。也就是从这样来看的话,先操作数据,再删除缓存较为优

黑马点评【Redis】_第16张图片

我这里的理解可能还不到位,慢慢来吧

3、缓存穿透

黑马点评【Redis】_第17张图片

新知识点,缓存穿透和布隆过滤

解决缓存穿透
黑马点评【Redis】_第18张图片

之前是这样的,从开始走到结束的过程中,看起来都挺好的,但是如果有人恶意多次查询redis中没有的数据和数据库中没有的数据,这样就会导致数据库的压力增大,甚至崩坏掉,所以要解决这个问题

黑马点评【Redis】_第19张图片

视频中是使用缓存空对象来解决

黑马点评【Redis】_第20张图片

4、缓存雪崩

黑马点评【Redis】_第21张图片

5、缓存击穿

黑马点评【Redis】_第22张图片

黑马点评【Redis】_第23张图片

这个逻辑过期就和逻辑删除感觉一样,通过在value中设置一个逻辑的ttl过期时间来代替这个数据的ttl,然后你还可以获取到这个数据但是还是有点小问题,就是数据可能不一致,这里通过新开一个线程去处理新的重建任务,此时其他的线程就会访问一个旧的数据

黑马点评【Redis】_第24张图片

  1. 基于互斥锁方式解决缓存击穿问题

黑马点评【Redis】_第25张图片

// 利用互斥锁解决缓存击穿
public Shop queryWithMutex(Long id) {
    String key = CACHE_SHOP_KEY + id;
    // 1、从redis中查缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2、如果命中直接返回即可
    if(StrUtil.isNotBlank(shopJson)){
        // 存在直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        // 如果判断命中的是空值的话就直接结束
        return shop;
    }
    // 未命中
    if(shopJson != null){
        return null;
    }
    // *实现缓存重建*
    // 1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    Shop shopInfo = null;
    try {
        boolean isLock = tryLock(lockKey);
        // 2.判断是否获取成功
        if(!isLock){
            // 3.失败,则休眠并重试
            Thread.sleep(50);
            return queryWithMutex(id);
        }
        // 如果获取成功的话就再检查一下缓存是否存在,存在就可以直接返回了,不用再重建缓存了
        if(StrUtil.isNotBlank(shopJson)){
            // 存在直接返回
            Shop shopAgain = JSONUtil.toBean(shopJson, Shop.class);
            // 如果判断命中的是空值的话就直接结束
            return shopAgain;
        }

        // 如果我们在等待了许久之后,redis还是空的话,我们就根据id查数据库,去重建redis缓存
        shopInfo = this.getById(id);
        // 模拟网络延迟
        Thread.sleep(200);
        // 4、判断商铺是否存在
        if(shopInfo == null){
            // 如果不存在的话会将空值写入reids
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 5、如果存在就将数据写到redis中返回即可
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shopInfo), CACHE_SHOP_TTL, TimeUnit.MINUTES );
    } catch (Exception e) {
        throw new RuntimeException(e);
    }finally {
        // 释放锁
        unlock(id.toString());
    }
    return shopInfo;
}

// 尝试获取锁
private boolean tryLock(String key){
    Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.MINUTES);
    return BooleanUtil.isTrue(aBoolean);
}

// 释放锁
private void unlock(String key){
    stringRedisTemplate.delete(key);
}

通过jmeter测试

黑马点评【Redis】_第26张图片

在这里插入图片描述

就查询一次数据库,太美了

  1. 基于逻辑过期方式解决缓存击穿问题
    注意:这里的数据不会过期,所以不用考虑缓存穿透的问题

黑马点评【Redis】_第27张图片

// 存储热点数据
public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
    // 1、查询店铺数据
    Shop shop = getById(id);
    Thread.sleep(200);
    // 2、逻辑疯转逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    // 3、写入Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
public Shop queryWithLogicalExpire(Long id) {
String key = CACHE_SHOP_KEY + id;
    // 1、从redis中查缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2、未命中后直接返回
    if(StrUtil.isBlank(shopJson)){
        return null;
    }

    // 命中后,先把json反序列出来
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    JSONObject data = (JSONObject)redisData.getData();
    Shop shop = JSONUtil.toBean(data, Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    // 判断是否过期
    if(expireTime.isAfter(LocalDateTime.now())){
        // 未过期就返回客户信息
        return shop;
    }
    String lockKey = LOCK_SHOP_KEY + id;
    // 已经过期,需要缓存重建
    // 获取互斥锁
    boolean isLock = tryLock(lockKey);
    // 判断时候获取锁成功
    if(isLock){
        // 成功,开启独立线程,去完成缓存重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            // 重建缓存
            try {
                this.saveShop2Redis(id, 20L);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }finally {
                // 释放锁
                unlock(lockKey);
            }
        });
    }
    return shop;
}

6、缓存工具封装

黑马点评【Redis】_第28张图片

三、优惠券秒杀

1、全局唯一id生成策略

黑马点评【Redis】_第29张图片
黑马点评【Redis】_第30张图片
黑马点评【Redis】_第31张图片

@Component
public class RedisIdWorker {
    // 开始的时间戳
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    // 序列号的位数
    private static final long COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextId(String keyPrefix){
        // 1、生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2、生成序列号
        // 2.1、获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        // 2.2、自增长
        Long count = stringRedisTemplate.opsForValue().increment("icr" + keyPrefix + ":" + date);

        // 3、拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}

视频中展示的是通过redis自增,说实话不太懂,目前就知道能搞一堆不重复的id,就像那个加密策略一样

黑马点评【Redis】_第32张图片

2、下单功能

黑马点评【Redis】_第33张图片

下单功能实现

  1. 查询优惠券的信息
  2. 判断秒杀是否开始
    1. 判断秒杀是否开始
    2. 判断秒杀是否结束
  3. 如果开始了
    1. 判断库存是否充足
      1. 扣减库存
      2. 生成订单
      3. 返回订单id
    2. 不充足就返回异常
  4. 没开始也返回异常

这里一看就是存在一个并发问题,也就是下面的超卖问题

3、超卖问题

黑马点评【Redis】_第34张图片

黑马点评【Redis】_第35张图片

黑马点评【Redis】_第36张图片

乐观锁版本号法实现:通过多添加一个version,在进行库存减一的时候where中的version就是就是查询的version,如果和数据库中的version一样的就正常,如果不一样的话就不会发生数据库的库存减一,也就是相当于是阻止了非法操作,但是我再考虑这个是不是要在数据库中直接新加一个字段,并且如果没有扣减成功是不是该线程就会直接废了

黑马点评【Redis】_第37张图片

使用了这个CAS,测试的时候,成功率太低了,就是我上面我猜的,如果库存扣减发生异常的时候,就直接就失败了,都没有机会,直接就消失了

然后这里又修改了一下,把判断库存的数量弄成了大于0,之前那样是太小心了,只要是不按套路来就直接失败了,现在这样就会提高成功率,当即将发生超卖的时候就给拦截住,让他失败
黑马点评【Redis】_第38张图片

黑马点评【Redis】_第39张图片

4、一人一单

遏制黄牛买卖
黑马点评【Redis】_第40张图片

这里通过实现这个逻辑,还是会出现并发的问题,在查询订单的时候涌入一大堆的线程,还是会发生一人多单的情况,这里应该使用悲观锁去解决这个问题,之前那个是更新优惠卷的操作,所以适合用乐观锁,这里这个是要查询一个数据,不太适合,所以使用悲观锁去解决

@Transactional
public Result createVoucherorder(Long voucherId){

    Long userId = UserHolder.getUser().getId();

    // 根据优惠券id和用户id查询订单
    Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();

    // 判断订单是否存在
    if (count > 0) {
        // 用户已经购买了
        return Result.fail("用户已经购买了");
    }

    // 4、充足就可以扣减库存
    boolean success = seckillVoucherService.update().setSql("stock = stock - 1")
            .eq("voucher_id", voucherId)
            // 多设施一个条件
            .gt("stock", 0)
            .update();
    if (!success) {
        // 扣减失败
        return Result.fail("库存不足!");
    }

    // 5、然后就创建订单并返回订单id
    VoucherOrder voucherOrder = new VoucherOrder();
    // 订单id
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    // 用户id

    voucherOrder.setUserId(userId);
    // 代金券id
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);

    return Result.ok(voucherId);
}

黑马点评【Redis】_第41张图片
两个知识点

  1. 上锁的方式,这里使用用户的id进行上锁,但是单纯的将userId用toString底层还是新建了一个stirng的字符串,事实上还是不一样的值,所以要用那个intern来解决这个,确保userId始终是一个值
  2. 第二个方式就是关于事务失效的,事务啥的都是由spring去统一进行管理的,你这个要加事务的方法,没有在spring的管理之下,所以会造成事务失效,这里给出的解决办法就是通过AOP获取到它的代理对象,把这个方法注入到spring中,这样才会生效

这里我有个小疑问,就是虽然是一人一单的实现了,但是我看还是查询了数据库100次,大量的访问数据库的话,会不会数据库会直接崩掉,正常来说不会有一个人闲着没事干,成百上千的访问,就怕那些恶意的,所以我觉得将这种优惠卷的东西,还是通过redis来做一下缓冲,要不就直接把这东西放到redis中去

5、集群下的线程并发安全问题

黑马点评【Redis】_第42张图片
黑马点评【Redis】_第43张图片

都要改,要不然的,nginx就不会负载均衡

然后通过debug后就可以复现,一人两单的问题

在这里插入图片描述

黑马点评【Redis】_第44张图片

又发现自己的知识欠缺的地方了,jvm虚拟机要提上日程了,在集群和分布式的情况下,之前解决一人一单的解决方案就不太行了

6、分布式锁实现版本1

黑马点评【Redis】_第45张图片
黑马点评【Redis】_第46张图片
黑马点评【Redis】_第47张图片

黑马点评【Redis】_第48张图片

这样的话,死锁的风险会很高,因为这三个操作不具有原子性

黑马点评【Redis】_第49张图片
在这里插入图片描述

这个把上述的问题基本解决了一下,就算是获取锁和释放锁直接出了差错,但是还是有过期时间,以免死锁,但是又引入了新的问题,你怎么知道一个合适的过期时间呢,太短了可能没执行完就没了,太长了又影响性能,这就很烦人了

黑马点评【Redis】_第50张图片

黑马点评【Redis】_第51张图片

public class SimpleRedisLock implements ILock{

    private String name;

    private StringRedisTemplate stringRedisTemplate;

    private static final String KEY_PREFIX = "lock:";

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 1、获取线程标识
        long threadId = Thread.currentThread().getId();
        // 2、获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        // 做拆箱的时候注意空指针的问题
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

7、分布式锁误删问题

黑马点评【Redis】_第52张图片
黑马点评【Redis】_第53张图片
黑马点评【Redis】_第54张图片

这里的锁的误删是因为业务阻塞问题,通过在释放锁之前加一个判断锁是否是自己的来解决误删锁的问题

黑马点评【Redis】_第55张图片

然后下面这种情况是,当你判断完锁是否是自己的,然后在判断锁和释放锁之间发生了阻塞的话,就又回到了之前那种情况,又会去删除别人的锁,也就是目前的判断锁和释放锁的操作不是一个原子操作,很容易被趁虚而入

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

黑马点评【Redis】_第56张图片
黑马点评【Redis】_第57张图片

黑马点评【Redis】_第58张图片

调用Lua脚本改造分布式锁
黑马点评【Redis】_第59张图片

黑马点评【Redis】_第60张图片

这个已经是比较成熟了,但是还有有进步的空间,我靠!太麻烦了,太细致了!

9、Redission

9.1、基本介绍

黑马点评【Redis】_第61张图片

黑马点评【Redis】_第62张图片

9.2、Redisson入门

黑马点评【Redis】_第63张图片

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.84.132:6379");
        // 创建ReissonClient对象
        return Redisson.create(config);
    }
}

黑马点评【Redis】_第64张图片

黑马点评【Redis】_第65张图片

这里直接换成Redisson的锁,直接就能用,好像是之前都是在造轮子,但是一步一步过来的,原理也清楚了些

9.3、Redisson可重入锁原理

黑马点评【Redis】_第66张图片

通过使用Hash来实现可重入锁

黑马点评【Redis】_第67张图片

黑马点评【Redis】_第68张图片

9.4、Redisson的锁重试和WatchDog机制

黑马点评【Redis】_第69张图片

这里好难啊,,,,先放一放

黑马点评【Redis】_第70张图片

9.5、Redisson的multiLock原理

黑马点评【Redis】_第71张图片
黑马点评【Redis】_第72张图片

黑马点评【Redis】_第73张图片

10、Redis优化秒杀

黑马点评【Redis】_第74张图片

-- 1、参数列表

-- 1.1、优惠卷id
local voucherId = ARGV[1]

-- 1.2、用户id
local userId = ARGV[2]

-- 2、数据key
-- 2.1、库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2、订单key
local orderKey = 'seckill:order:' .. voucherId


-- 3、脚本业务
-- 3.1、判断库存是否充足get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2、库存不足,返回1
    return 1
end

-- 3.2、判断用户是否下单
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3、存在,说明是重复下单
    return 2
end

-- 3.4、扣库存
redis.call('incrby', stockKey, -1)
-- 3.5、下单(保存用户)
redis.call('sadd', orderKey, userId)
return 0
// 秒杀部分优化
@Override
public Result seckillVoucher(Long voucherId) {
    // 获取用户
    Long userId = UserHolder.getUser().getId();
    // 1、执行lua脚本
    Long result = stringRedisTemplate.execute(
            SECKILL_SCRIPT,
            Collections.emptyList(),
            voucherId.toString(),
            userId.toString()
    );
    // 2、判断结果是否是0
    int r = result.intValue();
    if(r != 0){
        // 2.1、不为0,代表没有购买资格
        return Result.fail(r == 1? "库存不足" : "不能重复下单");
    }
    // 2.2、为0,有购买资格,把下单信息保存到阻塞队列中
    long order = redisIdWorker.nextId("order");

    return Result.ok(order);
}

这里用到了阻塞队列还有线程来进行扣库存

// 队列
    private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
    // 线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

    @PostConstruct
    private void init(){
        // 初始化完毕就运行这个
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    // 使用线程来结合阻塞队列实现扣库存
    private class VoucherOrderHandler implements Runnable{
        @Override
        public void run() {
        	while(true){
	            try {
	                // 1、获取队列中的订单信息
	                VoucherOrder take = orderTasks.take();
	                // 2、创建订单
	                handleVoucherOrder(take);
	            } catch (InterruptedException e) {
	                log.error("处理订单异常", e);
	            }
            }
        }
    }

    private void handleVoucherOrder(VoucherOrder take) {
        // 获取用户
        Long userId = take.getUserId();
        // 创建锁对象(这个是自己定义的锁)
        RLock lock = redissonClient.getLock("lock:order:" + userId);

        boolean isLock  = lock.tryLock();

        // 判断是否获取锁成功
        if(!isLock){
            // 获取锁失败,返回错误或重试
            log.error("不允许重复下单");
            return ;
        }
        try {
            proxy.createVoucherorder(take);
        } finally {
            lock.unlock();
        }
    }
@Transactional
    public void createVoucherorder(VoucherOrder voucherOrder){

        Long userId = voucherOrder.getUserId();

        // 根据优惠券id和用户id查询订单
        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();

        // 判断订单是否存在
        if (count > 0) {
            // 用户已经购买了
            log.error("用户已经购买过一次");
            return;
        }

        // 4、充足就可以扣减库存
        boolean success = seckillVoucherService.update().setSql("stock = stock - 1")
                .eq("voucher_id", voucherOrder.getVoucherId())
                // 多设施一个条件
                .gt("stock", 0)
                .update();
        if (!success) {
            // 扣减失败
            log.error("库存不足!");
            return;
        }
        save(voucherOrder);
    }

黑马点评【Redis】_第75张图片

跟着视频做到这里,还有有些小疑问,在此之前我们使用的是一个判断库存的乐观锁,一个是判断一人一单的Redisson的非重入锁,那时候已经基本完成了我们所需要的任务,但是为了提高性能,我们将在redis优化中,将判断库存和一人一单提取到了redis中去做判断,然后将合法的订单生成出来放入阻塞队列中,然后另开一个线程来异步的进行数据库的操作,这时候,我发现我们一共运用了4个锁了,除了之前那两个锁以外,我们又在redis中加了一个库存的锁,一个判断一人一单的锁,功能重复实现了,我觉得把后面的那两个删除了也可以,视频中最后给那些判断的返回值都弄成了return;应该就是这个原因了吧

11、Redis消息队列实现异步秒杀

黑马点评【Redis】_第76张图片
黑马点评【Redis】_第77张图片

黑马点评【Redis】_第78张图片

黑马点评【Redis】_第79张图片

黑马点评【Redis】_第80张图片

黑马点评【Redis】_第81张图片

黑马点评【Redis】_第82张图片

黑马点评【Redis】_第83张图片

黑马点评【Redis】_第84张图片

黑马点评【Redis】_第85张图片

黑马点评【Redis】_第86张图片

黑马点评【Redis】_第87张图片

黑马点评【Redis】_第88张图片
黑马点评【Redis】_第89张图片

黑马点评【Redis】_第90张图片

黑马点评【Redis】_第91张图片

最后呢,使用了redis中的stream来进行对项目进行了优化,其实这里提供了三种方式,最优的就是Stream了,所以我觉得我其他两种就当是了解了,学一下stream,速度快,安全

应该熟悉的命令

  1. 创建一个消费者组:XGROUP CREATE key的名称 消费者组的名称 起始id标识 MKSTREAM

示例:XGROUP CREATE stream.orders g1 0 MKSTREAM

  1. 向消费者组里面添加消息:XADD key的名称 * k1 v1 k2 v2…

示例(lua脚本):redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)

  1. 从消费者组里面获取消息:XREADGROUP GROUP 消费者组的名称 消费者名称 COUNT 读取几个 BLOCK 等待时间 STREAMS key的名称 从未消费的开始(>)/从pending-list中读取第一个(0)

示例(java的api):stringRedisTemplate.opsForStream().read( Consumer.from("g1", "c1"), StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)), StreamOffset.create("stream.orders", ReadOffset.lastConsumed())

感觉还有一个重要的点,就是读取消息队列中的消息,正常的读取后就要进行ACK确认,出异常的话就要再去读取pending-list中的消息,进行处理读取pending-list的命令也有些不同,下面就贴点代码

lua代码

-- 1、参数列表

-- 1.1、优惠卷id
local voucherId = ARGV[1]

-- 1.2、用户id
local userId = ARGV[2]

-- 1.3、订单id
local orderId = ARGV[3]

-- 2、数据key
-- 2.1、库存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2、订单key
local orderKey = 'seckill:order:' .. voucherId


-- 3、脚本业务
-- 3.1、判断库存是否充足get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
    -- 3.2、库存不足,返回1
    return 1
end

-- 3.2、判断用户是否下单
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3、存在,说明是重复下单
    return 2
end

-- 3.4、扣库存
redis.call('incrby', stockKey, -1)
-- 3.5、下单(保存用户)
redis.call('sadd', orderKey, userId)

-- 这个地方就是将我们需要的消息放到消费者组里面
-- 3.6、 发送消息到队列中, XADD stream.orders * k1 v1 k2 v2
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0

java代码

private class VoucherOrderHandler implements Runnable{
  String queueName = "stream.orders";
    @Override
    public void run() {
        while(true) {
            try {
                // 1、获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS streams.order >
                List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                        Consumer.from("g1", "c1"),
                        StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                        StreamOffset.create(queueName, ReadOffset.lastConsumed())
                );
                // 2、判断消息获取是否成功
                // 2.1、如果获取失败,说明没有消息,继续下一次循环
                if (list == null || list.isEmpty()){
                    // 如果获取失败,说明没有消息,继续下一次循环
                    continue;
                }
                // 解析消息中的订单信息
                MapRecord<String, Object, Object> record = list.get(0);
                Map<Object, Object> value = record.getValue();
                VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                // 3、如果获取成功就可以下单
                handleVoucherOrder(voucherOrder);
                // 4、ACK确认
                stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());

            } catch (Exception e) {
                log.error("处理订单异常", e);
                handlePendingList();
            }
        }
    }

    private void handlePendingList(){
        while(true) {
            try {
                // 1、获取pending-list中的订单信息 XREADGROUP GROUP g1 c1  STREAMS streams.order 0
                List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                        Consumer.from("g1", "c1"),
                        StreamReadOptions.empty().count(1),
                        StreamOffset.create(queueName, ReadOffset.from("0"))
                );

                // 2、判断消息获取是否成功
                // 2.1、如果获取失败,说明没有消息,继续下一次循环
                if (list == null || list.isEmpty()){
                    // 如果获取失败,说明pending-list中没有消息,结束循环
                    break;
                }
                // 解析消息中的订单信息
                MapRecord<String, Object, Object> record = list.get(0);
                Map<Object, Object> value = record.getValue();
                VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                // 3、如果获取成功就可以下单
                handleVoucherOrder(voucherOrder);
                // 4、ACK确认
                stringRedisTemplate.opsForStream().acknowledge(queueName, "g1", record.getId());

            } catch (Exception e) {
                log.error("处理pengding-list订单异常", e);
                try {
                    Thread.sleep(20);
                } catch (InterruptedException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }
}

四、达人探店

1、发布探店笔记

他给我实现了

2、实现查看发布探店笔记的接口

黑马点评【Redis】_第92张图片

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

3、完善点赞功能

黑马点评【Redis】_第93张图片

根据应用场景去选择合适的数据类型去实现,这里就使用set集合,这里面本来就不允许重复,判断集合中是否点赞过,没点赞就+1,点过赞就-1

@Override
public Result likeBlog(Long id) {
    // 1、获取登录用户
    Long userId = UserHolder.getUser().getId();
    // 2、判断当前登录用户是否已经点赞
    String key = "blog:liked:" + id;
    Boolean member = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
    if(BooleanUtil.isFalse(member)){
        // 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());
        }
    }
    return Result.ok();
}

4、点赞排行榜

黑马点评【Redis】_第94张图片

这里要把刚才点赞功能的那个给修改一下,换成sortedSet

黑马点评【Redis】_第95张图片

小tips:这里发现点赞排行榜的点赞顺序有问题,视频中解释的数据库的问题,也就是使用in的那个地方的问题,使用了in和要查询的顺序就反过来了,然后使用了order by field来解决这个问题

 // 2、解析出其中的用户id
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
// 3、解析出用户id的用户
String idStr = StrUtil.join(",", ids);
List<UserDTO> collect = userService.query().
        in("id", ids).last("order by field (id," + idStr + ")").list()
        .stream()
        .map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());

这里在查询id的用户的时候,就使用in和last配置实现了在数据库中那个正确查询顺序的操作,很神奇,又学到了一点东西

在这里插入图片描述

5、好友关注

首先这里的按钮只有关注和取消关系,当点击关注的时候,会往后台发送一条请求,将关注的用户id和是否关注发到后台,通过判断是取关还是关注,来进行对数据库中关注表的增删,然后返回,并且还有一个请求在判断页面上是否是关注还是取消关注

实现思路

  1. 获取登录用户的id,和关注的id
  2. 判断关注还是取关
  3. 不同的情况进行对数据库的增删

另一个请求

  1. 获取登录用户的id和关注的id
  2. 在数据库中是否能查询出来,然后将count是否大于0的布尔值返回即可

6、好友共同关注

实现思路

  1. 通过redis中的set类型,在进行好友关注和取关的同时,将关注信息放到redis中
  2. 通过redis中的一个方法,来求指定key的交集,也就睡好友共同关注,然后转换为UserDto进行返回(这里多次使用到了stream,map映射什么的东西,不太懂,得学

7、关注推送

黑马点评【Redis】_第96张图片

黑马点评【Redis】_第97张图片

黑马点评【Redis】_第98张图片
黑马点评【Redis】_第99张图片

需要去临时拉取信息,延迟高

黑马点评【Redis】_第100张图片

已经拉好了,直接读取就可以了

黑马点评【Redis】_第101张图片

通过对大V和粉丝的区分,进行活跃用户推消息,普通用户拉消息,这样就稍微均衡一些

黑马点评【Redis】_第102张图片

黑马点评【Redis】_第103张图片

在这里插入图片描述

黑马点评【Redis】_第104张图片

实现思路

  1. 修改保存博文业务,保存博文成功即查询follow表中关注的该用户的粉丝id
  2. 推送笔记的id给粉丝,也就是将博客的id推送到粉丝的收件箱里面,这里是使用了zsort的数据结构来存储信息的,为什么要用这个东西,是因为传统的那个分页会出现重复读的现象,所以要使用滚动分页,那个lastId就取时间戳即可

8、滚动分页查询收件箱

黑马点评【Redis】_第105张图片

这个实现思路,是通过来倒序根据分数来查询数据,并进行分页展示的,其中不仅要记录上一次查询的最小时间戳,也要查询上一次结果中,与最小值一样的元素的个数作为偏移量进行查询

黑马点评【Redis】_第106张图片

@Override
    public Result queryBlogOfFollow(Long max, Integer offset) {
        // 1、获取当前用户
        Long userId = UserHolder.getUser().getId();
        // 2、查询收件箱
        String key = FEED_KEY + userId;
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
                .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
        // 3、解析数据:blogId, 时间戳, offset
        if(typedTuples == null || typedTuples.isEmpty()){
            return Result.ok(Collections.emptyList());
        }
        ArrayList<Long> ids = new ArrayList<>(typedTuples.size());
        long minTime = 0;
        int os = 1;
        for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
            // 4.1、获取blogId
            ids.add(Long.valueOf(typedTuple.getValue()));
            // 4.2、获取分数(时间戳)
            // 4.3、获取offset
            long time = typedTuple.getScore().longValue();
            // 相当于是当前的和上一次的做对比
            if(time == minTime){
                os++;
            }else{
                minTime = time;
                // 恢复现场
                os = 1;
            }
        }
        // 4、查询blog
        String idStr = StrUtil.join(",", ids);
        List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
        for (Blog blog : blogs) {
            // 查询blog有关的用户
            queryBlogUser(blog);
            // 查询blog是否被点赞了
            isBlogLiked(blog);
        }
        // 5、封装并返回
        ScrollResult r = new ScrollResult();
        r.setList(blogs);
        r.setOffset(os);
        r.setMinTime(minTime);
        return Result.ok(r);
    }

实现步骤

  1. 获取到用户的id
  2. 查询收件箱
  3. 通过对收件箱信息的解析,获取blogId,时间戳,offset,这里就跟复杂,又要获取到最小的时间戳,而且还有获取到最小时间戳的个数
  4. 又是之前排行榜遇到的那个问题,查询的和我要的是反的,通过以下方式解决

List blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();

  1. 最后是对一些数据的补充和封装

9、附近商户

9.1、GEO数据结构和用法

黑马点评【Redis】_第107张图片

9.2、导入店铺数据到GEO中

黑马点评【Redis】_第108张图片

@Test
public void loadShopData(){
    // 1、查询店铺信息
    List<Shop> list = shopService.list();
    // 2、把店铺分组,把typeId分组,id一致的放到一个集合
    // stream流好牛啊
    Map<Long, List<Shop>> map = list
            .stream()
            .collect(Collectors.groupingBy(Shop::getTypeId));
    // 3、分批写入redis
    for (Map.Entry<Long, List<Shop>> longListEntry : map.entrySet()) {
        // 1、获取类型id
        Long typeId = longListEntry.getKey();
        String key = "shop:geo:" + typeId;
        // 2、获取同类型的店铺
        List<Shop> value = longListEntry.getValue();
        List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>();
        // 3、写入redis
        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);
    }
}

9.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>> radius = stringRedisTemplate.opsForGeo()
            .radius(key, new Circle(new Point(x, y), new Distance(5000, Metrics.MILES)), RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().
                    //包含距离,包含经纬度,升序前五个
                            includeDistance().includeCoordinates().sortAscending().limit(end));

    // 4、解析出id
    if(radius == null){
        return Result.ok(Collections.emptyList());
    }

    List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = radius.getContent();
    if(content.size() <= from){
        // 没有下一页了,结束
        return Result.ok(Collections.emptyList());
    }
    // 截取from-end的部分
    List<Long> ids = new ArrayList<>(content.size());
    Map<String, Distance> distanceMap = new HashMap<>(content.size());

    content.stream().skip(from).forEach(res -> {
        // 店铺id
        String shopIdStr = res.getContent().getName();
        ids.add(Long.valueOf(shopIdStr));
        // 获取距离
        Distance distance = res.getDistance();
        distanceMap.put(shopIdStr, distance);
    });

    // 5、根据id查询Shop
    String idStr = StrUtil.join(",", ids);
    List<Shop> list = query().in("id", ids).last("ORDER BY FIELD (id," + idStr + ")").list();
    for (Shop shop : list) {
        shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
    }

    // 6、返回
    return Result.ok(list);
}

逻辑有点复杂,一个是使用了滚动分页,说实话我看这后端不知道是怎么实现了,应该是前端设计成这样子的,而且这个计算分页参数也是一些小小的经验,还有那个redis的附近商店查询,我的redis有点旧,我就使用了那个低版本的去实现了,还有那个用stream的跳过啥的然后在forEach中去收集一些数据,之前还有在stream中去构造,修改一些数据,这东西真方便,这个redis实战看完就学那个,最后还是in ("id", ids).last("order by field(id, + idsStr + ")",已经见过好多次了

10、用户签到

黑马点评【Redis】_第109张图片

黑马点评【Redis】_第110张图片

@Override
public Result sign() {
    // 1、获取当前登录用户
    Long userId = UserHolder.getUser().getId();
    // 2、获取日期
    LocalDateTime now = LocalDateTime.now();
    // 3、拼接key
    // 获取年月的时间
    String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    // 拼接好了  前缀+用户id+年月
    String key = USER_SIGN_KEY + userId + keySuffix;
    // 4、获取今天是本月的第几天
    int dayOfMonth = now.getDayOfMonth();
    // 5、写入redis
    stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
    return Result.ok();
}

黑马点评【Redis】_第111张图片

@Override
public Result signCount() {
    // 1、获取当前登录用户
    Long userId = UserHolder.getUser().getId();
    // 2、获取日期
    LocalDateTime now = LocalDateTime.now();
    // 3、拼接key
    // 获取年月的时间
    String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    // 拼接好了  前缀+用户id+年月
    String key = USER_SIGN_KEY + userId + keySuffix;
    // 4、获取今天是本月的第几天
    int dayOfMonth = now.getDayOfMonth();
    // 5、获取本月截止今天为止的所有签到记录,返回的是一个十进制的数字
    List<Long> result = stringRedisTemplate.opsForValue().bitField(
            key,
            BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
    );
    if(result == null || result.isEmpty()){
        // 没有任何签到结果
        return Result.ok();
    }
    Long aLong = result.get(0);
    if(aLong == null || aLong == 0){
        return Result.ok(0);
    }
    // 6、循环遍历
    int count = 0;
    while(true){
        // 7、让这个数字与1做与运算,得到数字的最后一个bit位  8、判断这个bit位是否为0
        if((aLong & 1) == 0){
            // 9、如果是0,说明未签到,结束
            break;
        }else{
            // 10、如果不为0.说明已签到,计数器加1
            count++;
        }
        // 11、把数字右移一位,抛弃最后一个bit位,继续判断下一个bit位
        // 右移一位
        aLong >>>= 1;
    }

    return Result.ok(count);
}

这里用了一个双重判断, 还用了进制和位运算,这些都不太熟悉,很难受

11、HyperLogLog

黑马点评【Redis】_第112张图片

黑马点评【Redis】_第113张图片

@Test
void testHyperLogLog(){
    String[] values = new String[1000];
    int j = 0;
    for(int i = 0; i < 1000000 ; i++){
        j = i % 1000;
        values[j] = "user_" + i;
        if(j == 999){
            stringRedisTemplate.opsForHyperLogLog().add("hl2", values);
        }
    }
    // 统计数量
    Long count = stringRedisTemplate.opsForHyperLogLog().size("hl2");
    System.out.println("count = " + count);
}

你可能感兴趣的:(redis,java,缓存)