重学redis系列之string(bitmap经典案例)

接上一篇 重学redis系列之string,本来这个案例原计划是在上一篇做的,结果由于篇幅问题挪到了本篇介绍,废话少说,开始进入正题。

回忆一下上一篇的需求:需要对我们app的用户做一个活跃度统计,我们app的用户量不算太大也不是很小(3000多w),需求提到需要统计任意时间段内的用户活跃度(一个用户在一定时间段内登录我们app的天数,一定时间段内登录天天数达到xx的用户数等等),要求要快速响应。上一篇我们分析了,通过传统的关系型数据库,例如mysql也可以处理这个需求,但是响应速度可能相对较慢,同时由于数据量较大,传统关系型数据库使用的空间会快速增加,而且随着数据量增加,数据库的响应速度会进一步变慢,因此,传统关系型数据库并不是最优选择。
有了上一节的基础之后,我们知道redis对位图操作的支持,可以精细的操作每一个位,每一个位由0或1表示,刚好我们的需求针对某一个用户来说一天只有登录了(1)和未登录(0)两总情况。一个字节有8位,可以表示一个用户8天是否登录的信息,一年按366天计算也只需要 366 / 8向上取整等于46个字节就可以存储一个用户一年的登录信息。3000W用户一年的登录信息只需要 30000000 * 46 / 1024 / 1024 / 1024 = 1.285G。1.3G空间储存3000w用户一年的登录数据,应该说已经做到了空间利用的最大化。redis储存1.3G数据还是蛮多的,但是我们完全可以通过hash(username)之类的方式把数据分布到不同的redis。数据存完了,统计方便吗?redis提供了大量的对应api来方便我们做一些或、与之类的逻辑运算。因此,类似需求选择redis即可以满足统计的及时性需求,还节省了资源,是类似需求的不二选择。说了这么多,直接上代码,看看怎么实现。
快速搭建一个springboot项目,引入相关jar:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

编写controller,模拟接收前端的用户登录,同时提供统计接口

package com.info.loginstatistics.controller;

import com.info.loginstatistics.dto.ResultCode;
import com.info.loginstatistics.service.StaticsService;
import com.info.loginstatistics.util.RedisUtil;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.text.ParseException;
import java.util.Date;


@RestController
public class IndexController {

    private final StaticsService staticsService;

    public IndexController(StaticsService staticsService) {
        this.staticsService = staticsService;
    }

    @GetMapping("/login")
    public ResultCode login(HttpServletRequest request) {
        String username = request.getParameter("username");
        if (StringUtils.isEmpty(username)) {
            return ResultCode.fail("登录用户用户名不能为空");
        }
        String date = request.getParameter("date");
        Date loginDate = null;
        if (!StringUtils.isEmpty(date)) {
            try {
                loginDate = DateUtils.parseDate(date, "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd");
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
        if (loginDate == null) {
            loginDate = new Date();
        }
        staticsService.login(username, loginDate);
        return ResultCode.success();
    }

    @GetMapping("/login-count")
    public ResultCode loginCount(HttpServletRequest request) {
        String username = request.getParameter("username");
        if (StringUtils.isEmpty(username)) {
            return ResultCode.fail("查询的用户名不能为空");
        }
        String startDate = request.getParameter("startDate");
        String endDate = request.getParameter("endDate");
        Date end = null;
        Date start = null;
        if (!StringUtils.isEmpty(startDate) && !StringUtils.isEmpty(endDate)) {

            try {
                start = DateUtils.parseDate(startDate, "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd");
                end = DateUtils.parseDate(endDate, "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd");
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
        long count;
        if (start != null && end != null) {
            count = staticsService.count(username, start, end);
        } else {
            count = staticsService.count(username);
        }
        return ResultCode.success(count);
    }
    
	@GetMapping("/active-user-count")
    public ResultCode activeUserCount(HttpServletRequest request) {
        String startDate = request.getParameter("startDate");
        String endDate = request.getParameter("endDate");
        return ResultCode.success(staticsService.activeUserCount(startDate, endDate));
    }
}

servie层实现具体的业务逻辑操作

package com.info.loginstatistics.service.impl;

import com.info.loginstatistics.service.StaticsService;
import com.info.loginstatistics.util.RedisUtil;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

/**
 * @date 2020/9/25 14:40
 */

@Service
public class StaticsServiceImpl implements StaticsService {

    private static final String REDIS_KEY_PREFIX = "count:";

    @Override
    public void login(String username, Date date) {
        LocalDateTime dateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
        int year = dateTime.getYear();
        int day = dateTime.getDayOfYear();
        String key = REDIS_KEY_PREFIX + username;
        RedisUtil.setBit(key, day - 1, true);
        String formatedDate = DateFormatUtils.format(date, "yyyy-MM-dd");
        // 便于后期统计用户一定时间段内的活跃度
        RedisUtil.setBit(REDIS_KEY_PREFIX + formatedDate, username.hashCode(), true);
    }

    @Override
    public long count(String key) {
        return RedisUtil.bitCount(REDIS_KEY_PREFIX + key);
    }

    @Override
    public long count(String key, Date startDate, Date endDate) {
        LocalDateTime startTime = LocalDateTime.ofInstant(startDate.toInstant(), ZoneId.systemDefault());
        LocalDateTime endTime = LocalDateTime.ofInstant(endDate.toInstant(), ZoneId.systemDefault());
        long dayOfYearStart = (long) startTime.getDayOfYear();
        long dayOfYearEnd = (long) endTime.getDayOfYear();
        return RedisUtil.bitCount(key, dayOfYearStart - 1, dayOfYearEnd - 1);
    }

    @Override
    public long count(String[] keys) {
        return RedisUtil.OpOrCount(keys);
    }

    @Override
    public long activeUserCount(String startDate, String endDate) {
        long count;
        if (StringUtils.isEmpty(startDate) && StringUtils.isEmpty(endDate)) {
            Set<String> keys = RedisUtil.getKeys(REDIS_KEY_PREFIX + "*");
            if (CollectionUtils.isEmpty(keys)) {
                return 0L;
            }
            count = RedisUtil.OpOrCount(keys.toArray(new String[keys.size()]));
        } else if (!StringUtils.isEmpty(startDate) && StringUtils.isEmpty(endDate)) {
            List<String> dateList = getBetweenDate(startDate, DateFormatUtils.format(new Date(), "yyyy-MM-dd"));
            count = RedisUtil.OpAndCount(dateList.toArray(new String[dateList.size()]));
        } else if (StringUtils.isEmpty(startDate) && !StringUtils.isEmpty(endDate)) {
            List<String> dateList = getBetweenDate(DateFormatUtils.format(getFirstDayOfYear(new Date()), "yyyy-MM-dd"), endDate);
            count = RedisUtil.OpAndCount(dateList.toArray(new String[dateList.size()]));
        } else {
            List<String> dateList = getBetweenDate(startDate, endDate);
            count = RedisUtil.OpAndCount(dateList.toArray(new String[dateList.size()]));
        }
        return count;
    }

    /**
     * 获取一定时间段内每一天的日期
     *
     * @param start
     * @param end
     * @return {@link List}
     * @date 2020/9/30 11:18
     */
    private static List<String> getBetweenDate(String start, String end) {
        List<String> list = new ArrayList<>();
        LocalDate startDate = LocalDate.parse(start);
        LocalDate endDate = LocalDate.parse(end);
        long distance = ChronoUnit.DAYS.between(startDate, endDate);
        if (distance < 1) {
            return list;
        }
        Stream.iterate(startDate, d -> d.plusDays(1)).limit(distance + 1).forEach(f -> list.add(f.toString()));
        return list;
    }

    /**
     * 获取某一年的第一天日期
     * @date 2020/9/30 12:31
     * @param date
     * @return {@link Date}
     */
    private static Date getFirstDayOfYear(Date date) {
        LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
        LocalDate localDate = localDateTime.withDayOfMonth(1).withDayOfYear(1).toLocalDate();
        return Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());

    }
}

另外封装了一个redis工具类,提供对bitmap的操作

package com.info.loginstatistics.util;

import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

    private static RedisTemplate<String, String> redisTemplate;

    public RedisUtil(RedisTemplate redisTemplate) {
        RedisUtil.redisTemplate = redisTemplate;
    }

    public static void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public static void set(String key, String value, long expire) {
        redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
    }

    public static String get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 设置key字段第offset位bit数值
     *
     * @param key    字段
     * @param offset 位置
     * @param value  数值
     */
    public static void setBit(String key, long offset, boolean value) {
        redisTemplate.execute((RedisCallback) con -> con.setBit(key.getBytes(), offset, value));
    }

    /**
     * 判断该key字段offset位否为1
     *
     * @param key    字段
     * @param offset 位置
     * @return 结果
     */
    public static boolean getBit(String key, long offset) {
        return (boolean) redisTemplate.execute((RedisCallback) con -> con.getBit(key.getBytes(), offset));

    }

    /**
     * 统计key字段value为1的总数
     *
     * @param key 字段
     * @return 总数
     */
    public static Long bitCount(String key) {
        return (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes()));
    }

    /**
     * 统计key字段value为1的总数,从start开始到end结束
     *
     * @param key   字段
     * @param start 起始
     * @param end   结束
     * @return 总数
     */
    public static Long bitCount(String key, Long start, Long end) {
        return (Long) redisTemplate.execute((RedisCallback) con -> con.bitCount(key.getBytes(), start, end));
    }

    /**
     * 取多个key并集并计算总数
     *
     * @param key key
     * @return 总数
     */
    public static Long OpOrCount(String... key) {
        byte[][] keys = new byte[key.length][];
        for (int i = 0; i < key.length; i++) {
            keys[i] = key[i].getBytes();
        }
        redisTemplate.execute((RedisCallback) con -> con.bitOp(RedisStringCommands.BitOperation.OR, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
        redisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
        return RedisUtil.bitCount(key[0] + "To" + key[key.length - 1]);
    }

    /**
     * 取多个key的交集并计算总数
     *
     * @param key key
     * @return 总数
     */
    public static Long OpAndCount(String... key) {
        byte[][] keys = new byte[key.length][];
        for (int i = 0; i < key.length; i++) {
            keys[i] = key[i].getBytes();
        }
        redisTemplate.execute((RedisCallback) con -> con.bitOp(RedisStringCommands.BitOperation.AND, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
        redisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
        return RedisUtil.bitCount(key[0] + "To" + key[key.length - 1]);
    }

    /**
     * 取多个key的补集并计算总数
     *
     * @param key key
     * @return 总数
     */
    public static Long OpXorCount(String... key) {
        byte[][] keys = new byte[key.length][];
        for (int i = 0; i < key.length; i++) {
            keys[i] = key[i].getBytes();
        }
        redisTemplate.execute((RedisCallback) con -> con.bitOp(RedisStringCommands.BitOperation.XOR, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
        redisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
        return RedisUtil.bitCount(key[0] + "To" + key[key.length - 1]);
    }

    /**
     * 取多个key的否集并计算总数
     *
     * @param key key
     * @return 总数
     */
    public static Long OpNotCount(String... key) {
        byte[][] keys = new byte[key.length][];
        for (int i = 0; i < key.length; i++) {
            keys[i] = key[i].getBytes();
        }
        redisTemplate.execute((RedisCallback) con -> con.bitOp(RedisStringCommands.BitOperation.NOT, (key[0] + "To" + key[key.length - 1]).getBytes(), keys));
        redisTemplate.expire(key[0] + "To" + key[key.length - 1], 10, TimeUnit.SECONDS);
        return RedisUtil.bitCount(key[0] + "To" + key[key.length - 1]);
    }
}

至此,主要的代码就是这些了,可以通过postman进行测试了。
模拟登录
在这里插入图片描述
统计用户的登录情况
在这里插入图片描述
今天的分享就先到这里了,有时间再做一些其余情况的测试,感谢大家的观看,如若有误,欢迎指正,谢谢!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不务正业的攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值