Redis 高级特性实战学习

270 阅读5分钟

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战

一、前言

使用 jedis,测试环境:

  1. pom.xml 配置
  2. 测试代码

  1. pom.xml
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.5.2</version>
</dependency>
  1. 测试代码
public class JedisTest {
​
    private Jedis jedis;
​
    @Before
    public void setUp() {
​
        jedis = new Jedis("127.0.0.1", 6379);
    }
​
    @After
    public void after() {
​
        jedis.close();
    }
​
    @Test
    public void testCache() {
​
        // 最简单的设置缓存
        jedis.set("key1", "value1");
​
        System.out.println(jedis.get("key1"));
    }
}

二、实战

(1)HyperLogLog

hyperloglog:是由数据结构 + 概率算法组合而成的,用于去重统计,近似数。

可以用于网站数据分析, PVUV等,没必要过于精确。

举个栗子:统计 UV

如果基于 set 来计数,太耗费内存了,基于 HyperLogLog 算法来计数,是近似计数,有0.8%的误差,但是误差不会太大,可以给出一个相对准确的近似计数,而且就占用 12kb 的内存。

实战命令如下:

  • pfadd key :可以对数据进行计数,如果计算过这个元素,就返回0,没计算过就返回1,他也是可以去重的
  • pfcount key:可以获取计数结果

1)基于 HyperLogLog 的网站 UV 统计程序

实现如下:

public class JedisTest {
    /**
     * 初始化 UV 数据
     */
    private void initUVData() {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String today = dateFormat.format(new Date());
​
        for (int i = 0; i < 1358; ++i) {
            jedis.pfadd("hyperloglog_uv_" + today, String.valueOf(i + 1));
        }
    }
​
    private long getUV2() {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String today = dateFormat.format(new Date());
​
        return jedis.pfcount("hyperloglog_uv_" + today);
    }
​
    @Test
    public void testUV2() {
        initUVData();
        long uv = getUV2();
        System.out.println("今天 UV 的值是:" + uv);
    }
}

输出结果:

今天 UV 的值是:1366

2)网站重复垃圾数据的快速去重和过滤

对于你的一些网站,垃圾数据,多的是一些不良广告,在一些论坛或者别的,有的时候你会看到一些在帖子下面的评论里,是一些广告,都有,可能会遇到有人频繁的提交相同的垃圾信息到你的站点里。

实战命令如下:

  • pfadd key content:如果返回的是1,那么说明之前没见过这条数据,如果返回的是0,说明之前见过这条数据了

实现如下:

public class JedisTest {
    /**
     * 判断当强内容是否是垃圾内容
     *
     * @param content 内容
     * @return 是否
     */
    private Boolean isGarbageContent(String content) {
        return jedis.pfadd("hyperloglog_content", content) == 0;
    }
​
    @Test
    public void testGarbageContent() {
        String content = "正常的内容";
        System.out.println("是否为垃圾内容: " + (isGarbageContent(content) ? "是" : "否"));
​
        content = "垃圾内容";
        System.out.println("是否为垃圾内容: " + (isGarbageContent(content) ? "是" : "否"));
​
        content = "垃圾内容";
        System.out.println("是否为垃圾内容: " + (isGarbageContent(content) ? "是" : "否"));
    }
}

输出结果:

是否为垃圾内容: 否
是否为垃圾内容: 否
是否为垃圾内容: 是

3)周活跃用户数、月活跃用户数、年活跃用户数的统计

实战命令如下:

  • pfmerge:统计合并多个 hyperloglog,进行统一的去重,可以算出来周、月和年的活跃用户数。

实现如下:

public class JedisTest {
    /**
     * 初始化 UV 数据
     * @param date 日期
     */
    private void initUVData3(String date) {
        Random random = new Random();
        int startIndex = random.nextInt(1000);
        System.out.println("今日访问uv起始id为:" + startIndex);
​
        for (int i = startIndex; i < startIndex + 1358; ++i) {
            for (int j = 0; j < 10; ++j) {
                jedis.pfadd("hyperloglog_uv_" + date, String.valueOf((i + 1)));
            }
        }
    }
​
    private long getUV3(String date) {
​
        return jedis.pfcount("hyperloglog_uv_" + date);
    }
​
    private long getWeeklyUV() {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
​
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
​
        List<String> keys = new ArrayList<>();
​
        for (int i = 0; i < 7; ++i) {
            calendar.add(Calendar.DAY_OF_YEAR, 1);
            String date = dateFormat.format(calendar.getTime());
            keys.add("hyperloglog_uv_" + date);
        }
​
        String [] keyArray = keys.toArray(new String[keys.size()]);
​
        jedis.pfmerge("weekly_uv", keyArray);
​
        return jedis.pfcount("weekly_uv");
    }
​
    @Test
    public void testUV3() {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
​
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
​
        for (int i = 0; i < 7; ++i) {
            calendar.add(Calendar.DAY_OF_YEAR, 1);
            String date = dateFormat.format(calendar.getTime());
            initUVData3(date);
            System.out.println("日期为:" + date + "的uv值为:" + getUV3(date));
        }
​
        long weeklyUV = getWeeklyUV();
        System.out.println("实际的周活跃用户数为:" + weeklyUV);
    }
}

输出结果:

今日访问uv起始id为:133
日期为:2021-08-11的uv值为:1365
今日访问uv起始id为:47
日期为:2021-08-12的uv值为:1364
今日访问uv起始id为:362
日期为:2021-08-13的uv值为:1360
今日访问uv起始id为:906
日期为:2021-08-14的uv值为:1360
今日访问uv起始id为:893
日期为:2021-08-15的uv值为:1361
今日访问uv起始id为:196
日期为:2021-08-16的uv值为:1364
今日访问uv起始id为:714
日期为:2021-08-17的uv值为:1359
实际的周活跃用户数为:2217

(2)bitmap 位图

如果说你要记录一下,在系统 里执行一些特殊的操作,每天执行过某个操作的用户有多少个人,操作日志,审计日志,记录下来每个用户每天做了哪些操作,每个用户每天搞一个 set,里面放他做的一些操作日志,也可以对每种操作都搞一个 set,里面放执行过这些操作的用户。

bitmap位图:二进制里的一位一位的。

可以把网站里每一种操作每天执行过的用户都放在一个位图里,一个用户仅仅对应了一位而已。

主要命令:

  • setbit key user_id 1:设置值
  • getbit key user_id:获取值
  • bitcount key:统计

一个位图统计100万用户的行为,也不过一百多kb的内存

实现如下:

public class JedisTest {
    /**
     * 记录用户的操作日志
     * @param operation 操作
     * @param userId 用户Id
     */
    private void recordUserOperationLog(String operation, long userId) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String today = dateFormat.format(new Date());
​
        jedis.setbit("operation::" + operation + "::" + today + "::log", userId, String.valueOf(1));
    }
​
    private Boolean hasOperated(String operation, long userId) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String today = dateFormat.format(new Date());
        return jedis.getbit("operation::" + operation + "::" + today + "::log", userId);
    }
​
    @Test
    public void testBit() {
        recordUserOperationLog("操作1", 110);
        System.out.println("用户110是否执行过操作:" + (hasOperated("操作1", 110) ? "是" : "否"));
        System.out.println("用户111是否执行过操作:" + (hasOperated("操作1", 111) ? "是" : "否"));
    }
}

输出结果:

用户110是否执行过操作:是
用户111是否执行过操作:否

(3)GeoHash

1)基于 GeoHash 计算你与商铺距离

redis 里还有一种特殊的数据结构,叫做 Geo,可以让你在里面添加很多的地理位置,每个位置对应一个名称和一个经纬度,然后可以利用这个数据结构计算各种距离的位置,获取方圆半径多少1公里内的店铺。

主要命令如下:

  • geoadd key longitude latitude:添加地理位置
  • geodist key user shop unit='km':获取 usershop 距离

实现如下:

public class JedisTest {
    /**
     * 添加地理位置
     *
     * @param name 名称
     * @param longitude 经度
     * @param latitude 纬度
     */
    private void addLocation(String name, double longitude, double latitude) {
        jedis.geoadd("location_data", longitude, latitude, name);
    }
​
    /**
     * 获得用户到商家距离
     * @param user 用户
     * @param shop 商家
     * @return 距离
     */
    private double getDistance(String user, String shop) {
        return jedis.geodist("location_data", user, shop, GeoUnit.KM);
    }
​
    @Test
    public void testGeo() {
        addLocation("张三", 116.49428833935545, 39.86700462665782);
        addLocation("小吃店", 116.45961274121092, 39.87517301328063);
        System.out.println("用户到商家的距离为:" + getDistance("张三", "小吃店"));
    }
}

输出结果:

用户到商家的距离为:3.0963

2)陌生人社交APP里的查找附近的人功能

主要命令:

  • geoadd key longitude latitude:记录每个用户的地理位置
  • georadiusbymember key user radius unit='km'radius 设置为1,则附近1公里的用户都找出来

实现如下:

public class JedisTest {
    /**
     * 查找附近5公里内的店铺
     * @return 店铺
     */
    private List<GeoRadiusResponse> getNearbyShops() {
        return jedis.georadiusByMember("location_data", "张三", 5.0, GeoUnit.KM);
    }
​
    @Test
    public void testGeoShops() {
​
        List<String> nearbyShops = new ArrayList<>();
​
        List<GeoRadiusResponse> results = getNearbyShops();
​
        for (GeoRadiusResponse result : results) {
            String name = result.getMemberByString();
            if (!"张三".equals(name)) {
                nearbyShops.add(name);
            }
        }
​
        System.out.println("附近5公里内的商家: " + nearbyShops);
    }
}

输出结果:

附近5公里内的商家: [小吃店]