我的编程空间,编程开发者的网络收藏夹
学习永远不晚

黑马点评项目全面业务总结

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

黑马点评项目全面业务总结

1 黑马点评项目

1.1 短信登陆

1.1.1 短信登陆简介

session共享问题:多台服务器并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。

  • 在进行短信登录时,运用redis的String数据结构把手机号作为key,验证码作为value进行存储。
  • 查询用户获得用户信息后,运用redis的hash结构,用token当做key存储(token的意思是“令牌”,是服务器生成的一段加密字符串),用户信息作为一个一个hash存储。
    为什么不用String数据结构?
    Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD,并且内存占用更少。String是以JSON字符串来保存的,虽然比较直观,但是不能进行CRUD。

1.1.2 校验登录状态

主要逻辑:验证手机号格式 如果不符合,返回错误信息,如果符合生成验证码,就运用redis的String数据结构对手机号和验证码进行存储 redis存储一般有个公共前缀并且设置有效时间 一般两分钟 最后返回ok;

//Slfg4    日志注解    public Result sendCode(String phone, HttpSession session) {        //1:先验证手机号格式  不符合就返回错误信息        if(RegexUtils.isPhoneInvalid(phone)){            //2:如果不符合,返回错误信息            return Result.fail("手机号格式错误");        }            //4:生成验证码            String code = RandomUtil.randomNumbers(6);           //保存验证码到redis phone作为key 并且有一个公共前缀            stringRedisTemplate.opsForValue()            .set(RedisConstants.LOGIN_CODE_KEY +phone,code                    ,RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);            //6:发送验证码            log.debug("验证码是{}",code);            return Result.ok();    }

1.1.3短信验证码登录和注册

主要逻辑:前端给手机号,以手机号从redis里面拿出验证码,比较验证码,如果不等则返回错误"验证码不正确",如果相等看看是否被注册过,如果没有被注册过,需要插入数据,没注册过的话直接返回UserDto对象(注:UserDto对象相比User对象少了一些敏感信息,例如:密码),然后将对象存储到redis的hash结构里面,并设置有效期 一般为30分钟

  @Override    public Result login(LoginFormDTO loginForm, HttpSession session) {        //获取手机号        String phone = loginForm.getPhone();        if(RegexUtils.isPhoneInvalid(phone)){            //2:如果不符合,返回错误信息            return Result.fail("手机号格式错误");        }        //从reids中拿出验证码        String code = stringRedisTemplate.opsForValue()        .get(RedisConstants.LOGIN_CODE_KEY + phone);        if(loginForm.getPhone().equals(code)){            //如果验证码错误直接返回false            return Result.fail("验证码不正确");        }        //如果正确 在确定手机号是否已经被注册过        User user = query().eq("phone", phone).one();        //生成token  用hutool工具类生成的uuid toString(true)可以把uuid中的-去掉        String token = UUID.randomUUID().toString(true);        if(user==null){            //没有注册过新建并插入新数据            user=CreateNewUser(phone);        }        //hutool工具类 Beanutil        UserDTO userDTO= BeanUtil.copyProperties(user, UserDTO.class);        //运用redis中的map数据结构存储userDto对象        Map<String,String> map=new  HashMap<>();        map.put("id",userDTO.getId().toString());        map.put("nickName",userDTO.getNickName());        map.put("icon",userDTO.getIcon());        stringRedisTemplate.opsForHash()        .putAll(RedisConstants.LOGIN_USER_KEY+token,map);        //设置时间一般是30分钟不进行操作,就会失效        stringRedisTemplate        .expire(RedisConstants.LOGIN_USER_KEY+token,         RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);      return Result.ok(token);    }    private User CreateNewUser(String phone) {     User user=new User();     user.setPhone(phone);     user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(4));     save(user);     return  user;    }

1.1.4 拦截器的实现

主要逻辑:一个拦截器是更新拦截器,主要是更新token的有效期,一个拦截不合法的路径,更新拦截器首先获得token对象 如果token为空直接放行。若不为空的话token刷新token的有效期,然后用token从redis里面拿出UserDTO的map对象,然后把map对象转换为UserDTO对象,存入ThreadLocal域中。在拦截器执行之后将TheadLocal域中的对象释放掉,避免发生内存泄漏.一个拦截器只用判断ThreadLocal域中有没有UserDTO对象,如果有则放行,如果没有就拦截.

//更新拦截器 主要是更新token有效期 另外拦截器不是spring管理的bean //里面不能用自动注入注解 需要用构造方法public class RefreshInterceptor implements HandlerInterceptor {    private StringRedisTemplate stringRedisTemplate;    public RefreshInterceptor(StringRedisTemplate stringRedisTemplate) {        this.stringRedisTemplate = stringRedisTemplate;    }    @Override    public boolean preHandle(HttpServletRequest request,     HttpServletResponse response, Object handler) throws Exception {        //从对象头获得token        String token = request.getHeader("authorization");        if(StrUtil.isBlank(token)){            return true;        }        //若不为空放行,并且把用户放进TheadLocal并且把时间重置为30分钟        Map<Object, Object> map = stringRedisTemplate.opsForHash()                .entries(RedisConstants.LOGIN_USER_KEY+token);        if(map.isEmpty()){            return true;        }        //hutool工具类 将map转换为实体类对象        UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);        UserHolder.saveUser(userDTO);        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,                RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);        return true;    }    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {     //释放Thread中的user类 避免内存泄露        UserHolder.removeUser();    }}//目的是拦截不合法的路径public class LoginInterceptor implements HandlerInterceptor {    @Override    public boolean preHandle(HttpServletRequest request    , HttpServletResponse response, Object handler) throws Exception {        System.out.println("执行拦截器");        System.out.println(UserHolder.getUser());        if(UserHolder.getUser()==null){           //状态码401 表示没授权           response.setStatus(401);           return false;       }       return true;    }}

拦截器的配置

@Configurationpublic class MvcConfig implements WebMvcConfigurer {    @Resource    private StringRedisTemplate stringRedisTemplate;    @Override    //登录拦截器    public void addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(             "/shop            return proxy.createVoucherOrder(voucherId);        }    }    @Transactional    public Result createVoucherOrder(Long voucherId) {        //为什么不在方法里面加锁?        //因为 在释放锁之后事务spring才提交事务 释放锁         //还没提交的时候 可能另一个线程可能拿到锁        //线程不安全        Long userId= UserHolder.getUser().getId();        //实现一人一单        int count = query().eq("user_id", userId)        .eq("voucher_id", voucherId).count();        if(count>0){            return Result.fail("您已经购买到了");        }        //前面已经判断库存是否充足        //这次在判断 是一种乐观锁的机制         //运用cas机制 在进行库存加减的时候 需要再次进行判断 库存是否有        boolean success = seckillVoucherService.update()                .setSql("stock=stock-1")                .eq("voucher_id", voucherId)                //                .gt("stock",0)                .update();        if(!success){            return Result.fail("优惠卷已经被发放完");        }        VoucherOrder voucherOrder=new VoucherOrder();        long orderId = redisIdWorker.nextId("order");        voucherOrder.setId(orderId);        voucherOrder.setUserId(UserHolder.getUser().getId());        voucherOrder.setVoucherId(voucherId);        this.save(voucherOrder);        return Result.ok(orderId);    }

然而上述锁只能运用在单体项目中,如果在分布式项目上并不能起到一人一单功能,所以需要分布式锁
分布式锁:满足分布式系统或集群模式下多线程可见并且互斥的锁

  • 多进程可见
  • 高可用
  • 安全性
  • 互斥
  • 高性能 等等
private StringRedisTemplate stringRedisTemplate;    private String name;    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {        this.stringRedisTemplate = stringRedisTemplate;        this.name = name;    }    private static final String KEY_PREFIX="lock:";    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";  @Override     // 上锁    public boolean tryLock(long timeoutSec) {        //运用uuid和线程id生成 value        String threadId = ID_PREFIX+Thread.currentThread().getId();        //前缀和name组成 key        //设置过期时间 如果redis宕机后 锁还能等时间结束后释放  避免造成死锁        Boolean success = stringRedisTemplate        .opsForValue().setIfAbsent(KEY_PREFIX + name, threadId        ,timeoutSec, TimeUnit.SECONDS);        return BooleanUtil.isTrue(success);    }    @Override    public void unLock() {        //在释放锁的时候 如果线程一 拿到锁 但是进行阻塞 然后锁失效了         //线程二拿到锁 线程一在阻塞消失后 直接删除了线程二的锁         //解决方案 lua脚本        String threadID = ID_PREFIX+Thread.currentThread().getId();        String id1=stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);        if(threadID.equals(id1)){            stringRedisTemplate.delete(KEY_PREFIX+name);        }    }
--- lua脚本能保证代码执行的原子性if(redis.call('get',KEYS[1])==ARGV[1]) then    return redis.call('del',KEYS[1])endreturn 0
 private  static  final DefaultRedisScript<Long> UNLOCK_SCRIPT;    static {        //初始化        UNLOCK_SCRIPT=new DefaultRedisScript<>();        //设置脚本位置   classPath下的资源        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));        //设置返回值        UNLOCK_SCRIPT.setResultType(Long.class);    }   public void unLock() {         stringRedisTemplate.execute(UNLOCK_SCRIPT,                 Collections.singletonList(KEY_PREFIX+name),                 ID_PREFIX+Thread.currentThread().getId()              );    }

使用Redisson分布式锁

Redisson可重入锁的原理

Redisson的锁重试,和看门狗机制

异步秒杀思路

异步秒杀的主要流程:在秒杀的时候 判断库存是否充足 如果不充足 直接返回错误,如果是充足的话,将优惠卷id,用户id和订单id存入阻塞队列,另开线程进行数据库交互

-- lua脚本在判断库存是否充足时 是原子性的 避免产生线程问题-- 优惠卷idlocal voucherId=ARGV[1]-- 1.2 用户idlocal userId=ARGV[2]-- 2.数据key-- 2.1 库存key-- .. 字符串连接符local stockKey='seckill:stock:' .. voucherIdlocal orderKey='seckill:order:' .. voucherId--3脚本业务--3.1判断库存是否充足 get stockKeyif(tonumber(redis.call('get',stockKey))<=0) then    return 1endif(redis.call('sismember',orderKey,userId)==1) then    return 2end--3.4 扣库存redis.call('incrby',stockKey,-1)redis.call('sadd',orderKey,userId)return 0
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {     @Resource     private SeckillVoucherServiceImpl seckillVoucherService;     @Resource     private RedisIdWorker redisIdWorker;     @Resource     private StringRedisTemplate stringRedisTemplate;     @Resource     private RedissonClient redissonClient;     private  IVoucherOrderService proxy;         private BlockingQueue<VoucherOrder> orderTasks     =new ArrayBlockingQueue<>(1024*1024);    //线程池的创建      private static  final ExecutorService SECKILL_ORDER_EXECUTOR=      Executors.newSingleThreadExecutor();    private  static  final DefaultRedisScript<Long> SECKILL_SCRIPT;    static {        //初始化        SECKILL_SCRIPT=new DefaultRedisScript<>();        //设置脚本位置   classPath下的资源        SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));        //设置返回值        SECKILL_SCRIPT.setResultType(Long.class);    }    //spring的知识 目的是为了让 在类创建时 对这个方法进行初始话    @PostConstruct//spring注解    private  void init(){        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());    } @Override    public Result seckillVoucher(Long voucherId){        //执行lua脚本        Long userId = UserHolder.getUser().getId();        Long result = stringRedisTemplate.execute(                SECKILL_SCRIPT,                Collections.emptyList(),                voucherId.toString(), userId.toString()        );        int r=result.intValue();        //判断结果为0                if(r!=0){            return Result.fail(r==1?"库存不足":"不能重复下单");        }        // 保存阻塞队列 将新建的对象存入阻塞队列        VoucherOrder voucherOrder=new VoucherOrder();        long orderId = redisIdWorker.nextId("order");        voucherOrder.setId(orderId);        voucherOrder.setUserId(userId);        voucherOrder.setVoucherId(voucherId);        //不为0 代表没有购买职责        //处理spring事务失效问题        proxy=(IVoucherOrderService) AopContext.currentProxy();        //加入队列 开辟线程         orderTasks.add(voucherOrder);        //直接返回        return Result.ok(orderId);    }    private   class VoucherOrderHandler implements Runnable{        @Override        public void run() {            while(true){                try{//这个线程主要从阻塞队列拿出voucherOrder对象 一直循环                    VoucherOrder voucherOrder=orderTasks.take();                    handleVoucherOrder(voucherOrder);                }catch (Exception e){                    log.error("处理订单异常",e);                }            }        }    }    private  void handleVoucherOrder(VoucherOrder voucherOrder){        //不能在线程里面拿对象了 因为线程变了        Long userId=voucherOrder.getUserId();        //分布式锁 获取锁 双重保障        RLock lock = redissonClient.getLock("lock:order"+userId);        boolean b = lock.tryLock();        if(!b){            log.error("不允许重复下单");            return;        }        try{            //调用方法            proxy.createVoucherOrder(voucherOrder);        }finally {            lock.unlock();        }    }       @Transactional    public void createVoucherOrder(VoucherOrder voucherOrder) {        Long userId= voucherOrder.getUserId();        //实现一人一单        int count = query().eq("user_id", userId)        .eq("voucher_id",voucherOrder.getVoucherId()).count();        if(count>0){            log.error("用户已经购买过一次!");            return;        }        boolean success = seckillVoucherService.update()                .setSql("stock=stock-1")                .eq("voucher_id", voucherOrder.getVoucherId())                .gt("stock",0)                .update();        if(!success){            log.error("库存不足");        }        this.save(voucherOrder);    }}

1.4点赞功能实现

需求:

  • 同一个用户只能点赞一次,再次点击则取消点赞
  • 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)

实现步骤:

  • 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
  • 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,一点赞过则点赞-1
  @Override    public Result likeBlog(Long id) {        //获得登录信息        Long userId = UserHolder.getUser().getId();        //判断当前登录用户是否已经点赞        //往redis里面存入key是前缀加上博客id,value是用户id        Boolean member = stringRedisTemplate.opsForSet()        .isMember(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());        if(BooleanUtil.isFalse(member)){            //如果未点赞,可以点赞            //数据库点赞+1            boolean success = update().setSql("liked=liked+1")            .eq("id", id).update();            if(success) {      //保存用户到redis的set集合     //stringRedisTemplate.opsForZSet().add(RedisConstants.BLOG_LIKED_KEY + id,     //    userId.toString(),System.currentTimeMillis());存放在zet中                stringRedisTemplate.opsForSet()                .add(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());            }        }else{            //如果已点赞,取消点赞            //数据库点赞数-1            boolean success = update().setSql("liked=liked-1").eq("id", id).update();            if(success) {           //把用户从redis的set集合移除                stringRedisTemplate.opsForSet().remove(RedisConstants.BLOG_LIKED_KEY + id, userId.toString());            }        }        return Result.ok();    }//在每次查询的时候需要判断该用户是否已经点赞了这个博客    private void isBlogLiked(Blog blog){        // 获得这个用户        Long userId = UserHolder.getUser().getId();        //key前缀加博客id          String key=RedisConstants.BLOG_LIKED_KEY+blog.getId();        //查redis里面有没有数据 如果有 isLike返回假 如果没有 则返回真        Boolean isMember = stringRedisTemplate.opsForSet()        .isMember(key, userId.toString());        blog.setIsLike(BooleanUtil.isTrue(isMember));    }

1.4.1 点赞排行榜

在实现点赞排行榜时 不能用set集合做判断了 因为set是无序的 因此将set要改成zset 将时间戳存放到score中 实现排行

    @Override    public Result queryBlogLikes(Long id) {        //1 查询top5的点赞用户        String key=RedisConstants.BLOG_LIKED_KEY+id;        Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);        //解析出其中的用户id        if(top5==null||top5.isEmpty()){            return  Result.ok(Collections.emptyList());        }        //运用了jdk8中的新特性 有时间学学 将set集合中的String统统改为Long        List<Long> ids = top5.stream().map(Long::valueOf)        .collect(Collectors.toList());        //hutool下的string工具类        String idsStr = StrUtil.join(",", ids);        //用in不会根据根据自己的顺序进行排序        //需要用 select * from tb_user where in(5,1) order by field(id,5,1)        List<UserDTO> userDTOS = userService.query()                .in("id",ids)                .last("ORDER BY FIELD(id,"+idsStr+")").list()                .stream()                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))                .collect(Collectors.toList());        return Result.ok(userDTOS);    }

1.4.2 关注和取关功能实现

public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {    @Override    public Result follow(Long followUserId, Boolean isFollow) {        //获得登录用户        Long userId = UserHolder.getUser().getId();        //判断到底是关注还是取关        if(isFollow){            //直接往表里插入数据         Follow follow=new Follow();         follow.setUserId(userId);         follow.setFollowUserId(followUserId);         save(follow);        }else {            //删除数据            remove(new QueryWrapper<Follow>()            .eq("user_id", userId).eq("follow_user_id", followUserId));        }        //关注,新增取关        //取关,删除        return Result.ok();    }    @Override    public Result isFollow(Long followUserId) {        //查看有没有关注        Long userId = UserHolder.getUser().getId();        Integer count = query()        .eq("user_id", userId).eq("follow_user_id", followUserId).count();        return Result.ok(count>0);    }

1.4.2 共同关注

在开发共同关注时,需要将关注和取关功能的功能改善一下 需要将用户id的key和关注的用户的id作为value 放到set 因为set可以进行查找供同拥有的value

   @Override    public Result followCommons(Long id) {        //用户id        Long userId=UserHolder.getUser().getId();        //用户id key        String key=RedisConstants.FOLLOW+userId;        //查找另一个用户的id        String key1=RedisConstants.FOLLOW+id;        //进行value查重        Set<String> intersect = stringRedisTemplate        .opsForSet().intersect(key, key1);        if(intersect==null||intersect.isEmpty()){            return Result.ok(Collections.emptyList());        }        List<Long> ids= intersect.stream().map(Long::valueOf)        .collect(Collectors.toList());        List<UserDTO> collect = userService.listByIds(ids)                .stream()                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))                .collect(Collectors.toList());        return Result.ok(collect);    }

1.4.3 订阅好友关注

feed流

拉模式:大v都有自己的发件箱,当发消息的时候会发到自己的发件箱里面,等用户上线查看收件箱,会将用户关注的所有大v的发件箱复制一份放到用户的收件箱,重新按时间戳进行排序,供用户读取!弊端:如果这个人是个变态,关注着几千多个人,成千上万个数据会复制到收件箱,耗费内存

推模式:大v在写消息时,会将关注自己的所有人的收件箱里面写一份,供粉丝阅读。缺点:如果大v有太多粉丝,也会造成太耗费内存

拉推结合模式:对待僵尸粉采用拉模式,对待活跃粉丝采用推模式

本次实现使用的是推模式

//在发布笔记时,向各个用户的收件箱发送   运用的时有序zset集合@Override    public Result saveBlog(Blog blog) {        //获得登录用户/         UserDTO user = UserHolder.getUser();        blog.setUserId(user.getId());        boolean success = save(blog);        if(!success){            return Result.fail("新增笔记失败");        }        //查询该用户的所有粉丝         select * from tb_follow where follow_id ='user.getId()'        List<Follow> follows = followService.query()        .eq("follow_user_id", user.getId()).list();        //        long l = System.currentTimeMillis();        for(Follow follow:follows){            //对每个粉丝的收件箱进行推送            String key=RedisConstants.FEED_KEY+follow.getUserId();            stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),l);        }        return Result.ok(blog.getId());    }
//滚动查询的意思是 当查询的时候   本来是10条数据 分页 一页两条数据 如果这时候插入一条新数据 索引变化 分页会重复查询 数据 这时候需要滚动查询 在查询第二页数据时 记录最后一个数据的score数据 并记录 这一页score 有几个 然后根据这个score数据查第三页 @Override    public Result queryBlogOfFollow(Long max, Integer offset) {        //获得当前用户id        Long userId = UserHolder.getUser().getId();        //查询收件箱 key max最大值 min最小值 limit offset偏移量 count 分页数量        String key=RedisConstants.FEED_KEY+userId;         //value 是blogId score 就是分数        Set<ZSetOperations.TypedTuple<String>> typedTuples =             stringRedisTemplate.opsForZSet().            reverseRangeByScoreWithScores(key, 0, max, offset, 2);        //非空判断        if(typedTuples==null||typedTuples.isEmpty()){            return Result.ok();        }        //解析数据:blogId,minTime,offset        ArrayList<Long> ids = new ArrayList<>(typedTuples.size());        long minTime=0;        int os=1;        for(ZSetOperations.TypedTuple<String> tuple:typedTuples){            ids.add(Long.valueOf(tuple.getValue()));            long time =tuple.getScore().longValue();            if(time==minTime){                os++;            }else{                minTime=time;                os=1;            }        }        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);        }        ScrollResult r=new ScrollResult();        r.setList(blogs);        r.setOffset(os);        r.setMinTime(minTime);        return Result.ok(r);    }

1.5用户签到

  @Override    public Result sign() {        //获取当前用户        Long userId = UserHolder.getUser().getId();        //获取日期        LocalDateTime now=LocalDateTime.now();        String keySuffix=now.format(DateTimeFormatter.ofPattern(":yyyyMM"));        //拼接key        String key=RedisConstants.USER_SIGN_KEY+userId+keySuffix;        //获得今天是本月第几天        int dayOfMonth = now.getDayOfMonth();        //写入redis        stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);        return Result.ok();    }

1.5.1 查询连续签到了几天

 @Override    public Result signCount() {        //获取当前用户        Long userId = UserHolder.getUser().getId();        //获取日期        LocalDateTime now=LocalDateTime.now();        String keySuffix=now.format(DateTimeFormatter.ofPattern(":yyyyMM"));        //拼接key        String key=RedisConstants.USER_SIGN_KEY+userId+keySuffix;        //获得今天是本月第几天        int dayOfMonth = now.getDayOfMonth();        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);        }        //循环遍历        int count=0;        while(true){            //让这个数字与1做与运算,得到数字的最后的一个bit            if((num&1)==0){                //如果为0,说明未签到 ,结束                break;            }else{                //如果不为0,说明已签到,计数器+1                count++;            }            //把数字右移一位,抛给最后的比特位,继续下一个bit位            num>>>=1;        }        return Result.ok(count);    }

总结

jdk8的新特性学一下 spring再重新学一下

来源地址:https://blog.csdn.net/m0_57710472/article/details/127985653

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

黑马点评项目全面业务总结

下载Word文档到电脑,方便收藏和打印~

下载Word文档

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录