黑马点评关键代码解析

关键代码

用户UserDTO线程

UserDTO:Long id , nickname, icon

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

验证码登录

redis数据本身就是共享的,就可以避免session共享的问题了

  1. 根据phone从redis获取验证码并校验,不存在创建用户;
  2. 为该用户生成一个唯一的 token,并将 UserDTO 转换为 Map 后与该 token 一起存储在 Redis 中。并设置有效期
public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.从redis获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.equals(code)) {
            // 不一致,报错
            return Result.fail("验证码错误");
        }

        // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();

        // 5.判断用户是否存在
        if (user == null) {
            // 6.不存在,创建新用户并保存
            user = createUserWithPhone(phone);
        }

        // 7.保存用户信息到 redis中
        // 7.1.随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 7.2.将User对象转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        // 7.3.存储
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 7.4.设置token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.返回token
        return Result.ok(token);
    }

对应的发送验证码:

 public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);

        // 4.保存验证码到redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);//和上面 3.从redis获取验证码并校验 对应

        // 5.发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);
        // 返回ok
        return Result.ok();
    }

登录凭证保存在前端浏览器,所以不能用phone之类的个人数据
在这里插入图片描述
Redis情况:
在这里插入图片描述

拦截器token判断

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 1.获取请求头中的token
    String token = request.getHeader("authorization");
    if (StrUtil.isBlank(token)) {
        return true;
    }
    // 2.基于TOKEN获取redis中的用户
    String key  = LOGIN_USER_KEY + token;
    Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
    // 3.判断用户是否存在
    if (userMap.isEmpty()) {
        return true;
    }
    // 5.将查询到的hash数据转为UserDTO
    UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
    // 6.存在,保存用户信息到 ThreadLocal
    UserHolder.saveUser(userDTO);
    // 7.刷新token有效期
    stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
    // 8.放行
    return true;
}

点赞

内存肯定够用,而且redis有持久化机制,数据可以被持久化到磁盘

Blog.userId和Use.id关联
给Blog类中添加一个isLike字段,标示是否被当前用户点赞
判断这篇笔记是否被点赞过:stringRedisTemplate.opsForZSet().score(key, userId.toString())

点赞功能的实现:

public Result likeBlog(Long id) {
        // 1.获取登录用户
        Long userId = UserHolder.getUser().getId();
        // 2.判断当前登录用户是否已经点赞
        String key = "blog:liked:" + 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();
    }

两个地方需要用到:首页搜索推荐的分页查询 和 点进该笔记
判断是否点赞过,只需要笔记ID即博客ID,获取当前用户,查Redis
// 1.查询top5的点赞用户 zrange key 0 4

  Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);

这样并没解决频繁操作数据库,可以弄个异步任务,定时去同步redis的笔记的点赞数量,同步到mysql。现在的是点赞一次操作一次数据

chatGPT: 异步任务定时去同步redis的笔记的点赞数量

1. 点赞操作变为异步记录到 Redis

用户每次点赞或取消点赞时,操作直接写入 Redis。这个过程非常快,不需要等待复杂的数据库操作完成,异步性体现在以下几种情况:

  • Redis 本身是内存数据库,操作效率非常高。
  • 点赞操作可以通过任务队列或异步线程池完成,不需要直接阻塞主线程。

示例代码:使用线程池处理点赞操作

// 点赞操作
public void like(Long noteId) {
    CompletableFuture.runAsync(() -> {
        redisTemplate.opsForHash().increment("note:like:count", noteId.toString(), 1);
    });
}

通过 CompletableFuture.runAsync,点赞的 Redis 操作已经是异步的,主线程不会阻塞。


2. 定时任务是异步执行的批量任务

在定时任务中,Redis 中的数据会被定期拉取,然后批量写入 MySQL。这个过程本身是独立于主线程的,体现为异步任务。比如:

  • 定时任务运行在一个独立的线程池中。
  • MySQL 的批量写入可以进一步使用异步操作完成。

示例代码:定时任务批量写入

@Scheduled(fixedRate = 60000) // 每分钟执行
public void syncLikesToDatabase() {
    CompletableFuture.runAsync(() -> {
        String redisKey = "note:like:count";
        Map<Object, Object> likeCounts = redisTemplate.opsForHash().entries(redisKey);

        if (!likeCounts.isEmpty()) {
            List<NoteLikeUpdateDTO> updates = new ArrayList<>();
            likeCounts.forEach((noteId, likeCount) -> {
                updates.add(new NoteLikeUpdateDTO(Long.valueOf(noteId.toString()), Integer.valueOf(likeCount.toString())));
            });

            // 批量更新数据库
            noteMapper.batchUpdateLikeCount(updates);

            // 删除 Redis 中的数据
            redisTemplate.delete(redisKey);
        }
    });
}

在这里,CompletableFuture.runAsync 将任务放入线程池执行,而非阻塞主线程。


3. 消息队列实现异步任务

如果 Redis 写入仍有瓶颈,可以将点赞操作写入消息队列(如 Kafka、RabbitMQ),由消费者异步处理 Redis 写入和数据库同步。

生产者:将操作写入队列

public void like(Long noteId) {
    // 异步将点赞操作发送到 Kafka
    kafkaTemplate.send("note-like-topic", noteId.toString(), "like");
}

消费者:处理队列消息

@KafkaListener(topics = "note-like-topic", groupId = "like-consumer-group")
public void processLikeMessage(ConsumerRecord<String, String> record) {
    Long noteId = Long.valueOf(record.key());
    String action = record.value();

    if ("like".equals(action)) {
        redisTemplate.opsForHash().increment("note:like:count", noteId.toString(), 1);
    } else if ("unlike".equals(action)) {
        redisTemplate.opsForHash().increment("note:like:count", noteId.toString(), -1);
    }
}
5. 总结

异步任务的体现:

  1. 用户的点赞请求异步写入 Redis(线程池或消息队列)。 Redis 中的数据定期由异步任务批量同步到 MySQL。减少了高频请求对数据库的压力。
  2. 使用 CompletableFuture 或消息队列将耗时任务与主线程分离,实现更高的系统吞吐量。

关注 follow

tb_follow表:id user_id(粉丝) follow_user_id(被followed 博主) create_time
和点赞相同,

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

获取共同关注:

Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);

关注推送Feed流

推模式没有写邮箱,只有收件箱

推模式

  1. 推送到邮箱
    String key = "feed:" + userId; stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
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 = ?
        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:" + userId;
            stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
        }
        // 5.返回id
        return Result.ok(blog.getId());
    }
  1. 从邮箱获取

将时间戳作为score,按照score降序排列

 Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
                .reverseRangeByScoreWithScores(key, 0, max, offset, 2);//2是查询数量 0是范围的最小值

+滚动分页查询:
如果相同时间戳的条数>每页显示的条数 那么offset=该页的大小,下次匹配的时候还是以这个时间戳开始,但是offset会将上一页查到的信息都跳过

//上次查询的最小时间就是本次查询的最大时间 @RequestParam("lastId") Long max
//要跳过元素的数量 @RequestParam(value = "offset", defaultValue = "0")Integer offset
@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);//2是查询数量 0是范围的最小值
    // 3.非空判断
    if (typedTuples == null || typedTuples.isEmpty()) {
        return Result.ok();
    }
    // 4.解析数据:blogId、minTime(时间戳)、offset(上次查询typedTuples里和最小值一样的元素的个数)
    List<Long> ids = new ArrayList<>(typedTuples.size());
    long minTime = 0; // 2  minTime最小时间是typedTuples的最后一个值
    int os = 1; // 2  offset: typedTuples里和最小值一样的元素的个数
    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;
        }
    }

    // 5.根据id查询blog
    String idStr = StrUtil.join(",", ids);
    List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();//不能用listbyid,因为这个接口用in实现的,返回并不是有序的

    for (Blog blog : blogs) {
        // 5.1.查询blog有关的用户
        queryBlogUser(blog);
        // 5.2.查询blog是否被点赞
        isBlogLiked(blog);
    }

    // 6.封装并返回 blogId、minTime(时间戳)、offset(上次查询的最小值一样的元素)
    ScrollResult r = new ScrollResult();
    r.setList(blogs);
    r.setOffset(os);
    r.setMinTime(minTime);

    return Result.ok(r);
}

lua实现一人一单,发送订单消息到队列

@Resource
private StringRedisTemplate stringRedisTemplate;
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;

static {
    SECKILL_SCRIPT = new DefaultRedisScript<>();
    SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
    SECKILL_SCRIPT.setResultType(Long.class);
}

public Result seckillVoucher(Long voucherId) {
    Long userId = UserHolder.getUser().getId();
    long orderId = redisIdWorker.nextId("order");//生成分布式ID
    // 1.执行lua脚本
    Long result = stringRedisTemplate.execute(
            SECKILL_SCRIPT,
            Collections.emptyList(),
            voucherId.toString(), userId.toString(), String.valueOf(orderId)
    );
    int r = result.intValue();
    // 2.判断结果是否为0
    if (r != 0) {
        // 2.1.不为0 ,代表没有购买资格
        return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
    }
    // 3.返回订单id
    return Result.ok(orderId);
}

在这里插入图片描述
seckill.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. 判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3. 存在,说明是重复下单,返回 2
    return 2
end
-- 3.4. 扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5. 下单(保存用户)sadd orderKey userId
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

主要功能:

  1. 参数接收:通过 ARGV 参数接收优惠券 ID(voucherId)、用户 ID(userId)和订单 ID(orderId)。
    即Long result = stringRedisTemplate.execute(
    SECKILL_SCRIPT,
    Collections.emptyList(),
    voucherId.toString(), userId.toString(), String.valueOf(orderId)
    );的参数
  2. 库存判断:使用 Redis 的 get 命令检查库存是否充足。如果库存为 0 或更少,返回 1 表示库存不足。
  3. 重复下单检查:使用 sismember 检查用户是否已经下单。如果是,返回 2 表示重复下单。//SISMEMBER 是 Redis 的一个原生命令,用于检查一个元素是否存在于 Redis 的 集合(set)中。
  4. 扣除库存:通过 incrby 命令扣减库存,每次秒杀成功后,库存减 1。
  5. 记录订单:使用 sadd 将用户 ID 加入到 orderKey 中,表示该用户已成功下单。
  6. 发送消息到队列:使用 xadd 将订单信息发送到 Redis 流(stream.orders),用于后续的异步处理,如创建订单、扣减库存等。

Controller:提供一个 REST API 接口,接受用户请求并调用业务逻辑。

@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {

    @Resource
    private IVoucherOrderService voucherOrderService;

    @PostMapping("seckill/{id}")
    public Result seckillVoucher(@PathVariable("id") Long voucherId) {
        return voucherOrderService.seckillVoucher(voucherId);
    }
}

从队列中获取订单信息,创建订单

代码工作流程
初始化:
@PostConstruct 注解的 init() 方法会在应用启动后自动执行,并启动一个单线程的 ExecutorService(SECKILL_ORDER_EXECUTOR),提交 VoucherOrderHandler 任务,开始处理秒杀订单。
消费者从 Redis Stream 读取订单消息:

VoucherOrderHandler 类中的 run() 方法循环读取 Redis Stream 中的订单信息,并通过 XREADGROUP 命令获取新消息。每次获取一条订单消息,处理后确认该消息。
异常处理和重试:

在处理订单过程中出现异常时,会调用 handlePendingList 方法,重新处理那些未确认的消息。
分布式锁确保顺序性:

每个用户的订单创建都通过分布式锁 RLock 来保证,避免同一个用户多次下单。

import java.util.concurrent.Executors;

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.获取消息队列中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >
     
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
        Consumer.from("g1", "c1"),  // 消费者组 g1 中的消费者 c1
        StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)), // 读取 1 条消息,最多阻塞 2 秒
        StreamOffset.create("stream.orders", ReadOffset.lastConsumed())  // 从上次消费的位置开始读取
);

                  // 2.判断订单信息是否为空
                    if (list == null || list.isEmpty()) {
                        // 如果为null,说明没有消息,继续下一次循环
                        continue;
                    }
                    // 解析数据
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> value = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                    // 3.创建订单
                    createVoucherOrder(voucherOrder);
                    // 4.确认消息 XACK
                    stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                    handlePendingList();
                }
            }
        }

XREADGROUP:这个命令是消费者组专用的,它允许消费者组的成员从指定流中读取数据。每次读取后,Redis 会记录每个消费者读取到的最后一条消息的 ID,确保每个消费者不会重复消费消息。

List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
        Consumer.from("g1", "c1"),  // 消费者组 g1 中的消费者 c1
        StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)), // 读取 1 条消息,最多阻塞 2 秒
        StreamOffset.create("stream.orders", ReadOffset.lastConsumed())  // 从上次消费的位置开始读取
);

XACK:该命令用于在消费者成功处理消息后,向 Redis 表示确认该消息已被成功消费。确认的消息会从消费者组的 pending list 中移除。

stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());

这段代码在成功处理订单后,通过 XACK 命令确认已经消费了流中的消息

handlePendingList 方法重新处理那些未确认的消息。

 private void handlePendingList() {
            while (true) {
                try {
                    // 1.获取pending-list中的订单信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1),
                            StreamOffset.create("stream.orders", ReadOffset.from("0"))
                   );
                    // 2.判断订单信息是否为空
                    if (list == null || list.isEmpty()) {
                        // 如果为null,说明没有异常消息,结束循环
                        break;
                    }
                    // 解析数据
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> value = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                    // 3.创建订单
                    createVoucherOrder(voucherOrder);
                    // 4.确认消息 XACK
                    stringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());
                } catch (Exception e) {
                    log.error("处理订单异常", e);
                }
            }
        }

StreamOffset.create(“stream.orders”, ReadOffset.from(“0”)): 这个部分非常关键,ReadOffset.from(“0”) 表示从流的最开始位置读取消息。通常情况下,XREADGROUP 会从 上次消费的位置(ReadOffset.lastConsumed())开始读取,但在 handlePendingList 中,它是从流的 最开始位置 读取消息,意味着它将读取所有 未确认的挂起消息

创建订单

private void createVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        Long voucherId = voucherOrder.getVoucherId();
        // 创建锁对象
        RLock redisLock = redissonClient.getLock("lock:order:" + userId);
        // 尝试获取锁
        boolean isLock = redisLock.tryLock();
        // 判断
        if (!isLock) {
            // 获取锁失败,直接返回失败或者重试
            log.error("不允许重复下单!");
            return;
        }

        try {
            // 5.1.查询订单
            int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
            // 5.2.判断是否存在
            if (count > 0) {
                // 用户已经购买过了
                log.error("不允许重复下单!");
                return;
            }

            // 6.扣减库存
            boolean success = seckillVoucherService.update()
                    .setSql("stock = stock - 1") // set stock = stock - 1
                    .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
                    .update();
            if (!success) {
                // 扣减失败
                log.error("库存不足!");
                return;
            }

            // 7.创建订单
            save(voucherOrder);
        } finally {
            // 释放锁
            redisLock.unlock();
        }
    }

g1 是消费者组的名字,表示一组消费者一起读取流中的消息。
c1 是消费者的名字,在消费者组 g1 中的一个消费者实例。每个消费者组可以包含多个消费者,且每个消费者负责读取流中的一部分数据。
消费者组和消费者的工作方式:
消费者组:Redis 的 Stream 使用消费者组来管理消息的消费。消费者组的目的是让多个消费者共享消息,但每个消息只被组内的一个消费者消费。
消费者:每个消费者会从消息流中获取消息,并进行处理。在你的代码中,c1 是 g1 消费者组中的一个消费者。c1 会读取流中的消息,处理订单,并确认消息。

签到

每位表示月的31天 是否签到,第0位表示当月的第一天
签到:
bitmap底层是string

// 4.获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
 // 5.写入Redis SETBIT key offset 1
 stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);

连续签到统计:

public Result signCount() {
        // 1.获取当前登录用户
        Long userId = UserHolder.getUser().getId();
        LocalDateTime now = LocalDateTime.now();
        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)
        );
        Long num = result.get(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);
    }

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值