文章目录
关键代码
用户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共享的问题了
- 根据phone从redis获取验证码并校验,不存在创建用户;
- 为该用户生成一个唯一的 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. 总结
异步任务的体现:
- 用户的点赞请求异步写入 Redis(线程池或消息队列)。 Redis 中的数据定期由异步任务批量同步到 MySQL。减少了高频请求对数据库的压力。
- 使用
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流
推模式没有写邮箱,只有收件箱
推模式
- 推送到邮箱
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());
}
- 从邮箱获取
将时间戳作为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
主要功能:
- 参数接收:通过 ARGV 参数接收优惠券 ID(voucherId)、用户 ID(userId)和订单 ID(orderId)。
即Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString(), String.valueOf(orderId)
);的参数 - 库存判断:使用 Redis 的 get 命令检查库存是否充足。如果库存为 0 或更少,返回 1 表示库存不足。
- 重复下单检查:使用 sismember 检查用户是否已经下单。如果是,返回 2 表示重复下单。//SISMEMBER 是 Redis 的一个原生命令,用于检查一个元素是否存在于 Redis 的 集合(set)中。
- 扣除库存:通过 incrby 命令扣减库存,每次秒杀成功后,库存减 1。
- 记录订单:使用 sadd 将用户 ID 加入到 orderKey 中,表示该用户已成功下单。
- 发送消息到队列:使用 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);
}