redis的几种特殊用法(BitMap,Geo,HyperLogLog,Pub/Sub)



前言

我们知道redis的常用的数据结构有String,list,hash,set,zset这五种,但是redis还有其他的特殊结构的用法,分别是BitMap(底层也是String),GeoHyperLogLogPubSub(发布订阅功能)。本文只涉及这些功能的用法,不涉及底层讲解


一、序列化redis的键值

用字符串序列化键值对能让我们比较清楚的感受到键值的数值,调试方便。

package com.bs.redis.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.Resource;

/**
 * @author bingshao
 * @date 2023/3/7
 **/
@Configuration
public class RedisConfig {

    @Resource
    private RedisConnectionFactory factory;

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }

}

二、BitMap

位图,即每位上面只用0/1标识,一个字节等于8位,即使上亿的数据也就需要十几M。

一个字节的位图可以初始标识为 00000000 。根据自身业务可以扩充字节数的大小并且决定为1的位的偏移量。

我们跟着案例学习相关的操作,如下:

1.setBit

假设有id为0,3,9,16,20的用户在2023年3月14号登录了系统,我们需要记录用户的登录记录。

一个字节只有八位,题中我们最大的id为20(从0开始算,即需要21位以上),所以需要用三个字节一共24位才能记录完毕,若用户登录过则将此id偏移量置为1。结果为10010000 01000000 10001000

代码为如下:

		redisTemplate.opsForValue().setBit("20230314", 0, true);
        redisTemplate.opsForValue().setBit("20230314", 3, true);
        redisTemplate.opsForValue().setBit("20230314", 9, true);
        redisTemplate.opsForValue().setBit("20230314", 16, true);
        redisTemplate.opsForValue().setBit("20230314", 20, true);

再假如2023年3月15日又有id为0,9,10,11,12的用户进行登录,我们继续记录一下(用redis回调进行操作)

        redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.setBit("20230315".getBytes(), 0, true));
        redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.setBit("20230315".getBytes(), 9, true));
        redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.setBit("20230315".getBytes(), 10, true));
        redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.setBit("20230315".getBytes(), 11, true));
        redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.setBit("20230315".getBytes(), 12, true));

设置好这两天的登录情况之后,我们需要查询一些信息也是比较方便的。

2.getBit

查询2023年3月14号用户id为3和id为4的用户是否登录过系统

        System.out.println(redisTemplate.opsForValue().getBit("20230314", 3)); //true
        System.out.println(redisTemplate.opsForValue().getBit("20230314", 4)); //false

3.bitCount

  1. 查询2023年3月14号登录过系统的总数量
        Long count1 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("20230314".getBytes()));
        System.out.println(count1); //5
  1. 查询2023年3月15号登录过系统的总数量
        Long count2 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("20230315".getBytes()));
        System.out.println(count2); //5
  1. 测试发现bitCount包含start和end两个边界,且注意这默认是byte不是bit
        Long count3 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("20230314".getBytes(), 0, 1));
        System.out.println(count3); //3
        Long count4 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("20230314".getBytes(), 0, 2));
        System.out.println(count4); //5
        Long count5 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("20230314".getBytes(), 0, 3));
        System.out.println(count5); //5
        Long count6 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("20230314".getBytes(), 0, 0));
        System.out.println(count6); //2

4.bitOp

此操作包含四种运算,分别为交集(and),并集(or),非(not),异或(xor)

  1. 查询2023年3月14号和2023年3月15号都登录过的用户id个数,结果为2
		byte[] bytes1 = "20230314".getBytes();
        byte[] bytes2 = "20230315".getBytes();

        // 交集
        Long count1 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitOp(RedisStringCommands.BitOperation.AND, "20230314_and_15".getBytes(), bytes1, bytes2));
        System.out.println(count1); //3,表示所需要的的字节数将结果存放到20230314_and_15上面
        Long count5 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("20230314_and_15".getBytes()));
        System.out.println(count5); //2,具体数量
  1. 查询2023年3月14号和2023年3月15号只要有一天登录的id个数,结果为8
// 并集
        Long count2 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitOp(RedisStringCommands.BitOperation.OR, "20230314_or_15".getBytes(), bytes1, bytes2));
        System.out.println(count2); //3  需要的字节数
        Long count6 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("20230314_or_15".getBytes()));
        System.out.println(count6); //8
  1. 非运算的是查看当前字节位数为0的数量,比如14号为1的有5个,计算非运算结果即为19个,因为需要三个字节即24位,24-5=19
        // 非
        Long count3 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitOp(RedisStringCommands.BitOperation.NOT, "20230314_not".getBytes(), bytes1));
        System.out.println(count3); //3,需要的字节数
        Long count7 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("20230314_not".getBytes()));
        System.out.println(count7); //19
  1. 异或运算,即并集-交集,8-2=6
// 异或
        Long count4 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitOp(RedisStringCommands.BitOperation.XOR, "20230314_xor_15".getBytes(), bytes1, bytes2));
        System.out.println(count4); //3
        Long count8 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("20230314_xor_15".getBytes()));
        System.out.println(count8); //6

5.bitPos

查询指定字节(注意是字节,不是bit位)范围,第一个为1/0的bit位

        Long index1 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitPos("20230314".getBytes(), true));
        System.out.println(index1); //0  第一个为true(1)的bit位

        Long index2 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitPos("20230314".getBytes(), true, Range.closed(1L, 2L))); // 双闭区间。默认为字节,字节也是从0开始算,
        System.out.println(index2); //9 字节区间[1,2]指的bit位即为[8,23]

        Long index3 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitPos("20230314".getBytes(), false));
        System.out.println(index3); //1

三、Geo

包含了一些对于地点位置经纬度的存储,已经存储后的查询。

1.geoAdd

我们存入天安门,故宫,长城的地理位置信息。

		Map<String, Point> map = new HashMap<>(4);
        map.put("天安门", new Point(116.403963, 39.915119));
        map.put("故宫", new Point(116.403414, 39.924091));
        map.put("长城", new Point(116.024067, 40.362639));
        redisTemplate.opsForGeo().add("city", map);

2.position

查看天安门和长城的位置信息

		// 查看天安门和长城的经纬度
        List<Point> position = redisTemplate.opsForGeo().position("city", "天安门", "长城");
        position.forEach(System.out::println);
        // Point [x=116.403963, y=39.915120]
        // Point [x=116.024066, y=40.362640]

3.distance

查询天安门和故宫的距离,指定单位km

        // 查询天安门和故宫的距离,单位km
        Distance distance = redisTemplate.opsForGeo().distance("city", "天安门", "故宫", RedisGeoCommands.DistanceUnit.KILOMETERS);
        System.out.println(distance.getValue() + distance.getUnit()); // 0.9988km

4.radius

  1. 指定经纬度查询100公里范围的地点从近到远的十条数据
		//返回10条
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(10);
        //半径100公里内
        Distance distance = new Distance(100, Metrics.KILOMETERS);
        Circle circle = new Circle(new Point(115.000000, 40.000000), distance);
        // 指定经纬度查询100公里范围的地点从近到远
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults1 = redisTemplate.opsForGeo().radius("city", circle, args);
        geoResults1.forEach(geoLocationGeoResult -> {
            System.out.println("geoResults1========");
            System.out.println(geoLocationGeoResult.getContent().getName());
            System.out.println(geoLocationGeoResult.getDistance().getValue());
            System.out.println(geoLocationGeoResult.getDistance().getMetric());
            //geoResults1========
            //长城
            //95.9151
            //KILOMETERS
        });
  1. 指定存入的地点即天安门查询100公里范围的地点从近到远的十条数据 ,结果包含本身
		//返回10条
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(10);
        //半径100公里内
        Distance distance = new Distance(100, Metrics.KILOMETERS);
        // 指定存入的地点即天安门查询100公里范围的地点从近到远 ,结果包含本身
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults2 = redisTemplate.opsForGeo().radius("city", "天安门", distance, args);
        geoResults2.forEach(geoLocationGeoResult -> {
            System.out.println("geoResults2========");
            System.out.println(geoLocationGeoResult.getContent().getName());
            System.out.println(geoLocationGeoResult.getDistance().getValue());
            System.out.println(geoLocationGeoResult.getDistance().getMetric());
            //geoResults2========
            //天安门
            //0.0
            //KILOMETERS
            //geoResults2========
            //故宫
            //0.9988
            //KILOMETERS
            //geoResults2========
            //长城
            //59.339
            //KILOMETERS

        });

5.geoHash

		List<String> hash = redisTemplate.opsForGeo().hash("city", "天安门");
        System.out.println(hash.get(0)); //wx4g0f6f2v0

四、HyperLogLog

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

在 Redis 中每个键占用的内容都是 12K,理论存储近似接近 2^64 个值,不管存储的内容是什么。这是一个基于基数估计的算法,只能比较准确的估算出基数,可以使用少量固定的内存去存储并识别集合中的唯一元素。但是这个估算的基数并不一定准确,是一个带有 0.81% 标准错误(standard error)的近似值。

因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

使用场景的话可以用来统计某个站点的访问的ip数量,或者类似于统计某个系统的月活量等等。。。

1.add

添加两个键hyperLogLog_name1,hyperLogLog_name2各自存入名称

		redisTemplate.opsForHyperLogLog().add("hyperLogLog_name1", "冰少");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name1", "妃妃");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name1", "云澈");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name1", "茉莉");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name1", "倾月");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name1", "彩脂");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name1", "冰少");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name1", "妃妃");

        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name2", "苍月");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name2", "凤兒儿");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name2", "小妖后");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name2", "冰少");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name2", "茉莉");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name2", "甄姬");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name2", "苍月");
        redisTemplate.opsForHyperLogLog().add("hyperLogLog_name2", "甄姬");

2.size

查询两个键的不重复的独立的名称的数量和

		Long size = redisTemplate.opsForHyperLogLog().size("hyperLogLog_name1", "hyperLogLog_name2");
        System.out.println(size); // 10 两个键总独立数和

3.merge

合并多个键的不重复的名称并保存在新的键hyperLogLog_name_total上面

		// 合并独立个数
        redisTemplate.opsForHyperLogLog().union("hyperLogLog_name_total", "hyperLogLog_name1", "hyperLogLog_name2");
        Long size1 = redisTemplate.opsForHyperLogLog().size("hyperLogLog_name_total");
        System.out.println(size1); // 10 

五、Pub/Sub

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

订阅方式有两种,一种是基于channel固定字符的,还有一种是基于pattern支持匹配符的。

注:发布订阅模式消息不会进行持久化,所以在客户端订阅之前的消息是获取不到的!!!

  1. 设置监听处理器,可以实现MessageListener类并且实现其onMessage方法,方法里面即为监听的数据
  2. 设置订阅关系,即订阅哪些类型的数据。可注入RedisMessageListenerContainer类,自行配置。

具体代码如下图:

package com.bs.redis.listener;

import lombok.extern.log4j.Log4j2;

import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.stereotype.Component;

/**
 * @author bingshao
 * @date 2023/3/7
 **/
@Log4j2
@Component
public class RedisListener implements MessageListener {


    @Override
    public void onMessage(Message message, byte[] pattern) {
        //获取订阅消息内容
        String topic = new String(pattern);
        String context = new String(message.getBody());
        String channel = new String(message.getChannel());
        log.info("topic:{},context:{},channel:{}", topic, context, channel);
    }

    @Bean
    RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory, RedisListener redisListener, MessageListenerAdapter test) {
        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);

        // 两种方式

        // 1.订阅topic - test1,只支持确定的字符
        redisMessageListenerContainer.addMessageListener(redisListener, new ChannelTopic("test1"));

        // 2.将回调的方法注册到 container 中去,支持匹配
        redisMessageListenerContainer.addMessageListener(test, new PatternTopic("test*"));
        return redisMessageListenerContainer;
    }

    /**
     * 绑定消息监听者和接收监听的方法
     *
     * @param receiver
     * @return
     */
    @Bean
    public MessageListenerAdapter test(ReceiverRedisMessage receiver) {
        // 这里的 test 是在 ReceiverRedisMessage 中具体存在的方法,发布订阅之后,会回调这个方法
        return new MessageListenerAdapter(receiver, "test");
    }


}

package com.bs.redis.listener;

import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;

/**
 * @author bingshao
 * @date 2023/3/7
 **/
@Log4j2
@Component
public class ReceiverRedisMessage {

    public void test(String jsonMsg) {
        // 具体回调的方法
        log.info("test ----->  {}", jsonMsg);
    }

}

发送端:

package com.bs.redis.controller;

import com.alibaba.fastjson.JSONObject;
import com.bs.redis.vo.MessageVo;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * @author bingshao
 * @date 2023/3/7
 **/
@RestController
@RequestMapping("/pubsub")
public class PubSubController {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @PostMapping("/send/{channel}/{message}")
    public void send(@PathVariable("channel") String channel, @PathVariable("message") String message) {
        redisTemplate.convertAndSend(channel, message);
    }

    @PostMapping("/send/{topic}")
    public void send(@PathVariable("topic") String topic, @RequestBody MessageVo messageVo) {
        redisTemplate.convertAndSend(topic, JSONObject.toJSONString(messageVo));
    }

}

package com.bs.redis.vo;

import lombok.Data;

import java.io.Serializable;

/**
 * @author bingshao
 * @date 2023/3/7
 **/
@Data
public class MessageVo implements Serializable {

    private Long id;

    private String name;

}


总结

以上即为redis特殊结构的简单用法,本项目的代码为https://gitee.com/bingshao0412/learning-induction.git仓库的bs-redis模块中,后续继续更新其他用法,敬请点波关注,感谢!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值