文章目录
前言
我们知道redis的常用的数据结构有String,list,hash,set,zset这五种,但是redis还有其他的特殊结构的用法,分别是BitMap(底层也是String),Geo,HyperLogLog和PubSub(发布订阅功能)。本文只涉及这些功能的用法,不涉及底层讲解。
一、序列化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
- 查询2023年3月14号登录过系统的总数量
Long count1 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("20230314".getBytes()));
System.out.println(count1); //5
- 查询2023年3月15号登录过系统的总数量
Long count2 = redisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("20230315".getBytes()));
System.out.println(count2); //5
- 测试发现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)
- 查询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,具体数量
- 查询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
- 非运算的是查看当前字节位数为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
- 异或运算,即并集-交集,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
- 指定经纬度查询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
});
- 指定存入的地点即天安门查询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支持匹配符的。
注:发布订阅模式消息不会进行持久化,所以在客户端订阅之前的消息是获取不到的!!!
- 设置监听处理器,可以实现MessageListener类并且实现其onMessage方法,方法里面即为监听的数据
- 设置订阅关系,即订阅哪些类型的数据。可注入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模块中,后续继续更新其他用法,敬请点波关注,感谢!!!