基于Redis实现共享Session登陆

  1. 对于将用户的信息保存到Redis中的操作 String结构不方便,如果需要修改单个字段需要改整个json序列
  • 这里使用Hash结构

保存手机信息

//        session.setAttribute("code",code);  
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);

保存用户信息

//        //保存到session  
//        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));  
  
        //保存用户信息到Redis中  
        //生成随机token作为登陆令牌,将user对象转换为Hash存储  
        String token = UUID.randomUUID().toString(true);  
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);  
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);  
  
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);  
        stringRedisTemplate.expire(LOGIN_USER_KEY,LOGIN_USER_TTL,TimeUnit.MINUTES);
  
    @Override  
    public Result login(LoginFormDTO loginForm, HttpSession session) {  
  
        //校验手机号并看用户是否存在  
        String phone = loginForm.getPhone();  
        if (RegexUtils.isPhoneInvalid(phone)) {  
            return Result.fail("手机格式错误");  
        }  
//        //从session中获取验证码  
//        Object cacheCode = session.getAttribute("code");  
  
        //改为redis  
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);  
        String code = loginForm.getCode();  
        if(cacheCode==null || !cacheCode.equals(code)){  
            return Result.fail("验证码错误");  
        }  
        User user = query().eq("phone",phone).one();  
        if(user==null){  
            user = createUserByPhone(phone);  
        }  
//        //保存到session  
//        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));  
  
        //保存用户信息到Redis中  
        //生成随机token作为登陆令牌,将user对象转换为Hash存储  
        String token = UUID.randomUUID().toString(true);  
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);  
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);  
          
        // 确保所有值都是String类型,避免StringRedisTemplate序列化时出错  
        Map<String, String> stringUserMap = new HashMap<>();  
        for (Map.Entry<String, Object> entry : userMap.entrySet()) {  
            stringUserMap.put(entry.getKey(), entry.getValue().toString());  
        }  
  
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token, stringUserMap);  
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);  
  
        return Result.ok(token);

拦截器

LoginInterceptor.java中注入private StringRedisTemplate stringRedisTemplate;时,因为没有@Compoment等注解,需要手动创建构造函数,通过MvcConfig.java中的入口传入 stringRedisTemplate 于此同时,需要在拦截器的prehandle中获取redis中的tokne并与网络请求中的token做比较来决定是否放行 并将用户信息存入线程,刷新token的有效期

@Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
  
//        //获取session判断是否拦截,如果存在,保存用户信息到treadLocal  
//        HttpSession session = request.getSession();  
//        Object user = session.getAttribute("user");  
        String token = request.getHeader("authorization");  
        if(StrUtil.isBlank(token)){  
            response.setStatus(401);  
            return false;  
        }  
        //基于token获取Redis数据  
  
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);  
  
        if(userMap.isEmpty()){  
            //拦截  
            response.setStatus(401);  
            return false;  
        }  
        //重新转换为DTO  
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);  
        //保存用户到线程  
        UserHolder.saveUser(userDTO);  
  
//        UserHolder.saveUser((UserDTO) user);  
        //刷新token有效期  
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);  
        return true;  
    }

拦截器的优化

因为token的刷新可能会失效,所以在外嵌套一个拦截器,保证刷新token有效期

在多个拦截器的情况下,默认是按添加顺序生效,也可以设置order字段来手动确定

@Configuration  
public class MvcConfig implements WebMvcConfigurer {  
  
    @Resource  
    private StringRedisTemplate stringRedisTemplate;  
  
    @Override  
    public void addInterceptors(InterceptorRegistry registry) {  
  
        registry.addInterceptor(new LoginInterceptor())  
                .excludePathPatterns(  
                        "/user/code",  
                        "/blog/hot",  
                        "/shop?**",  
                        "/shop-type/**",  
                        "/voucher/**",  
                        "/user/login",  
                        "/upload/**"  
                ).order(1);  
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);//默认拦截所有路径  
  
    }  
}

商品缓存

添加缓存

缓存更新

这里主要难点是第三种:主动更新

主动更新策略

线程安全问题(CacheAsidePattern模式选择)

  • 方案2的概率会比1低非常多,因为缓存的写是微秒级别.

三种经典问题

缓存穿透

  • 缓存空对象
  • 布隆过滤
    • 增强id的复杂度,避免被猜测id规律
    • 做好数据的基础格式校验
    • 加强用户权限校验
    • 做好热点参数的限流

缓存雪崩

  • 添加随机TTL
  • Redis集群
  • 给缓存业务加降级限流策略
  • 给业务添加多级缓存

(SpringCloud微服务知识)

缓存击穿(热点key问题)

  • 不要有无数个请求过来!

  • 互斥锁
  • 逻辑过期

互斥锁模式
private boolean tryLock(String key){  
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);  
    return BooleanUtil.isTrue(flag);  
}  
private void unlock(String key){  
    stringRedisTemplate.delete(key);  
}
public Shop queryWithMutex(Long id){  
    //根据redis查询是否存在商铺缓存,来决定是否访问数据库和回写操作  
    String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);  
  
    if(StrUtil.isNotBlank(shopJson)){  
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);  
        return shop;  
    }  
    //判断命中的是否为空字符串(isNotBlank只要非数据就会true)  
    if(shopJson!=null){  
        return null;  
    }  
  
    //开始实现缓存重建  
    //1.获取互斥锁  
    String lockKey = "lock:shop:"+id;  
    Shop shop = null;  
    try {  
        boolean isLock = tryLock(lockKey);  
        //2.判断是否成功  
        //3.失败则休眠并重试  
        if(!isLock){  
            Thread.sleep(50);  
            return queryWithMutex(id);  
        }  
        //4.成功,根据id查询数据库  
        shop = getById(id);  
        //5.不存在,返回失败  
        if(shop==null){  
            stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);  
            return null;  
        }  
        //6.存在,存入redis  
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    }finally {  
        unlock(lockKey);  
    }  
    //7.释放互斥锁  
    return shop;  
}
逻辑过期方式

  • 先封装一个存储shop信息与过期时间到redis的函数(RedisData为自定义的@Data对象)

  • 难点1:从redisData(自定义Data)中取出时间和对象,对象是需要先从String转为RedisData

  • 再从RedisData在转出时间和对象,其中对象需要从object转为需要的对象(通过JSONUtil.toBean)

  • 创建线程池

缓存工具封装

待完成

优惠券秒杀

全局ID生成器

  
@Component  
public class RedisIdWorker {  
    private static final long BEGIN_TIMESTAMP = 1735689600L;  
    private final StringRedisTemplate stringRedisTemplate;  
  
    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {  
        this.stringRedisTemplate = stringRedisTemplate;  
    }  
  
    public Long nextId(String keyPrefix){  
        //生成时间戳  
        LocalDateTime nowTime = LocalDateTime.now();  
        long nowSecond = nowTime.toEpochSecond(ZoneOffset.UTC);  
        long timeStamp = nowSecond-BEGIN_TIMESTAMP;  
  
        String data = nowTime.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));  
        long incrementCount = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + data);  
        return timeStamp<<32| incrementCount;  
    }  
  
    public static void main(String[] agrs){  
        LocalDateTime time = LocalDateTime.of(2025,1,1,0,0,0);  
        long second = time.toEpochSecond(ZoneOffset.UTC);  
        System.out.println("second = " + second);  
    }  
  
}

业务逻辑

  • 单纯逻辑(无解决并发问题)
@Service  
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {  
  
    @Resource  
    private ISeckillVoucherService seckillVoucherService;  
    @Resource  
    private RedisIdWorker redisIdWorker;  
  
    @Override  
    @Transactional    public Result seckillVoucher(Long voucherId) {  
  
        //查询优惠券,判断秒杀是否开始  
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);  
        if(voucher.getBeginTime().isAfter(LocalDateTime.now())){  
            return Result.fail("秒杀尚未开始");  
        }  
        if(voucher.getEndTime().isBefore(LocalDateTime.now())){  
            return Result.fail("秒杀已经结束");  
        }  
  
        //是,判断库存是否充足  
        if( voucher.getStock()<1){  
            return Result.fail("库存不足!");  
        }  
        boolean success = seckillVoucherService.update()  
                .setSql("stock = stock -1")  
                .eq("voucher_id",voucherId).update();  
        if(!success){  
            return Result.fail("库存不足1");  
        }  
  
        //创建订单  
        VoucherOrder voucherOrder = new VoucherOrder();  
        Long id = redisIdWorker.nextId("order");  
        voucherOrder.setId(id);  
        Long userId = UserHolder.getUser().getId();  
        voucherOrder.setUserId(userId);  
        voucherOrder.setVoucherId(voucherId);  
        save(voucherOrder);  
        return Result.ok(id);  
    }  
}

超卖问题

弊端:

  • 性能太低

乐观锁

这里的id也就是有人说的版本号

弊端

  • 失败率高
boolean success = seckillVoucherService.update()  
        .setSql("stock = stock -1")//set  
        .eq("voucher_id",voucherId).gt("stock",0)//where  
        .update();  
if(!success){  
    return Result.fail("库存不足1");  
}

一人一单

事务可能失效的情况

  • 拿到代理对象
  • 需要配置pom和注解
synchronized (userId1.toString().intern()) {  
    IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();  
    return proxy.createVoucherOrder(voucherId);

集群模式下的并发问题

分布式锁

满足分布式系统或集群模式下多进程可见并且互斥的锁

  • 多进程可见
  • 互斥
  • 高可用
  • 高性能
  • 安全性

基于Redis的分布式锁

优化: 在获取锁的时候添加标识,在释放🔒的时候确认是不是自己的锁

Lua脚本

  • 保证redis命令的原子性

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;  
static {  
    UNLOCK_SCRIPT = new DefaultRedisScript<>();  
    UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));  
    UNLOCK_SCRIPT.setResultType(Long.class);  
}
 
    @Override  
    public void unlock(){  
        stringRedisTemplate.execute(  
                UNLOCK_SCRIPT,  
                Collections.singletonList(KEY_PREFIX+name),  
                ID_PREFIX+Thread.currentThread().getId()  
                );  
    }  
//  
//    @Override  
//    public void unlock() {  
//        //获取线程标识  
//        String threadId =ID_PREFIX+Thread.currentThread().getId();  
//        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);  
//        if(threadId.equals(id)){  
//            stringRedisTemplate.delete(KEY_PREFIX+name);  
//        }  
//  
//    }

Redisson

快速入门

可重入锁原理

  • 类似读者写者问题,最后一个人签到完后释放锁
  • 使用Lua脚本实现
  • 已实现,只需了解原理

源码阅读(是怎么解决开头的四个问题的),未完成

主从一致性问题

总结

Redis秒杀优化

多加一层Redis负责前置判断,再开线程见库存

思路实现

  • 将库存信息保存到Redis(string),将用户信息保存到Reids (set)中

  
---参数列表  
local voucherId = ARGV[1]  
local userId = ARGV[2]  
---数据key  
local stockKey = 'seckill:stock:'..voucherId  
local orderKey = 'seckill:order:'..voucherId  
  
---脚本业务  
local stock = redis.call('get', stockKey)  
if(not stock or tonumber(stock) == nil or tonumber(stock) <= 0) then  
    return 1  
end  
if(redis.call('sismember',orderKey,userId)==1) then  
    return 2  
end  
---扣库存,保存用户  
redis.call('incrby',stockKey,-1)  
redis.call('sadd',orderKey,userId)  
return 0
  • 异步下单(写到数据库的操作)未完成后
  • 线程池,阻塞队列

阻塞队列

private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);
 
orderTasks.add(VoucherOrder);

线程池

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 {  
                    VoucherOrder voucherOrder = orderTasks.take();  
                    handleVocherOrder(voucherOrder);  
                } catch (Exception e) {  
                    log.error("订单处理异常",e);  
                }  
            }  
        }  
  
        private void handleVocherOrder(VoucherOrder voucherOrder) {  
            //使用分布式锁  
//        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId1, stringRedisTemplate);  
            Long userId = voucherOrder.getVoucherId();  
            RLock lock = redissonClient.getLock("lock:order:" + userId);  
        boolean isLock = lock.tryLock();  
        if(!isLock){  
            //获取锁失败🔒  
            log.error("不许重复下单");  
            return ;  
        }  
        try {  
            proxy.createVoucherOrder(voucherOrder);  
        } catch (IllegalStateException e) {  
            throw new RuntimeException(e);  
        } finally {  
            lock.unlock();  
        }  
        }  
  
    }  
  
    private IVoucherOrderService proxy;  
    @Override  
    public Result seckillVoucher(Long voucherId) {  
  
        //1.执行lua脚本,根据返回值判断是否有购买资格并保存订单  
        Long userId = UserHolder.getUser().getId();  
  
        Long result = stringRedisTemplate.execute(  
                SECKILL_SCRIPT,  
                Collections.emptyList(),  
                voucherId.toString(),userId.toString()  
        );  
        int r = result.intValue();  
        if(r!=0){  
            return Result.fail(r==1? "库存不足":"不能重复下单");  
        }  
  
        //将下单信息保存在阻塞队列中  
        long orderId = redisIdWorker.nextId("orderId");  
  
        //创建订单  
        VoucherOrder voucherOrder = new VoucherOrder();  
        Long id = redisIdWorker.nextId("order");  
        voucherOrder.setId(id);  
        voucherOrder.setUserId(userId);  
        voucherOrder.setVoucherId(voucherId);  
  
        orderTasks.add(VoucherOrder);  
  
        proxy = (IVoucherOrderService)AopContext.currentProxy();  
  
  
  
        return Result.ok(orderId);
    }

Redis消息队列异步秒杀

基于List结构模拟

BRPOP🆚BLPOP

优点:

  • 基于Redis存储,不受限于JVM内存上限
  • 基于Redis的持久化机制,数据安全有保障
  • 满足消息有序性 缺点:
  • 无法避免消息丢失(Redis服务挂掉)
  • 只支持单消费者

基于PubSub

优点:

  • 多生产,多消费

缺点:

  • 不支持数据持久化
  • 无法避免消息丢失
  • 消息堆积有上限,超出时数据丢失
  • (你比jvm还拉啊)

基于String

单消费者模式下 (XREAD)特点:

  • 消息可回溯
  • 一个消息可被多个消费者读取
  • 可以阻塞读取
  • 但有消息漏读风险
消费者组

消费者组模式下 (XREADGROUP)特点:

  • 消息可回溯
  • 一个消息可被多个消费者读取
  • 多消费者可以争抢消息,加快消费速度(纯乞丐)
  • 可以阻塞读取
  • 消息漏读风险
  • 消息确认机制,保证至少被消费一次
  • 生产者出问题就玩大单了,只能解决消费者问题

总结

达人探店

发布探店笔记

基于Redis点赞功能

点赞排行榜

  • 将原本的string变为zset(ShortedSet)

  • 解决id不一致的问题

  • 类型转换问题

  • 将opsForZSet中的返回的Set<String>提取用户ids

  • 自定义的MyBatisPuls语句

 
String key = "blog:user:"+id;  
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);  
if(top5==null||top5.isEmpty()){  
    return Result.ok(Collections.emptyList());  
}
 
//解析其中的用户id  
//        将 top5 集合中的每个元素通过 Long.valueOf 转换为 Long 类型。  
//        将转换后的 Long 类型元素收集到一个新的 List 中,并将这个 List 赋值给变量 ids。  
        List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());  
  
  
        String idStr = StrUtil.join(",",ids);  
        List<UserDTO> userDTOS = userService.query()  
                .in("id",ids).last("Order by field(id,"+idStr+")").list()  
//        List<UserDTO> userDTOS = userService.listByIds(ids)  
                .stream()  
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))  
                .collect(Collectors.toList());

好友关注功能

实现共同关注

  • 使用set集合来实现交集

关注推送(Feed流)

拉模式(读扩散)

推模式(写扩散)

推拉混合

因为List结构的Redis不支持游动角标,这里使用Sorted-set

  • 滑动窗口算法?

redis中的分页查询

 
@Override  
public Result queryBlogOfFollow(Long max, Integer offset) {  
  
    Long userId = UserHolder.getUser().getId();  
    String key = "feed:"+userId;  
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()  
            .reverseRangeByScoreWithScores(key, 0, max, offset, 2);  
  
    if(typedTuples==null||typedTuples.isEmpty()){  
        return Result.ok();  
    }  
  
    List<Long> ids = new ArrayList<>(typedTuples.size());  
    long minTime = 0;  
    int os =1;  
    for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {  
        String value = typedTuple.getValue();  
        long score = typedTuple.getScore().longValue();  
        ids.add(Long.valueOf(value));  
  
        if(score==minTime){  
            os++;  
        }else {  
            minTime=score;  
            os=1;  
        }  
  
    }  
    String idsStr = StrUtil.join(",", ids);  
    List<Blog> blogs = query().in("id", ids).last("order by field(id," + idsStr + ")").list();  
  
    for (Blog blog : blogs) {  
        User user = userService.getById(userId);  
        blog.setName(user.getNickName());  
        blog.setIcon(user.getIcon());  
        //查询bolg是否被点赞  
        Double score = stringRedisTemplate.opsForZSet().score("blog:user:" + blog.getId(), UserHolder.getUser().getId().toString());  
        blog.setIsLike(score!=null);  
    }  
  
    ScrollResult r = new ScrollResult();  
    r.setList(blogs);  
    r.setOffset(os);  
    r.setMinTime(minTime);  
  
    return Result.ok(r);  
}

附近商户(GEO)

GEO数据结构

导入GEO数据

@Test  
void loadShopData(){  
    List<Shop> shopList = shopService.list();  
    Map<Long, List<Shop>> map = shopList.stream().collect(Collectors.groupingBy(Shop::getTypeId));  
    for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {  
        Long typeId = entry.getKey();  
        String key = "shop:geo:"+typeId;  
  
        List<Shop> value = entry.getValue();  
        List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>();  
  
        for (Shop shop : value) {  
            locations.add(  
                    new RedisGeoCommands.GeoLocation<>(  
                            shop.getId().toString(),  
                            new Point(shop.getX(),shop.getY())  
                    )  
            );  
        }  
        stringRedisTemplate.opsForGeo().add(key,locations);  
    }  
}

查询GEO数据

 
@Override  
public Result queryByType(Integer typeId, Integer current, Double x, Double y) {  
  
    //根据是否带有xy坐标参数来决定在数据库中查还是redis中查  
    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());  
    }  
    //计算分页参数,查处redis中的shopId,返回shop  
    int from = (current-1)*SystemConstants.DEFAULT_PAGE_SIZE;  
    int end = current*SystemConstants.DEFAULT_PAGE_SIZE;  
    String key = "shop:geo:"+typeId;  
    GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()  
            .search(  
                    key,  
                    GeoReference.fromCoordinate(x, y),  
                    new Distance(5000),  
                    RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(end)  
            );  
    if(results == null){  
        return Result.ok(Collections.emptyList());  
    }  
  
    List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();  
    if (list == null || list.isEmpty()) {  
        return Result.ok(Collections.emptyList());  
    }  
      
    List<Long> ids = new ArrayList<>(list.size());  
    Map<String,Distance> distanceMap = new HashMap<>(list.size());  
    list.stream().skip(from).forEach(result->{  
        //从list里获取店铺id和距离  
        String shopIdStr = result.getContent().getName();  
        ids.add(Long.valueOf(shopIdStr));  
        distanceMap.put(shopIdStr,result.getDistance());  
    });  
      
    if (ids.isEmpty()) {  
        return Result.ok(Collections.emptyList());  
    }  
      
    //根据id查shop  
    String idStr = StrUtil.join(",",ids);  
    List<Shop> shops = query().in("id", ids).last("order by field(id," + idStr + ")").list();  
    for (Shop shop : shops) {  
        shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());  
    }  
    return Result.ok(shops);

签到

BitMap用法

统计签到

@Override  
public Result signcount() {  
  
    Long userId = UserHolder.getUser().getId();  
    LocalDateTime now = LocalDateTime.now();  
    String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));  
    String key = "sign:"+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){  
        if((num&1)==0){  
            break;  
        }else {  
            count++;  
        }  
        num >>>=1;  
    }  
    return Result.ok(count);  
}

UV统计