ZSET、Geo 、 Stream
redis zset数据结构
- zset 是 set 的一个升级版本,它在 set 的基础上增加了一个顺序属性,它和 set 一样,zset也是 string 类型元素的集合,且不允许重复的成员,不同的是每个元素都会关联一个 double类型的 score。
- 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 2的(32 - 1)次方 (4294967295, 每个集合可存储40多亿个成员)。
- zset 最经典的应用场景就是排行榜。
常用命令
- ZADD
ZADD key score member [[score member] [score member] …]
将一个或多个member元素及其score值加入到有序集key当中。 - ZRANGE
ZRANGE key start stop [WITHSCORES]
返回有序集key中,指定区间内的成员。
案例:创业公司招进了4个员工,分别为: alex 工资2000元 tom工资5000元 jack工资6000元 阿甘1000元,请按工资升序排序
39.100.196.99:6379> zadd salary 2000 alex 5000 tom 6000 jack 1000 agan
(integer) 4
39.100.196.99:6379> zrange salary 0 -1 withscores
1) "agan"
2) "1000"
3) "alex"
4) "2000"
5) "tom"
6) "5000"
7) "jack"
8) "6000"
(1.21s)
- ZREM
ZREM key member [member …]
移除有序集key中的一个或多个成员,不存在的成员将被忽略。
案例:创业公司 tom离职了
39.100.196.99:6379> zrange salary 0 -1 withscores
1) "agan"
2) "1000"
3) "alex"
4) "2000"
5) "tom"
6) "5000"
7) "jack"
8) "6000"
(1.21s)
39.100.196.99:6379> zrem salary tom
(integer) 1
39.100.196.99:6379> zrange salary 0 -1 withscores
1) "agan"
2) "1000"
3) "alex"
4) "2000"
5) "jack"
6) "6000"
- ZCARD
ZCARD key
返回有序集key的基数。
案例:创业公司 有多少人
39.100.196.99:6379> zrange salary 0 -1 withscores
1) "agan"
2) "1000"
3) "alex"
4) "2000"
5) "jack"
6) "6000"
39.100.196.99:6379> zcard salary
(integer) 3
- ZCOUNT
ZCOUNT key min max
返回有序集key中,score值在min和max之间(默认包括score值等于min或max)的成员。
案例:创业公司老板问你 ,工资在2000 至 6000有多少人
39.100.196.99:6379> zrange salary 0 -1 withscores
1) "agan"
2) "1000"
3) "alex"
4) "2000"
5) "tom"
6) "5000"
7) "jack"
8) "6000"
39.100.196.99:6379> ZCOUNT salary 2000 6000
(integer) 3
- ZSCORE
ZSCORE key member
返回有序集key中,成员member的score值。
案例:创业公司老板问你 ,阿甘的工资是多少 ?
39.100.196.99:6379> zrange salary 0 -1 withscores
1) "agan"
2) "1000"
3) "alex"
4) "2000"
5) "tom"
6) "5000"
7) "jack"
8) "6000"
39.100.196.99:6379> zscore salary agan
"1000"
- ZINCRBY
ZINCRBY key increment member
为有序集key的成员member的score值加上增量increment。
案例:创业公司老板说阿甘表现很好,给他加500元吧
39.100.196.99:6379> ZINCRBY salary 500 agan
"1500"
39.100.196.99:6379> zrange salary 0 -1 withscores
1) "agan"
2) "1500"
3) "alex"
4) "2000"
5) "tom"
6) "5000"
7) "jack"
8) "6000"
- ZREVRANGE
ZREVRANGE key start stop [WITHSCORES]
返回有序集key中,指定区间内的成员,降序。
案例:创业公司老板说经济不好,成本太大,看工资最多的是哪些人?
39.100.196.99:6379> zrange salary 0 -1 withscores #升序
1) "agan"
2) "1500"
3) "alex"
4) "2000"
5) "tom"
6) "5000"
7) "jack"
8) "6000"
39.100.196.99:6379> ZREVRANGE salary 0 -1 withscores #降序
1) "jack"
2) "6000"
3) "tom"
4) "5000"
5) "alex"
6) "2000"
7) "agan"
8) "1500"
- ZRANGEBYSCORE 取某个范围score的member,可以用于分页查询
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
返回有序集key中,所有score值介于min和max之间(包括等于min或max)的成员。有序集成员按score值递增(从小到大)次序排列。
案例:创业公司老板要给工资低的人加薪水,老板要求先看低于5000元的有哪些人?人多的话分页查看
39.100.196.99:6379> ZREVRANGE salary 0 -1 withscores
1) "jack"
2) "6000"
3) "tom"
4) "5000"
5) "alex"
6) "2000"
7) "agan"
8) "1500"
39.100.196.99:6379> ZRANGEBYSCORE salary 1 5000
1) "agan"
2) "alex"
3) "tom"
39.100.196.99:6379> ZRANGEBYSCORE salary 1 5000 LIMIT 0 2
1) "agan"
2) "alex"
39.100.196.99:6379> ZRANGEBYSCORE salary 1 5000 LIMIT 2 2
1) "tom"
-
ZREVRANGEBYSCORE 和上面的功能意义,但是这次是降序的
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
返回有序集key中,score值介于max和min之间(默认包括等于max或min)的所有的成员。有序集成员按score值递减(从大到小)的次序排列。 -
ZRANK 取某个member的排名,升序
ZRANK key member
案例:创业公司老板要查,工资从低到高,查某个员工排第几名?
9.100.196.99:6379> ZREVRANGE salary 0 -1 withscores
1) "jack"
2) "6000"
3) "tom"
4) "5000"
5) "alex"
6) "2000"
7) "agan"
8) "1500"
(0.75s)
39.100.196.99:6379> ZRANK salary agan
(integer) 0
-
ZREVRANK 取某个member的排名,降序
ZREVRANK key member -
ZREMRANGEBYRANK 移除指定排名(rank)区间内的所有成员。
ZREMRANGEBYRANK key start stop
案例:经济不好,老板要裁员了,把工资最低的2个人裁掉
39.100.196.99:6379> zrange salary 0 -1 withscores
1) "agan"
2) "1500"
3) "alex"
4) "2000"
5) "tom"
6) "5000"
7) "jack"
8) "6000"
39.100.196.99:6379> ZREMRANGEBYRANK salary 0 1
(integer) 2
39.100.196.99:6379> zrange salary 0 -1 withscores
1) "tom"
2) "5000"
3) "jack"
4) "6000"
ZREMRANGEBYRANK 移除指定排名(rank)区间内的所有成员。
ZREMRANGEBYRANK key start stop
案例:经济不好,老板要裁员了,把工资最低的2个人裁掉
39.100.196.99:6379> zrange salary 0 -1 withscores
1) "agan"
2) "1500"
3) "alex"
4) "2000"
5) "tom"
6) "5000"
7) "jack"
8) "6000"
39.100.196.99:6379> ZREMRANGEBYRANK salary 0 1
(integer) 2
39.100.196.99:6379> zrange salary 0 -1 withscores
1) "tom"
2) "5000"
3) "jack"
4) "6000"
- ZINTERSTORE 求交集
ZINTERSTORE destination numkeys key [key …] [WEIGHTS weight [weight …]] [AGGREGATE SUM|MIN|MAX]
计算给定的一个或多个有序集的交集,其中给定key的数量必须以numkeys参数指定,并将该交集(结果集)储存到destination。
39.100.196.99:6379> zadd group1 10 a 20 b 30 c
(integer) 3
39.100.196.99:6379> zadd group2 10 x 20 y 30 z 20 c
(integer) 4
39.100.196.99:6379> ZINTERSTORE group3 group1 group2
(error) ERR value is not an integer or out of range
39.100.196.99:6379> ZINTERSTORE group3 2 group1 group2
(integer) 1
39.100.196.99:6379> zrange group3 0 -1 withscores
1) "c"
2) "50"
- ZUNIONSTORE 求并集
ZUNIONSTORE destination numkeys key [key …] [WEIGHTS weight [weight …]] [AGGREGATE SUM|MIN|MAX]
计算给定的一个或多个有序集的并集,其中给定key的数量必须以numkeys参数指定,并将该并集(结果集)储存到destination。
39.100.196.99:6379> ZUNIONSTORE group4 2 group1 group2
(integer) 6
39.100.196.99:6379> zrange group4 0 -1 withscores
1) "a"
2) "10"
3) "x"
4) "10"
5) "b"
6) "20"
7) "y"
8) "20"
9) "z"
10) "30"
11) "c"
12) "50"
排行榜
技术模拟思路:
采用26个英文字母来实现排行,随机为每个字母生成一个随机数作为score
为了更好的体验,先做几件事:
- 先初始化1个月的历史数据
- 定时5秒钟,模拟微博的热度刷新(例如模拟点赞 收藏 评论的热度值更新)
- 定时1小时合并统计 天、周、月的排行榜。
步骤一、初始化1个月的历史数据
@Service
@Slf4j
public class InitService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 先初始化1个月的历史数据
*/
public void init30day(){
//计算当前的小时key
long hour=System.currentTimeMillis()/(1000*60*60);
//初始化近30天,每天24个key
for(int i=1;i<24*30;i++){
//倒推过去30天
String key=Constants.HOUR_KEY+(hour-i);
this.initMember(key);
System.out.println(key);
}
}
/**
*初始化某个小时的key
*/
public void initMember(String key) {
Random rand = new Random();
//采用26个英文字母来实现排行,随机为每个字母生成一个随机数作为score
for(int i = 1;i<=26;i++){
this.redisTemplate.opsForZSet().add(key,String.valueOf((char)(96+i)),rand.nextInt(10));
}
}
}
步骤二:定时刷新数据
@Service
@Slf4j
public class TaskService {
@Autowired
private RedisTemplate redisTemplate;
/**
*2. 定时5秒钟,模拟微博的热度刷新(例如模拟点赞 收藏 评论的热度值更新)
* 3. 定时1小时合并统计 天、周、月的排行榜。
*/
@PostConstruct
public void init(){
log.info("启动初始化 ..........");
// 2. 定时5秒钟,模拟微博的热度刷新(例如模拟点赞 收藏 评论的热度值更新)
new Thread(()->this.refreshDataHour()).start();
// 3. 定时1小时合并统计 天、周、月的排行榜。
new Thread(()->this.refreshData()).start();
}
/**
*采用26个英文字母来实现排行,随机为每个字母生成一个随机数作为score
*/
public void refreshHour(){
//计算当前的小时key
long hour=System.currentTimeMillis()/(1000*60*60);
//为26个英文字母来实现排行,随机为每个字母生成一个随机数作为score
Random rand = new Random();
for(int i = 1;i<=26;i++){
//redis的ZINCRBY 新增这个积分值
this.redisTemplate.opsForZSet().incrementScore(Constants.HOUR_KEY+hour,String.valueOf((char)(96+i)),rand.nextInt(10));
}
}
/**
*刷新当天的统计数据
*/
public void refreshDay(){
long hour=System.currentTimeMillis()/(1000*60*60);
List<String> otherKeys=new ArrayList<>();
//算出近24小时内的key
for(int i=1;i<23;i++){
String key=Constants.HOUR_KEY+(hour-i);
otherKeys.add(key);
}
//把当前的时间key,并且把后推23个小时,共计近24小时,求出并集存入Constants.DAY_KEY中
//redis ZUNIONSTORE 求并集
this.redisTemplate.opsForZSet().unionAndStore(Constants.HOUR_KEY+hour,otherKeys,Constants.DAY_KEY);
//设置当天的key 40天过期,不然历史数据浪费内存
for(int i=0;i<24;i++){
String key=Constants.HOUR_KEY+(hour-i);
this.redisTemplate.expire(key,40, TimeUnit.DAYS);
}
log.info("天刷新完成..........");
}
/**
*刷新7天的统计数据
*/
public void refreshWeek(){
long hour=System.currentTimeMillis()/(1000*60*60);
List<String> otherKeys=new ArrayList<>();
//算出近7天内的key
for(int i=1;i<24*7-1;i++){
String key=Constants.HOUR_KEY+(hour-i);
otherKeys.add(key);
}
//把当前的时间key,并且把后推24*7-1个小时,共计近24*7小时,求出并集存入Constants.WEEK_KEY中
this.redisTemplate.opsForZSet().unionAndStore(Constants.HOUR_KEY+hour,otherKeys,Constants.WEEK_KEY);
log.info("周刷新完成..........");
}
/**
*刷新30天的统计数据
*/
public void refreshMonth(){
long hour=System.currentTimeMillis()/(1000*60*60);
List<String> otherKeys=new ArrayList<>();
//算出近30天内的key
for(int i=1;i<24*30-1;i++){
String key=Constants.HOUR_KEY+(hour-i);
otherKeys.add(key);
}
//把当前的时间key,并且把后推24*30个小时,共计近24*30小时,求出并集存入Constants.MONTH_KEY中
this.redisTemplate.opsForZSet().unionAndStore(Constants.HOUR_KEY+hour,otherKeys,Constants.MONTH_KEY);
log.info("月刷新完成..........");
}
/**
*定时1小时合并统计 天、周、月的排行榜。
*/
public void refreshData(){
while (true){
//刷新当天的统计数据
this.refreshDay();
// 刷新7天的统计数据
this.refreshWeek();
// 刷新30天的统计数据
this.refreshMonth();
//TODO 在分布式系统中,建议用xxljob来实现定时
try {
Thread.sleep(1000*60*60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
*定时5秒钟,模拟微博的热度刷新(例如模拟点赞 收藏 评论的热度值更新)
*/
public void refreshDataHour(){
while (true){
this.refreshHour();
//TODO 在分布式系统中,建议用xxljob来实现定时
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
步骤3:排行榜查询接口
@RestController
@Slf4j
public class Controller {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping(value = "/getHour")
public Set getHour() {
long hour=System.currentTimeMillis()/(1000*60*60);
//ZREVRANGE 返回有序集key中,指定区间内的成员,降序。
Set<ZSetOperations.TypedTuple<Integer>> rang= this.redisTemplate.opsForZSet().reverseRangeWithScores(Constants.HOUR_KEY+hour,0,30);
return rang;
}
@GetMapping(value = "/getDay")
public Set getDay() {
Set<ZSetOperations.TypedTuple<Integer>> rang= this.redisTemplate.opsForZSet().reverseRangeWithScores(Constants.DAY_KEY,0,30);
return rang;
}
@GetMapping(value = "/getWeek")
public Set getWeek() {
Set<ZSetOperations.TypedTuple<Integer>> rang= this.redisTemplate.opsForZSet().reverseRangeWithScores(Constants.WEEK_KEY,0,30);
return rang;
}
@GetMapping(value = "/getMonth")
public Set getMonth() {
Set<ZSetOperations.TypedTuple<Integer>> rang= this.redisTemplate.opsForZSet().reverseRangeWithScores(Constants.MONTH_KEY,0,30);
return rang;
}
}
一般情况下,我们浏览各大网站时,点击某篇文章,博客,帖子,其阅读量就会+1,或者点击 点赞按钮,又或是评论数量;
这些都会根据热度算法,计算其热度;
GeoHash
Redis 3.2开始,Redis基于geohash和zset提供了地理位置相关功能。
Geohash是一种地址编码,它能把二维的经纬度编码成一维的字符串
命令
- GEOADD
将给定的位置对象(纬度、经度、名字)添加到指定的key;
39.100.196.99:6379> geoadd hotel 113.9807127428 22.5428248089 "世界之窗" 113.9832042690 22.5408496326 "南山威尼斯酒店" 114.0684865267 22.5412294122 "福田喜来登酒店" 114.3135524539 22.5999265998 "大梅沙海景酒店" 113.9349465491 22.5305488659 "南山新年酒店" 114.0926367279 22.5497917634 "深圳华强广场酒店"
6
39.100.196.99:6379> zrange hotel 0 -1
南山新年酒店
世界之窗
南山威尼斯酒店
福田喜来登酒店
深圳华强广场酒店
大梅沙海景酒店
注:
- 这里我们采用的是中文存储,如果出现了乱码,redis命令的登录命令加上 --raw
例如: ./redis-cli --raw
- GEOPOS
从key里面返回所有给定位置对象的位置(经度和纬度);
39.100.196.99:6379> GEOPOS hotel "世界之窗"
113.98071080446243286
22.54282525199023013
- GEOHASH:
返回一个或多个位置对象的Geohash表示;
39.100.196.99:6379> GEOHASH hotel "世界之窗"
ws101xy1rp0
- GEODIST key member1 member2 [unit]
返回两个给定位置之间的距离;
指定单位的参数 unit 必须是以下单位的其中一个:
– m 表示单位为米。
– km 表示单位为千米。
– mi 表示单位为英里。
– ft 表示单位为英尺。
39.100.196.99:6379> GEODIST hotel "世界之窗" "南山威尼斯酒店" m
337.4887
- GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
给定一个经纬度,然后以半径为中心,计算出半径内的数据。
39.100.196.99:6379> GEORADIUS hotel 113.9410499639 22.5461508801 10 km WITHDIST WITHCOORD count 10
南山新年酒店
1.8451
113.93494695425033569
22.53054959741555052
世界之窗
4.0910
113.98071080446243286
22.54282525199023013
南山威尼斯酒店
4.3704
113.98320525884628296
22.54085070420710224
– WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
– WITHCOORD: 将位置元素的经度和维度也一并返回。
– WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
– ASC、DESC 排序方式,按照距离的 升序、降序排列
– STORE key1 把结果存入key1,zset格式,以坐标hash为score
– STOREDIST key2 把结果存入key2,zset格式,以距离为score
- GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
GEORADIUSBYMEMBER 和 GEORADIUS 一样的功能,区别在于,GEORADIUS是以经纬度去查询,而GEORADIUSBYMEMBER是以当前集合中的某个member元素来查询
39.100.196.99:6379> GEORADIUSBYMEMBER hotel "世界之窗" 10 km WITHDIST WITHCOORD count 10
世界之窗
0.0000
113.98071080446243286
22.54282525199023013
南山威尼斯酒店
0.3375
113.98320525884628296
22.54085070420710224
南山新年酒店
4.8957
113.93494695425033569
22.53054959741555052
福田喜来登酒店
9.0190
114.06848877668380737
22.54122837765984144
附近酒店搜索实现
@RestController
@Slf4j
public class Controller {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping(value = "/init")
public void init() {
Map<String, Point> map= Maps.newHashMap();
map.put("世界之窗",new Point(113.9807127428,22.5428248089));
map.put("南山威尼斯酒店",new Point(113.9832042690 ,22.5408496326));
map.put("福田喜来登酒店" ,new Point(114.0684865267,22.5412294122));
map.put("大梅沙海景酒店",new Point(114.3135524539 ,22.5999265998));
map.put("南山新年酒店",new Point(113.9349465491,22.5305488659));
map.put("深圳华强广场酒店",new Point(114.0926367279 ,22.5497917634));
this.redisTemplate.opsForGeo().add(Constants.HOTEL_KEY,map);
}
@GetMapping(value = "/position")
public Point position(String member) {
//获取经纬度坐标
List<Point> list= this.redisTemplate.opsForGeo().position(Constants.HOTEL_KEY,member);
return list.get(0);
}
@GetMapping(value = "/hash")
public String hash(String member) {
//geohash算法生成的base32编码值
List<String> list= this.redisTemplate.opsForGeo().hash(Constants.HOTEL_KEY,member);
return list.get(0);
}
@GetMapping(value = "/distance")
public Distance distance(String member1, String member2) {
Distance distance= this.redisTemplate.opsForGeo().distance(Constants.HOTEL_KEY,member1,member2, RedisGeoCommands.DistanceUnit.KILOMETERS);
return distance;
}
/**
* 通过经度,纬度查找附近的
*/
@GetMapping(value = "/radiusByxy")
public GeoResults radiusByxy() {
//这个坐标是腾讯大厦位置
Circle circle = new Circle(113.9410499639, 22.5461508801, Metrics.KILOMETERS.getMultiplier());
//返回50条
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(Constants.HOTEL_KEY,circle, args);
return geoResults;
}
/**
* 通过地方查找附近
*/
@GetMapping(value = "/radiusByMember")
public GeoResults radiusByMember() {
String member="世界之窗";
//返回50条
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);
//半径10公里内
Distance distance=new Distance(10, Metrics.KILOMETERS);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(Constants.HOTEL_KEY,member, distance,args);
return geoResults;
}
}
关注Pull推送
场景:每个用户都有一个关注微博列表List,和个人微博列表List;
明星发表一条微博、如果使用 Redis List数据结构, 就需要先获取明星的粉丝集合,再将微博的ID发送到粉丝的List,如果粉丝的用户量不大,就几十万,还是勉强可以支撑的。适合中小型并发。但是像大明星,粉丝数量几千万,将明星微博的ID推送Push到粉丝的关注List,这个过程太耗时间,会直接把服务器给卡死了。
而且明星实时在线粉丝数量可能只有百分之一,也就是几十万。push操作短时间内看来相当于做了无用功;
替代方案:使用zset pull 推送,每个用户都有一个关注微博列表Zset , 和个人列表Zset;
当用户登录后,主动去查询关注用户的微博;并将他们的微博放到自己的关注微博列表Zset里面;
PULL 与PUSH的差别
push : 每次用户发微博都要异步推送给每个粉丝的关注列表;
pull :每个粉丝查看关注微博列表,都需要主动去关注人的个人微博列表下拉取,再存储到自己的关注微博列表里;
选择pull 方式, 需要自己去关注人的个人微博列表下拉取最新微博,这种方式可以通过客户端定时轮询服务端,查询最新的微博;
pull 技术方案
- 用户发微博、先写入DB、再写入Redis, 使用Hash数据结构存储微博 、 key = post::id
- 异步推送到个人微博列表Zset;
为什么个人列表和关注列表采用zset集合
拉取微博是根据刷新时间t进行过滤的,
使用List集合的话,只能用将微博的发表时间和id一起转化为JSON字符串(或者将将ID写入list),再写入List, 获取数据时,需要根据时间进行过滤,就意味着,我们并不知道微博的发表时间,只能根据List的微博ID拉取全部微博JSON字符串,再获取发表时间,再进行过滤,太麻烦了,效率也很低;
使用Zset集合,将发表时间作为score, id作为value,查询时,根据score进行排序,进行过滤,进行分页查询,比List方便一点;
基于pull技术,实现微博个人列表
PullContentController
@Api(description = "微博内容:pull功能")
@RestController
@RequestMapping("/pull-content")
public class PullContentController {
@Autowired
private PullContentService contentService;
@ApiOperation(value="用户发微博")
@PostMapping(value = "/post")
public void post(@RequestBody ContentVO contentVO) {
Content content=new Content();
BeanUtils.copyProperties(contentVO,content);
contentService.post(content);
}
@ApiOperation(value="获取个人列表")
@GetMapping(value = "/homeList")
public PageResult<Content> getHomeList(Integer userId, int page, int size){
return this.contentService.homeList(userId,page,size);
}
}
PullContentService
@Slf4j
@Service
public class PullContentService extends ContentService{
/**
* 用户发微博
*/
public void post(Content obj){
Content temp=this.addContent(obj);
this.addMyPostBox(temp);
}
/**
* 发布微博的时候,加入到我的个人列表
*/
public void addMyPostBox(Content obj){
String key= Constants.CACHE_MY_POST_BOX_ZSET_KEY+obj.getUserId();
//按秒为单位
long score=obj.getCreateTime().getTime()/1000;
this.redisTemplate.opsForZSet().add(key,obj.getId(),score);
}
/**
* 获取个人列表
*/
public PageResult<Content> homeList(Integer userId, int page, int size){
PageResult<Content> pageResult=new PageResult();
List<Integer> list=new ArrayList<>();
long start = (page - 1) * size;
long end = start + size - 1;
try {
String key= Constants.CACHE_MY_POST_BOX_ZSET_KEY+userId;
//1.设置总数
long total=this.redisTemplate.opsForZSet().zCard(key);
pageResult.setTotal(total);
//2.分页查询
//redis ZREVRANGE
Set<ZSetOperations.TypedTuple<Integer>> rang= this.redisTemplate.opsForZSet().reverseRangeWithScores(key,start,end);
for (ZSetOperations.TypedTuple<Integer> obj:rang){
list.add(obj.getValue());
log.info("个人post集合value={},score={}",obj.getValue(),obj.getScore());
}
//3.去拿明细数据
List<Content> contents=this.getContents(list);
pageResult.setRows(contents);
}catch (Exception e){
log.error("异常",e);
}
return pageResult;
}
}
基于pull技术,实现微博关注列表
/**
* 刷新拉取用户关注列表
* 用户第一次刷新或定时刷新 触发
*/
private void refreshAttentionBox(int userId){
//获取刷新的时间
String refreshkey=Constants.CACHE_REFRESH_TIME_KEY+userId;
Long ago=(Long) this.redisTemplate.opsForValue().get(refreshkey);
//如果时间为空,取2天前的时间
if (ago==null){
//当前时间
long now=System.currentTimeMillis()/1000;
//当前时间减去2天
ago=now-60*60*24*2;
}
//提取该用户的关注列表
String followerkey=Constants.CACHE_KEY_FOLLOWEE+userId;
Set<Integer> sets= redisTemplate.opsForSet().members(followerkey);
log.debug("用户={}的关注列表={}",followerkey,sets);
//当前时间
long now=System.currentTimeMillis()/1000;
String attentionkey= Constants.CACHE_MY_ATTENTION_BOX_ZSET_KEY+userId;
for (Integer id:sets){
//去关注人的个人主页,拿最新微博
String key= Constants.CACHE_MY_POST_BOX_ZSET_KEY+id;
Set<ZSetOperations.TypedTuple<Integer>> rang= this.redisTemplate.opsForZSet().rangeByScoreWithScores(key,ago,now);
if(!CollectionUtils.isEmpty(rang)){
//加入我的关注post集合 就是通过上次刷新时间计算出最新的微博,写入关注zset集合;再更新刷新时间
this.redisTemplate.opsForZSet().add(attentionkey,rang);
}
}
//关注post集合 只留1000个
//计算post集合,总数
long count=this.redisTemplate.opsForZSet().zCard(attentionkey);
//如果大于1000,就剔除多余的post
if(count>1000){
long end=count-1000;
//redis ZREMRANGEBYRANK
this.redisTemplate.opsForZSet().removeRange(attentionkey,0,end);
}
long days=this.redisTemplate.getExpire(attentionkey,TimeUnit.DAYS);
if(days<10){
//设置30天过期
this.redisTemplate.expire(attentionkey,30,TimeUnit.DAYS);
}
this.redisTemplate.opsForValue().set(refreshkey,now);
}
Stream
Redis 5.0推出了一个新的数据结构:Stream。Stream就是一个流处理 的数据结构.
基于流处理的数据结构,它的功能应用于类似IM的聊天工具和典型的消息队列。
Redis 的Stream几乎满足了消息队列具备的全部内容,包括但不限于:
1.消息ID的序列化生成
2.消息遍历
3.消息的阻塞和非阻塞读取
4.消息的分组消费
5.未完成消息的处理
6.消息队列监控
xadd:向Stream追加消息
xdel:从Stream中删除消息,删除仅仅是设置标志位,不影响消息总长度。
xrange:获取Stream中的消息列表,自动过滤已经删除的消息。-表示最小值,+表示最大值。
xlen:获取Stream的消息长度,所有在链表中存在的消息
del:删除整个Stream中的所有消息。
命令
- stream生产消息
XADD,命令用于在某个stream(流数据)中追加消息,语法如下:
xadd key ID field string [field string ...]
需要提供key,消息ID方案,消息内容,其中消息内容为key-value型数据。
ID,最常使用*,表示由Redis生成消息ID,这也是强烈建议的方案。
field string [field string], 就是当前消息内容,由1个或多个key-value构成。
39.100.196.99:6379> xadd message * hello agan
"1587196058726-0" #消息ID
39.100.196.99:6379> xadd message * hello agan2
"1587196063320-0"
39.100.196.99:6379> xadd message * hello agan3
"1587196067111-0"
- stream独立消费
从消息队列中获取消息,XREAD,消费消息
xread [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]
[COUNT count],用于限定获取的消息数量
[BLOCK milliseconds],用于设置XREAD为阻塞模式,默认为非阻塞模式
ID,用于设置由哪个消息ID开始读取。使用0表示从第一条消息开始。(本例中就是使用0),
消息队列javaID是单调递增的,所以通过设置起点,可以向后读取。在阻塞模式中,可以使用,表示最新的消息ID。
39.100.196.99:6379> xread STREAMS message 0
1) 1) "message"
2) 1) 1) "1587196058726-0"
2) 1) "hello"
2) "agan"
2) 1) "1587196063320-0"
2) 1) "hello"
2) "agan2"
3) 1) "1587196067111-0"
2) 1) "hello"
2) "agan3"
消息ID的原理
上文这个消息id 1587196058726-0,是redis 生成的消息id。
它由2部分组成:时间戳-序号。时间戳是毫秒级,序号是为了防止相同时间内生成的id重复。
39.100.196.99:6379> multi
OK
39.100.196.99:6379> xadd message * hello agan1
QUEUED
39.100.196.99:6379> xadd message * hello agan2
QUEUED
39.100.196.99:6379> xadd message * hello agan3
QUEUED
39.100.196.99:6379> exec
1) "1587198991846-0"
2) "1587198991846-1"
3) "1587198991846-2"
IM聊天室
@Api
@RequestMapping("/im")
@RestController
@Slf4j
public class ImController {
@Autowired
RedisTemplate redisTemplate;
public final String room_key = "room::";
@GetMapping("/init")
public Object init(){
Long increment = redisTemplate.opsForValue().increment("room::id");
return increment;
}
@GetMapping("/add")
public RecordId add(String roomId , String username , String content){
String key = room_key + roomId;
HashMap<Object, Object> map = new HashMap<>();
map.put(username , content);
RecordId recordId = redisTemplate.opsForStream().add(key, map);
return recordId;
}
public List read(String roomId , String username , String content){
String key = room_key + roomId;
StreamOffset<String> streamOffset = StreamOffset.create(key, ReadOffset.latest());
StreamReadOptions options = StreamReadOptions.empty().block(Duration.ofMinutes(10));
List list = redisTemplate.opsForStream().read(options, streamOffset);
return list;
}
}
布隆过滤器
- 布隆过滤器底层的存储结构是位图 、
- 判断一个key是否存在,就先通过几个hash函数得出在位图中的位置,判断位置上的值是否为1,全部为1,则存在,如果有一个为0 ,则说明该key不存在;
- 布隆过滤器典型应用场景就是 防止缓存穿透;
实现BloomFilter
添加Jedis依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
package com.redis.zset.filter;
import com.google.common.base.Preconditions;
import com.google.common.hash.Funnel;
import com.google.common.hash.Hashing;
public class BloomFilterHelper<T> {
private int numHashFunctions;
private int bitSize;
private Funnel<T> funnel;
public BloomFilterHelper(Funnel<T> funnel, int expectedInsertions, double fpp) {
Preconditions.checkArgument(funnel != null, "funnel不能为空");
this.funnel = funnel;
bitSize = optimalNumOfBits(expectedInsertions, fpp);
numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, bitSize);
}
public int[] murmurHashOffset(T value) {
int[] offset = new int[numHashFunctions];
long hash64 = Hashing.murmur3_128().hashObject(value, funnel).asLong();
int hash1 = (int) hash64;
int hash2 = (int) (hash64 >>> 32);
for (int i = 1; i <= numHashFunctions; i++) {
int nextHash = hash1 + i * hash2;
if (nextHash < 0) {
nextHash = ~nextHash;
}
offset[i - 1] = nextHash % bitSize;
}
return offset;
}
/**
* 计算bit数组长度
*/
private int optimalNumOfBits(long n, double p) {
if (p == 0) {
p = Double.MIN_VALUE;
}
return (int) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
}
/**
* 计算hash方法执行次数
*/
private int optimalNumOfHashFunctions(long n, long m) {
return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
}
}
public class RedisBloomFilter <T>{
private Jedis jedis;
public RedisBloomFilter(Jedis jedis) {
this.jedis = jedis;
}
public<T> void addByBloomFilter(BloomFilterHelper<T> filterHelper, String key, T value) {
int[] offset = filterHelper.murmurHashOffset(value);
for ( int i :offset) {
jedis.setbit(key,i , true);
}
}
public<T> Boolean includeByBloomFilter(BloomFilterHelper<T> filterHelper, String key, T value) {
int[] offset = filterHelper.murmurHashOffset(value);
for (int i = 0; i <offset.length ; i++) {
if (!jedis.getbit(key,offset[i])){
return false;
}
}
return true;
}
}
public class JedisBloomFilterTest {
public static void main(String[] args) throws IOException {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(10);
jedisPoolConfig.setMaxIdle(5);
jedisPoolConfig.setMinIdle(2);
// timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeout的构造函数
JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.0.60", 6380, 3000, null);
Jedis jedis = null;
try {
//从redis连接池里拿出一个连接执行命令
jedis = jedisPool.getResource();
//******* Redis测试布隆方法 ********
BloomFilterHelper<CharSequence> bloomFilterHelper = new BloomFilterHelper<>(Funnels.stringFunnel(Charset.defaultCharset()), 1000, 0.1);
RedisBloomFilter<Object> redisBloomFilter = new RedisBloomFilter<>(jedis);
int j = 0;
for (int i = 0; i < 100; i++) {
redisBloomFilter.addByBloomFilter(bloomFilterHelper, "bloom", i+"");
}
for (int i = 0; i < 1000; i++) {
boolean result = redisBloomFilter.includeByBloomFilter(bloomFilterHelper, "bloom", i+"");
if (!result) {
j++;
}
}
System.out.println("漏掉了" + j + "个");
} catch (Exception e) {
e.printStackTrace();
} finally {
//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
if (jedis != null)
jedis.close();
}
}
}