阅读视图

Redis数据结构之Bitfleid

1.概述

bitfield将一个字符串看作一个由二进制位构成的数组,并对这个数组中任何偏移进行访问,通过bitfield命令可以一次性操作多个比特位域(指的是连续的多个比特位),它会执行一系列操作并返回一个响应数组,这个数组中的元素对应参数列表中的相应操作的执行结果。

在实际应用中,bitfield并不是一种常用的数据结构。

  •  

Redis数据结构之Bitmap

1.概述

  • 由0和1状态表现的二进制位的bit数组(数组里每个元素只能是0或1)该数组由多个二进制位组成,每个二进制位都对应一个偏移量(我们称之为一个索引)。
  • 基于String数据类型的按位的操作。
  • Bitmap支持的最大位数是2³²位,它可以极大的节约存储空间,使用512M内存就可以存储多达42.9亿的字节信息(2³²= 4294967296)

2.常见操作

2.1 SETBIT

设置0或1到Bitmap

SETBIT key offset value
  • key
  • offset 偏移位,从0开始计算
  • value 只能是0或1

例:

127.0.0.1:6379> setbit k1 1 1(integer) 1127.0.0.1:6379> setbit k1 2 1(integer) 1127.0.0.1:6379> setbit k1 3 1(integer) 1

验证:Bitmap基于String数据类型的

127.0.0.1:6379> type k1string

2.2 GETBIT

获取0或1,不是1就默认0

127.0.0.1:6379> getbit k1 1(integer) 1127.0.0.1:6379> getbit k1 2(integer) 1127.0.0.1:6379> getbit k1 3(integer) 1127.0.0.1:6379> getbit k1 4(integer) 0

2.3 STRLEN

不是字符串长度而是Bitmap占据几个字节,按照8位一组扩容,统计有多少组

127.0.0.1:6379> setbit k3 0 1(integer) 0127.0.0.1:6379> setbit k3 7 1(integer) 0127.0.0.1:6379> strlen k3(integer) 1127.0.0.1:6379> setbit k3 8 1(integer) 0127.0.0.1:6379> strlen k3(integer) 2

2.4 BITCOUNT

一个bitmap中从START到END有多少个1,不加START|END统计全部

BITCOUNT KEY [START|END]

例:

127.0.0.1:6379> bitcount k3(integer) 3

2.5 BITOP

对bitmap进行二进制运算(与AND、或OR、非NOT、异或XOR)保存结果到DESTKEY KEY

BITOP AND|OR|NOT|XOR DESTKEY KEY [KEY ...]

例:

准备数据

127.0.0.1:6379> setbit bin1 0 1(integer) 0127.0.0.1:6379> setbit bin1 1 1(integer) 0127.0.0.1:6379> setbit bin1 2 0(integer) 0127.0.0.1:6379> setbit bin1 3 1(integer) 0127.0.0.1:6379> setbit bin2 0 0(integer) 0127.0.0.1:6379> setbit bin2 1 1(integer) 0127.0.0.1:6379> setbit bin2 2 1(integer) 0127.0.0.1:6379> setbit bin2 3 0

测试:BITOP AND

127.0.0.1:6379> BITOP AND bintemp bin1 bin2(integer) 1

查看写入到bintemp中的结果

127.0.0.1:6379> getbit bintemp 0(integer) 0127.0.0.1:6379> getbit bintemp 1(integer) 1127.0.0.1:6379> getbit bintemp 2(integer) 0127.0.0.1:6379> getbit bintemp 3(integer) 0

3.总结

命令作用时间复杂度
setbit给指定key的值的第offset赋值valO(1)
getbit获取指定key的第offset位O(1)
bitcount返回指定key中[start,end]中为1的数量O(n)
bitop对不同的二进制存储数据进行位运算(AND、OR、NOT、XOR)O(n)

使用场景:

1.统计签到、登录、打卡等,例如:ID为10001的用户2025年5月的第12个工作日未打卡

setbit sign:uid_10001:202505 12 0
  •  

Redis数据结构之GEO

1.概述

移动互联网时代LBS应用越来越多,交友软件中附近的人、外卖软件中附近的美食店铺等,那这种附近各种形形色色的地址位置选择是如何实现的?

地球上的地理位置是使用二维的经纬度表示,经度范围(-180,180],纬度范围(-90,90],只要我们确定一个点的经纬度就可以取得它在地球上的位置。

例如滴滴打车,最直观的操作就是实时记录更新各个车的位置,然后当我们要找车时,在数据库中查找距离我们(x0, y0)附近r公里范围内部的车辆,使用如下SQL即可:

select taxi from position where x0 - r < x < x0 + r and y0 - r < y < y0 + r

但是这样会有查询性能问题,如果并发高数据量大,这种查询会影响数据库性能,而且这个查询到的是一个矩形范围,而不是以点为中心r公里为半径的圆形范围。

为了解决这一问题,Redis在3.2版本之后支持了GEO这一数据结构,GEO主要用于存储地理位置信息,并对存储的信息进行操作,包括:

  • 添加地理位置的坐标
  • 获取地理位置的坐标
  • 计算两个位置之间的距离
  • 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合

2.常用命令

2.1 GEOADD

多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的key中

GEOADD key longitude latitude member [longitude latitude member ...]

经纬度可以通过百度地图坐标拾取获得:https://api.map.baidu.com/lbsapi/getpoint/index.html

例如:

127.0.0.1:6379> GEOADD city 116.515802 39.951893 "北京朝阳站" 116.385671 39.871141 "北京南站"(integer) 1

GEO的本质是Set的子类型:

127.0.0.1:6379> type cityzset127.0.0.1:6379> zrange city 0 -11) "\xe5\x8c\x97\xe4\xba\xac\xe5\x8d\x97\xe7\xab\x99"2) "\xe5\x8c\x97\xe4\xba\xac\xe7\xab\x99"3) "\xe5\x8c\x97\xe4\xba\xac\xe6\x9c\x9d\xe9\x98\xb3\xe7\xab\x99"127.0.0.1:6379> 

中文乱码的解决:修改客户端参数

127.0.0.1:6379> quit[root@localhost bin]# ./redis-cli --raw127.0.0.1:6379> auth lzjOK127.0.0.1:6379> zrange city 0 -1北京南站北京站北京朝阳站

2.2 GEOPOS

用于从给定的key里返回所有指定名称(member)的位置(经度和纬度),不存在的返回nil

GEOPOS key member [member ...]

127.0.0.1:6379> GEOPOS city 北京站116.4341434836387634339.90890963654599233127.0.0.1:6379> GEOPOS city  北京朝阳站116.5158006548881530839.95189343796607062

2.3 GEODIST

返回两个给定位置之间的距离

  • m
  • km千米
  • ft英尺
  • mi英里
GEODIST key memberl member2 [m|km|ft|mi]

例:高铁站之间的距离

127.0.0.1:6379> GEODIST city 北京站 北京朝阳站 km8.4477127.0.0.1:6379> GEODIST city 北京南站 北京朝阳站 km14.2804127.0.0.1:6379> GEOADD city 117.978057 40.893571 "承德南站"1127.0.0.1:6379> GEODIST city 承德南站 北京朝阳站 km162.1702

2.4 GEORADIUS

以给定的经纬度为中心,返回与中心的距离不超过给定最大距离的所有位置元素,说白了就是根据半径检索附近的POI(兴趣点)

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
  • key: 存储地理位置的有序集合键名
  • longitude latitude: 中心点的经度和纬度
  • radius: 半径距离
  • m|km|ft|mi: 距离单位(米、千米、英尺、英里)
  • WITHCOORD: 将位置元素的经度和维度也一并返回。
  • WITHDIST: 在返回位置元素的同时,将位置元素与中心之间的距离也一并返回, 距离的单位和用户给定的范围单位保持一致
  • WITHHASH: 以52位有符号整数的形式,返回位置元素经过原始geohash编码的有序集合分值,这个选项主要用于底层应用或者调试,实际中的作用并不大
  • COUNT count: 限制返回结果数量
  • ASC|DESC: 按距离排序(升序/降序)
  • STORE key: 将结果存储为有序集合(存储 geohash)
  • STOREDIST key: 将结果存储为有序集合(存储距离)

例:以朝阳公园(116.486817,39.953201)为圆心,10千米为半径,搜索city中满足条件的地点

127.0.0.1:6379> GEORADIUS city 116.486817 39.953201 10 km WITHCOORD WITHDIST北京站6.6672116.4341434836387634339.90890963654599233北京朝阳站2.4755116.5158006548881530839.95189343796607062

2.5 GEORADIUSBYMEMBER

跟GEORADIUS类似

2.6 GEOHASH

返回一个或多个位置元素的GEOHASH表示,比位数众多的小数更好处理

GEOHASH算法生成的base32编码值,主要分为三步:
1.将三维的地球变为二维的坐标
2.将二维的坐标转换为一维的点块
3.最后将一维的点块转换为二进制再通过base32编码

GEOHASH key member [member ...]

127.0.0.1:6379> GEOHASH city 北京站 北京南站 北京朝阳站wx4g190y0p0wx4fb6m6nx0wx4g73h0wu0
  •  

Redis数据结构之Hash

1.概述

  • Hash是一个String类型的field(字段)和value(值)的映射表,而且value是一个键值对集合,类似Map<String, Map<Object, Object>>,Hash特别适合用于存储对象。
  • 每个Hash可以存储2³²-1个键值对 (40多亿)。

2.常见操作

2.1 H(M)SET/H(M)GET

HSET/HGET,设置和获取hash的键值对

语法:

hset key field value [field value ......] 

例:

127.0.0.1:6379> hset user:01 name liming(integer) 1hset user:01 id 1 name liming age 30

语法:

hget key field [field ......] 

例:

127.0.0.1:6379> hget user:01 name"liming"127.0.0.1:6379> hget user:01 age"30"127.0.0.1:6379> hget user:01 id"1"

HMSET,一次批量设置hash的多个值

127.0.0.1:6379> HMSET user:02 id 02 name lisiOK

从 Redis 4.0.0 开始,HSET 也支持批量设置,HMSET 被视为已弃用,但仍可使用。

HMGET,一次获取某个hash的多个值

127.0.0.1:6379> hmget user:02 id name 1) "02"2) "lisi"

2.2 HGETALL

获取一个hash中的所有键值

127.0.0.1:6379> hgetall user:011) "name"2) "liming"3) "id"4) "1"5) "age"6) "30"

2.3 HDEL

删除hash中的某个键值对

127.0.0.1:6379> hdel user:01 age(integer) 1127.0.0.1:6379> hgetall user:011) "name"2) "liming"3) "id"4) "1"

2.4 HLEN

hash中键值对数量

127.0.0.1:6379> hlen user:01(integer) 2

2.5 HEXISTS

hash中某个键是否存在

127.0.0.1:6379> HEXISTS user:01 age(integer) 0127.0.0.1:6379> HEXISTS user:01 name(integer) 1

2.6 HKEYS/HVALS

获取一个hash中所有的键/值

127.0.0.1:6379> HKEYS user:011) "name"2) "id"
127.0.0.1:6379> HVALS user:011) "liming"2) "1"

2.7 HINCRBY

对hash中某个键的值进行自增

127.0.0.1:6379> hmset user:03 age 13OK127.0.0.1:6379> hincrby user:03 age 1(integer) 14127.0.0.1:6379> hincrby user:03 age 2(integer) 16

2.8 HSETNX

不存在就赋值,如已存在则无效

127.0.0.1:6379> hgetall user:031) "age"2) "16"127.0.0.1:6379> HSETNX user:03 name liming(integer) 1127.0.0.1:6379> HSETNX user:03 age 3(integer) 0

3.总结

KV键值对的结构,适合早期的购物车等场景

  •  

Redis数据结构之HyperLogLog

1.概述

基数统计是一种去重复统计功能的基数估计算法,HyperLogLog是用来做基数统计的数据结构,HyperLogLog的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定且是很小的。

在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2⁶⁴个不同元素的基数,这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

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

去重类似Java的HashSet,只是不能存储数据

2.常用命令

2.1 添加元素

PFADD key element [element ..]

127.0.0.1:6379> pfadd pf1 1 2 3 4 5 6 5 4 5 6 7 34 2 5 6 4(integer) 1127.0.0.1:6379> pfadd pf2 1 2 3 4 5 6 5 4 43 4 21 65 32 54 (integer) 1

hyperloglog本质上是string

127.0.0.1:6379> type pf1string

2.2 返回基数估算值

PFCOUNT key [key....]

例:去重后只有8个元素

127.0.0.1:6379> PFCOUNT pf1(integer) 8

2.3 合并hyperloglog

合并到新的destkey中

PFMERGE destkey sourcekey [sourcekey...]

例:合并pf1和pf2为pfm

127.0.0.1:6379> PFMERGE pfm pf1 pf2OK127.0.0.1:6379> pfcount pfm(integer) 13

3.总结

使用场景:

1.网站UV统计,文章阅读数UV统计

  •  

Redis数据结构之List

1.概述

  • List是简单的字符串列表,单key多个value,按照插入顺序排序。

  • 支持添加一个元素到列表的头部(左边)或者尾部(右边)

  • 它的底层实际是个双端链表,主要功能有push/pop等,用在栈,队列,消息队列等场景,left/right都可以插入添加,如果键不存在创建新的链表,键已存在,则新增内容,如果值全被移除了,对应的键也就消失了。

    双端链表两端操作的效率很高,通过索引下标的操作性能略有下降

  • 最多可以包含2³²-1个元素 (4294967295, 每个列表超过40亿个元素)。

2.常见操作

2.1 LPUSH/RPUSH/LRANGE

LPUSH将一个或多个值插入头部(左边),RPUSH是将一个或多个值插入尾部(右边)

案例:

127.0.0.1:6379> lpush list1 1 2 3 4 5(integer) 5127.0.0.1:6379> rpush list2 1 2 3 4 5(integer) 5

LRANGE从头部开始遍历,获取元素,下标从0开始,0到-1是全部遍历

案例:

127.0.0.1:6379> LRANGE list1 0 31) "5"2) "4"3) "3"4) "2"127.0.0.1:6379> LRANGE list2 0 -11) "1"2) "2"3) "3"4) "4"5) "5"127.0.0.1:6379> LRANGE list1 0 -11) "5"2) "4"3) "3"4) "2"5) "1"

不存在RRANGE命令

2.2 LPOP/RPOP

LPOP,从左侧弹出(移除)元素,RPOP从右侧弹出元素,被弹出的元素会被返回。

127.0.0.1:6379> lrange list1 0 -11) "5"2) "4"3) "3"4) "2"5) "1"127.0.0.1:6379> lpop list1 "5"127.0.0.1:6379> lpop list1 "4"127.0.0.1:6379> lpop list1 "3"127.0.0.1:6379> lpop list1 "2"127.0.0.1:6379> lpop list1 "1"
127.0.0.1:6379> lrange list2 0 -11) "1"2) "2"3) "3"4) "4"5) "5"127.0.0.1:6379> rpop list2"5"127.0.0.1:6379> rpop list2"4"127.0.0.1:6379> rpop list2"3"127.0.0.1:6379> rpop list2"2"127.0.0.1:6379> rpop list2"1"

2.3 LINDEX

根据下标获取元素

127.0.0.1:6379> rpush list1 1 2 3 4 5(integer) 5127.0.0.1:6379> lindex list1 0"1"127.0.0.1:6379> lindex list1 2"3"

2.4 LLEN

元素个数,list.size();

127.0.0.1:6379> lrange list1 0 -11) "1"2) "2"3) "3"4) "4"5) "5"127.0.0.1:6379> llen list1(integer) 5

2.5 LREM

删除num个值是value的元素

lrem key num value

例:

127.0.0.1:6379> lpush list3 1 2 3 4 5 5 5 5 5 6 7 8(integer) 12127.0.0.1:6379> lrange list3 0 -1 1) "8" 2) "7" 3) "6" 4) "5" 5) "5" 6) "5" 7) "5" 8) "5" 9) "4"10) "3"11) "2"12) "1"127.0.0.1:6379> lrem list3 2 5(integer) 2127.0.0.1:6379> lrange list3 0 -1 1) "8" 2) "7" 3) "6" 4) "5" 5) "5" 6) "5" 7) "4" 8) "3" 9) "2"10) "1"

2.6 LTRIM

截取指定范围的值后再赋给key

LTRIM key start end

例:

127.0.0.1:6379> rpush list1 1 2 3 4 5 6 (integer) 6127.0.0.1:6379> lrange list1 0 -11) "1"2) "2"3) "3"4) "4"5) "5"6) "6"127.0.0.1:6379> LTRIM list1 3 5OK127.0.0.1:6379> lrange list1 0 -11) "4"2) "5"3) "6"

2.7 RPOPLPUSH

移除列表的最后一个元素,并将该元素添加到另一个列表并返回

RPOPLPUSH key source target

例:

127.0.0.1:6379> lrange list1 0 -11) "1"2) "2"3) "3"4) "4"127.0.0.1:6379> lrange list2 0 -11) "5"2) "6"3) "7"4) "8"127.0.0.1:6379> rpoplpush list1 list2"4"127.0.0.1:6379> lrange list1 0 -11) "1"2) "2"3) "3"127.0.0.1:6379> lrange list2 0 -11) "4"2) "5"3) "6"4) "7"5) "8"

2.8 LSET

设置某个下标的值

LSET key index value

例:

127.0.0.1:6379> lrange list2 0 -11) "4"2) "5"3) "6"4) "7"5) "8"127.0.0.1:6379> lset list2  3 abcOK127.0.0.1:6379> lrange list2 0 -11) "4"2) "5"3) "6"4) "abc"5) "8"

2.9 LINSERT

在某个已有值existValue前或后加个新的值newValue

LINSERT key before|after existValue newValue

例:

127.0.0.1:6379> lrange list2 0 -11) "4"2) "5"3) "6"4) "abc"5) "8"127.0.0.1:6379> LINSERT list2 before abc def(integer) 6127.0.0.1:6379> lrange list2 0 -11) "4"2) "5"3) "6"4) "def"5) "abc"6) "8"
  •  

Redis数据结构之Set

1.概述

  • Set是String类型的无序集合,集合成员是唯一的,这就意味着集合中不能出现重复的数据,集合对象的编码可以是intset或者hashtable。
  • Set是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
  • Set中最大的成员数为2³²-1 (4294967295,每个集合可存储40多亿个成员)。

2.常见操作

2.1 SADD

添加元素到set中,不能重复添加

127.0.0.1:6379> sadd set1 a b c d e f g(integer) 7127.0.0.1:6379> sadd set2 a a a b b b c c c(integer) 3

2.2 SMEMBERS

查看set中所有元素

127.0.0.1:6379> SMEMBERS set11) "a"2) "g"3) "c"4) "f"5) "d"6) "e"7) "b"127.0.0.1:6379> SMEMBERS set21) "a"2) "c"3) "b"

2.3 SISMEMBER

判断set中是否有某个元素

127.0.0.1:6379> SISMEMBER set2 a(integer) 1127.0.0.1:6379> SISMEMBER set2 d(integer) 0

2.4 SREM

从set 中移除元素

127.0.0.1:6379> SMEMBERS set11) "a"2) "g"3) "c"4) "f"5) "d"6) "e"7) "b"127.0.0.1:6379> srem set1 g(integer) 1127.0.0.1:6379> SMEMBERS set11) "a"2) "c"3) "f"4) "d"5) "e"6) "b"

2.5 SCARD

set中元素个数

127.0.0.1:6379> SCARD set1(integer) 6

2.6 SRANDMEMBER

从set中随机选择n个元素(不会删除)

SRANDMEMBER key n

例:

127.0.0.1:6379> SMEMBERS set11) "a"2) "c"3) "f"4) "d"5) "e"6) "b"127.0.0.1:6379> SRANDMEMBER set1 21) "f"2) "e"127.0.0.1:6379> SRANDMEMBER set1 21) "a"2) "e"

2.7 SPOP

从set中随机弹出(删除)n个元素

SPOP key n

例:

127.0.0.1:6379> SMEMBERS set11) "a"2) "c"3) "f"4) "d"5) "e"6) "b"127.0.0.1:6379> SPOP set1 21) "c"2) "a"127.0.0.1:6379> SPOP set1 21) "b"2) "f"127.0.0.1:6379> SPOP set1 21) "d"2) "e"127.0.0.1:6379> SPOP set1 2(empty list or set)

2.8 SMOVE

将set1中的v移动到set2中去

smove key key value

例:

127.0.0.1:6379> sadd set1 1 2 3(integer) 3127.0.0.1:6379> sadd set2 a b c(integer) 3127.0.0.1:6379> smove set1 set2 2(integer) 1127.0.0.1:6379> SMEMBERS  set11) "1"2) "3"127.0.0.1:6379> SMEMBERS  set21) "a"2) "2"3) "c"4) "b"

2.9 集合运算

127.0.0.1:6379> smembers set31) "1"2) "c"3) "b"4) "a"5) "2"6) "3"127.0.0.1:6379> smembers set41) "c"2) "b"3) "2"4) "5"5) "4"6) "d"7) "3"

1.差集运算

set3-set4:set3有set4没有

127.0.0.1:6379> sdiff  1) "a"2) "1"

2.并集运算

语法

sunion key key [key ......]  

例:

127.0.0.1:6379> sunion set3 set41) "c"2) "4"3) "5"4) "1"5) "3"6) "b"7) "a"8) "2"9) "d"

3.交集运算

sinter,同时属于几个集合的公共部分

sinter key key [key ...]  

例:

127.0.0.1:6379> sinter set3 set41) "c"2) "b"3) "2"4) "3"

SINTERCARD,redis7新命令,不返回结果集,只返回结果的基数,返回由所有给定集合的交集产生的集合的基数

SINTERCARD numkeys key [key ...] [LIMIT limit]

例:2个集合,set3和set4,交集中共有4个元素,可以限制返回的个数,但是不能超过元素的总个数

127.0.0.1:6379> SINTERCARD  2 set3 set4(integer) 4127.0.0.1:6379> SINTERCARD  2 set3 set4 limit 1(integer) 1127.0.0.1:6379> SINTERCARD  2 set3 set4 limit 2(integer) 2127.0.0.1:6379> SINTERCARD  2 set3 set4 limit 3(integer) 3127.0.0.1:6379> SINTERCARD  2 set3 set4 limit 4(integer) 4127.0.0.1:6379> SINTERCARD  2 set3 set4 limit 5(integer) 4

3.总结

Set集合的使用场景很多,例如:

  1. 可能认识的人或共同感兴趣的话题,商品
    sdiff a b
    sdiff b a

  2. 年会抽奖活动
    sadd 活动key 用户ID:参与抽奖
    scard 活动key:统计参加总人数
    SRANDMEMBER 活动key n:抽取n个幸运的人,这几个人还能继续抽奖
    spop 活动key n:抽取n个幸运的人,这几个人不能继续抽奖

  •  

Redis数据结构之Stream

1.概述

Redis Stream是Redis 5.0版本新增加的数据结构。

Redis Stream主要用于消息队列(MQ,Message Queue),Redis本身是有一个Redis发布订阅(pub/sub)来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis宕机等,消息就会被丢弃,而且没有ACK机制来保证数据的可靠性,假设一个消费者都没有,那消息就直接被丢弃了,简单来说发布订阅(pub/sub)可以分发消息,但无法记录历史消息。

而Redis Stream提供了消息的持久化和主备复制功能,支持自动生成全局唯一ID、支持ack确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

Redis Stream是一个链表,会将所有加入的消息都串起来,每个消息都有一个唯一的ID和对应的内容

Redis Stream组成部分说明:

  • Message Content

    消息

  • Consumer group

    消费组,通过XGROUP CREATE命令创建,同一个消费组可以有多个消费者

  • Last_delivered_id

    游标,每个消费组会有个游标last_delivered_id,任意一个消费者读取了消息都会使游标last_delivered_id往前移动

  • Consumer

    消费者,包含在消费组当中

  • Pending_ids

    消费者会布一个状态变量,用于记录被当前消费者已读取但未ack的消息id,如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack,它就开始减少。这个pending_ids变量在Redis官方被称之为PEL(Pending Entries List),记录了当前已经被客户端读取的消息,但是还没有ack(Acknowledge character:确认字符),它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理

2.队列(生产)相关命令

2.1 XADD

添加消息到队列末尾,如果队列不存在,则会先创建队列

  • key
  • *|id 消息的ID,格式必须是时间戳-序列号这样的方式,下一条的ID必须比上一条的要大,写成*号,系统将自动生成。ID比较大小时,先比较时间戳大小,若相同再比较序列号
  • field value 键值对
XADD <key> <*|id> <field value> [<field value ...>]

127.0.0.1:6379> XADD mystream * name lzj age 28"1758432825026-0"127.0.0.1:6379> XADD mystream * name xxn age 24"1758432832463-0"

返回的1748835683065-01748835967616-0就是消息的ID,-前的数字为生成消息时的毫秒级时间戳,-后面的数字代表同一毫秒内产生的第几个序列号

在相同的毫秒下序列号从0开始递增,序列号是64位长度,理论上在同一毫秒内生成的数据量无法到达这个级别,因此不用担心序列号会不够用。毫秒数取的是Redis节点服务器的本地时间,如果存在当前的毫秒时间戳比以前已经存在的数据的时间戳小的话(本地时间钟后跳),那么系统将会采用以前相同的毫秒创建新的ID,也即redis在增加信息条目时会检査当前id与上一条目的id,自动纠正错误的情况,一定要保证后面的id比前面大,一个流中信息条目的ID必须是单调增的,这是stream的基础。

Stream的数据类型就是Stream

127.0.0.1:6379> type mystreamstream

2.2 XRANGE

获取消息列表(可以指定范围),忽略删除的消息

  • key 对应的stream
  • start 开始值,用-表示最小
  • end 结束值,用+表示最小
  • count 限制条数
XRANGE <key> <start> <end> [Count <count>]

127.0.0.1:6379> xrange mystream - + 1) 1) "1758432825026-0"   2) 1) "name"      2) "lzj"      3) "age"      4) "28"2) 1) "1758432832463-0"   2) 1) "name"      2) "xxn"      3) "age"      4) "24"

2.3 XREVRANGE

和XRANGE相比,区别在于反向获取,ID从大到小

127.0.0.1:6379> xrevrange mystream + - 1) 1) "1758432832463-0"   2) 1) "name"      2) "xxn"      3) "age"      4) "24"2) 1) "1758432825026-0"   2) 1) "name"      2) "lzj"      3) "age"      4) "28"

2.4 XDEL

删除消息,按照ID删除

XDEL <key> <id> [<id ...>]

XDEL mystream 1748835967616-0

2.5 XLEN

获取Stream中的消息长度

127.0.0.1:6379> XLEN mystream2

2.6 XTRIM

限制Stream的长度,如果已经超长会进行截取

  • MAXLEN 允许的最大长度,对流进行修剪限制长度
  • MINID 允许的最小id,从某个id值开始比该id值小的将会被抛弃
XTRIM <stream> MAXLEN|MINID <n>

127.0.0.1:6379> XTRIM mystream MAXLEN 24
127.0.0.1:6379> XTRIM mystream MINID 1748841640027-01

2.7 XREAD

获取消息(阻塞/非阻塞),返回大于指定ID的消息

  • COUNT 最多读取多少条
  • BLOCK 是否以阻塞的方式读取消息,默认不阻塞,如果miliseconds设置为0,表示永远阻塞
  • STREAMS 队列
  • ID 指定的ID,用$代表队列内现存最大的ID的后一个ID,用0-0(或0; 00)代表队列中最小的ID
XREAD [COUNT <count>] [BLOCK <milliseconds>] STREAMS <key> [<key ...>] ID [<id ...>]

例:用$代表队列内现存最大的ID的后一个ID,因为该ID并不存在所以会返回空

127.0.0.1:6379> Xrange mystream - +1) 1) "1758432825026-0"   2) 1) "name"      2) "lzj"      3) "age"      4) "28"2) 1) "1758432832463-0"   2) 1) "name"      2) "xxn"      3) "age"      4) "24"127.0.0.1:6379> XREAD  STREAMS mystream  $(nil)

例:用0-0代表队列中最小的ID,当指定为0-0的同时不指定count时,会返回队列中所有的元素

127.0.0.1:6379> Xrange mystream - +1) 1) "1758432825026-0"   2) 1) "name"      2) "lzj"      3) "age"      4) "28"2) 1) "1758432832463-0"   2) 1) "name"      2) "xxn"      3) "age"      4) "24"127.0.0.1:6379> XREAD  STREAMS mystream  0-01) 1) "mystream"   2) 1) 1) "1758432825026-0"         2) 1) "name"            2) "lzj"            3) "age"            4) "28"      2) 1) "1758432832463-0"         2) 1) "name"            2) "xxn"            3) "age"            4) "24"

例:阻塞:监听mystream中比最新的一条还靠后的一条,读取不到就会阻塞,一直监听

127.0.0.1:6379> XREAD COUNT 1 BLOCK 0  STREAMS mystream $

打开另一个命令窗口,生产消息

127.0.0.1:6379> xadd mystream * k1 v1"1758434052255-0"

XREAD停止阻塞,打印出新生产的一条消息和等待时间

127.0.0.1:6379> XREAD COUNT 1 BLOCK 0  STREAMS mystream $1) 1) "mystream"   2) 1) 1) "1758434052255-0"         2) 1) "k1"            2) "v1"(183.38s)

3.消费(组)相关命令

3.1 XGROUP CREATE

创建消费组

  • stresm 队列
  • group 消费组
  • id 创建消费组时必须指定ID,0代表从头消费,$代表从队尾消费(只消费最新消息)
XGROUP CREATE <stresm> <group> <id>

例:

127.0.0.1:6379> xgroup create mystream group1 $OK127.0.0.1:6379> xgroup create mystream group2 0OK

3.2 XREADGROUP GROUP

允许多个消费者作为一个组来合作消费同一个stream中的消息,同一个stream中的消息一旦被消费组里面的一个消费者读取了,同组的其他消费者就无法再次读取

  • group 消费组
  • consumer 消费者
  • stream 队列
  • id 指定从哪个ID开始读取,特殊写法:>代表获取组内未分发给其他消费者的新消息(未分发必然未ACK),0代表获取已分发但未被消费者ACK的消息
  • count 读取的数量,可以用于每个消费者读取一部分消息,实现消费的负载均衡
XREADGROUP GROUP <group> <consumer> [COUNT <count>] STREAMS <stream, ...> <id>

例:同组消费者不能重复消费消息

先建两个消费组

127.0.0.1:6379> xgroup create mystream groupA 0OK127.0.0.1:6379> xgroup create mystream groupB 0OK

groupA里面的新建消费者consumer1进行消费

127.0.0.1:6379> XREADGROUP GROUP groupA consumer1 STREAMS mystream >1) 1) "mystream"   2) 1) 1) "1758432825026-0"         2) 1) "name"            2) "lzj"            3) "age"            4) "28"      2) 1) "1758432832463-0"         2) 1) "name"            2) "xxn"            3) "age"            4) "24"      3) 1) "1758434052255-0"         2) 1) "k1"            2) "v1"

groupA中再去新建消费者consumer2进行消费,无法再消费消息

127.0.0.1:6379> XREADGROUP GROUP groupA consumer2 STREAMS mystream >(nil)

但是,groupB中新建一个消费者取消费,可以读取到消息

127.0.0.1:6379> XREADGROUP GROUP groupB consumer1 STREAMS mystream >1) 1) "mystream"   2) 1) 1) "1758432825026-0"         2) 1) "name"            2) "lzj"            3) "age"            4) "28"      2) 1) "1758432832463-0"         2) 1) "name"            2) "xxn"            3) "age"            4) "24"      3) 1) "1758434052255-0"         2) 1) "k1"            2) "v1"

例:负载均衡的消费,新建消费组groupC,三个消费者每个消费一条消息

127.0.0.1:6379> xgroup create mystream groupC 0OK127.0.0.1:6379> XREADGROUP GROUP groupC consumer1 COUNT 1 STREAMS mystream >1) 1) "mystream"   2) 1) 1) "1758432825026-0"         2) 1) "name"            2) "lzj"            3) "age"            4) "28"127.0.0.1:6379> XREADGROUP GROUP groupC consumer2 COUNT 1 STREAMS mystream >1) 1) "mystream"   2) 1) 1) "1758432832463-0"         2) 1) "name"            2) "xxn"            3) "age"            4) "24"127.0.0.1:6379> XREADGROUP GROUP groupC consumer3 COUNT 1 STREAMS mystream >1) 1) "mystream"   2) 1) 1) "1758434052255-0"         2) 1) "k1"            2) "v1"

3.3 消息的ACK机制

基于Stream的消息,怎样保证消费者发生故障或宕机以后,仍然能读取未处理完的消息?Stream采用的是一个内部队列pending_list,记录消费组中消费者的读取记录,直到消费者使用xack命令来通知stream消息已经处理完成,这种消费确认机制增强了消息的可靠性。

刚刚的消费操作仅仅是对消息的读取,实际上并没有ACK“签收”

例:通过命令XPENDING查询groupC中已读取,但未确认的消息

127.0.0.1:6379> XPENDING mystream groupC1) (integer) 3 #总数2) "1758432825026-0" #起始ID3) "1758434052255-0" #结束ID4) 1) 1) "consumer1" #每个消费组消费的消息数量      2) "1"   2) 1) "consumer2"      2) "1"   3) 1) "consumer3"      2) "1"

例:通过命令XPENDING查询groupB中consumer1已读取,但未确认的消息,从小到大查询10个

127.0.0.1:6379> XPENDING mystream groupB - + 10  consumer11) 1) "1758432825026-0"   2) "consumer1"   3) (integer) 2848434   4) (integer) 12) 1) "1758432832463-0"   2) "consumer1"   3) (integer) 2848434   4) (integer) 13) 1) "1758434052255-0"   2) "consumer1"   3) (integer) 2848434   4) (integer) 1

例:使用XACK向消息队列确认groupB组的消息1758432825026-0已经处理完成

127.0.0.1:6379> XACK mystream groupB 1758432825026-0(integer) 1
127.0.0.1:6379> XPENDING mystream groupB - + 10  consumer11) 1) "1758432832463-0"   2) "consumer1"   3) (integer) 3301753   4) (integer) 12) 1) "1758434052255-0"   2) "consumer1"   3) (integer) 3301753   4) (integer) 1

4.XINFO

XINFO命令用于打印出一些stream结构相关的信息

例:XINFO stream打印出mystream队列的详细信息

127.0.0.1:6379> XINFO stream  mystream 1) "length" 2) (integer) 3 3) "radix-tree-keys" 4) (integer) 1 5) "radix-tree-nodes" 6) (integer) 2 7) "last-generated-id" 8) "1758434052255-0" 9) "max-deleted-entry-id"10) "0-0"11) "entries-added"12) (integer) 313) "recorded-first-entry-id"14) "1758432825026-0"15) "groups"16) (integer) 517) "first-entry"18) 1) "1758432825026-0"    2) 1) "name"       2) "lzj"       3) "age"       4) "28"19) "last-entry"20) 1) "1758434052255-0"    2) 1) "k1"       2) "v1"

例:XINFO groups打印出mystream队列队列上存在的消费组信息

127.0.0.1:6379> XINFO  groups mystream1)  1) "name"    2) "group1"    3) "consumers"    4) (integer) 0    5) "pending"    6) (integer) 0    7) "last-delivered-id"    8) "1758434052255-0"    9) "entries-read"   10) (nil)   11) "lag"   12) (integer) 02)  1) "name"    2) "group2"    3) "consumers"    4) (integer) 0    5) "pending"    6) (integer) 0    7) "last-delivered-id"    8) "0-0"    9) "entries-read"   10) (nil)   11) "lag"   12) (integer) 33)  1) "name"    2) "groupA"    3) "consumers"    4) (integer) 2    5) "pending"    6) (integer) 3    7) "last-delivered-id"    8) "1758434052255-0"    9) "entries-read"   10) (integer) 3   11) "lag"   12) (integer) 04)  1) "name"    2) "groupB"    3) "consumers"    4) (integer) 1    5) "pending"    6) (integer) 2    7) "last-delivered-id"    8) "1758434052255-0"    9) "entries-read"   10) (integer) 3   11) "lag"   12) (integer) 05)  1) "name"    2) "groupC"    3) "consumers"    4) (integer) 3    5) "pending"    6) (integer) 3    7) "last-delivered-id"    8) "1758434052255-0"    9) "entries-read"   10) (integer) 3   11) "lag"   12) (integer) 0
  •  

Redis数据结构之String

1.概述

  • String是最常用的数据类型,一个key对应一个value。
  • String是二进制安全的,可以包含任何数据(例如图片和序列化对象),支持序列化。
  • 单个Value最大512MB。

2.常见操作

2.1 SET/GET

语法:

[ ]是可选的参数

SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]

SET命令有EXPXNXXX以及KEEPTTL五个可选参数,其中KEEPTTL为6.0版本添加的可选参数,其它为2.6.12版本添加的可选参数。

  • EX seconds 以秒为单位设置过期时间
  • PX milliseconds 以毫秒为单位设置过期时间
  • EXAT timestamp 设置以秒为单位的UNIX时间戳所对应的时间为过期时间
  • PXAT milliseconds-timestamp 设置以毫秒为单位的UNIX时间戳所对应的时间为过期时间
  • NX 键不存在的时候设置键值
  • XX 键存在的时候设置键值
  • KEEPTTL 保留设置前指定键的生存时间
  • GET 返回指定键原本的值,若键不存在时返回nil

SET命令使用EXPXNX参数,其效果等同于SETEXPSETEXSETNX命令。根据官方文档的描述,未来版本中SETEXPSETEXSETNX命令可能会被淘汰。

EXNX可用于分布式锁。

案例:最常用的set/get

127.0.0.1:6379> set k1 v1OK127.0.0.1:6379> get k1"v1"

案例:NX,键不存在才能创建,否则不能创建

127.0.0.1:6379> set k1 v1 nxOK127.0.0.1:6379> set k1 v1 nx(nil)

案例:XX,已存在的才创建,否则不能创建

127.0.0.1:6379> set k1 v1 OK127.0.0.1:6379> set k1 v1 xxOK127.0.0.1:6379> get k2(nil)127.0.0.1:6379> set k2 v2 xx(nil)

案例:GET,设置新的值前先把旧的值返回

127.0.0.1:6379> set k1 v1OK127.0.0.1:6379> set k1 v2 get"v1"

案例:EX,10秒过期

127.0.0.1:6379> set k1 v1 ex 10OK127.0.0.1:6379> ttl k1(integer) 8127.0.0.1:6379> ttl k1(integer) 6127.0.0.1:6379> ttl k1(integer) 4

set ex是原子操作,和先set key value然后expire key是不同的,后者不是原子的

案例:PX,9000毫秒过期

127.0.0.1:6379> set k1 v1 px 9000OK127.0.0.1:6379> ttl k1(integer) 7127.0.0.1:6379> ttl k1(integer) 5127.0.0.1:6379> ttl k1(integer) 4

案例:KEEPTTL

同一个key如果设置了新的值,又没有追加过期时间,redis会令其立即过期

127.0.0.1:6379> set k1 v1 ex 40OK127.0.0.1:6379> ttl k1(integer) 37127.0.0.1:6379> set k1 v2OK127.0.0.1:6379> ttl k1(integer) -1

如果需要续接过期时间,就需要用到参数KEEPTTL,设置新值后,过期时间会被续接下来

127.0.0.1:6379> set k1 v1 ex 50  OK127.0.0.1:6379> ttl k1(integer) 46127.0.0.1:6379> set k1 v1 keepttlOK127.0.0.1:6379> ttl k1(integer) 33

2.2 MSET/MGET/MSETNX

案例:MSET同时设置和获取多个值

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3OK127.0.0.1:6379> mget k1 k2 k31) "v1"2) "v2"3) "v3"

案例 MSETNX,同时设置多个key的值,且key不存在才设置,只会同时成功或同时失败

失败,因为k1已经存在,k1没有成功修改,k2也根本存不进去

127.0.0.1:6379> flushdbOK127.0.0.1:6379> set k1 v1OK127.0.0.1:6379> msetnx k1 a1 k2 v2(integer) 0127.0.0.1:6379> get k1"v1"127.0.0.1:6379> get k2(nil)

成功,k1,k2都不存在,全部添加成功

127.0.0.1:6379> flushdbOK127.0.0.1:6379> msetnx k1 v1 k2 v2(integer) 1127.0.0.1:6379> mget k1 k21) "v1"2) "v2"

2.3 GETRANGE/SETRANGE

GETRANGE,类似Java中的substring(),字符串截取, 0到-1代表不截取

案例:

127.0.0.1:6379> set k1 abcdefgOK127.0.0.1:6379> getrange k1 0 -1"abcdefg"127.0.0.1:6379> getrange k1 1 4"bcde"

SETRANGE,从第几个字符开始设置新的内容

案例:

127.0.0.1:6379> set k1 abcdefgOK127.0.0.1:6379> setrange k1 1 xxyy(integer) 7127.0.0.1:6379> get k1"axxyyfg"

2.4 INCR(BY)/DECR(BY)

数值的加减,值一定要是数字才能进行这个操作

案例:INCR,每次执行加1

127.0.0.1:6379> set k1 100OK127.0.0.1:6379> get k1"100"127.0.0.1:6379> incr k1(integer) 101127.0.0.1:6379> incr k1(integer) 102127.0.0.1:6379> incr k1(integer) 103127.0.0.1:6379> incr k1(integer) 104

案例:INCRBY,修改步长为5

127.0.0.1:6379> set k1 0OK127.0.0.1:6379> incrby k1 5(integer) 5127.0.0.1:6379> incrby k1 5(integer) 10127.0.0.1:6379> incrby k1 5

案例:DECR,递减1,DECRBY同理

127.0.0.1:6379> set k1 100OK127.0.0.1:6379> decr k1(integer) 99127.0.0.1:6379> decr k1(integer) 98127.0.0.1:6379> decr k1(integer) 97
127.0.0.1:6379> set k1 100OK127.0.0.1:6379> decrby k1 5(integer) 95127.0.0.1:6379> decrby k1 5(integer) 90127.0.0.1:6379> decrby k1 5(integer) 85

2.5 STRLEN

字符串长度

语法

strlen key

案例

127.0.0.1:6379> set k1 aaaOK127.0.0.1:6379> strlen k1(integer) 3

2.6 APPEND

字符串追加

语法

APPEND key value

案例

127.0.0.1:6379> set k1 aaaOK127.0.0.1:6379> append k1 bbb(integer) 6127.0.0.1:6379> get k1"aaabbb"

2.7 GETSET

getset,顾名思义,先取值在设置新的值进去,和set key value get命令相同

127.0.0.1:6379> set k1 v1OK127.0.0.1:6379> getset k1 v2"v1"127.0.0.1:6379> get k1"v2"

3.小结

字符串是一个最基本的数据结构,可用于分布式锁,点赞数量统计等场景。

  •  

Redis数据结构之ZSet

1.概述

  • ZSet和Set一样也是String类型元素的集合,且不允许重复的成员,不同的是ZSet每个元素都会关联一个double类型的分数,Redis正是通过分数来为集合中的成员进行从小到大的排序。
  • ZSet的成员是唯一的,但分数(score)却可以重复。
  • ZSet集合是通过哈希表实现的,所以添加,删除,査找的复杂度都是O(1)。
  • ZSet集合中最大的成员数为2³²-1。

2.常见操作

2.1 ZADD

向有序集合中添加元素和元素的分数

ZADD key score member [score member ...]

例:

127.0.0.1:6379> zadd zset1 10 v1 20 v2 30 v3 40 v4(integer) 4

2.2 ZRANGE

遍历,0到-1代表遍历所有,WITHSCORES结果带着分数

ZRANGE key start stop [WITHSCORES]

例:遍历

127.0.0.1:6379> zrange zset1 0 -11) "v1"2) "v2"3) "v3"4) "v4"

例:遍历,结果带着分数

127.0.0.1:6379> zrange zset1 0 -1 withscores1) "v1"2) "10"3) "v2"4) "20"5) "v3"6) "30"7) "v4"8) "40"

2.3 ZREVRANGE

根据分数反转

127.0.0.1:6379> ZREVRANGE zset1 0 -11) "v4"2) "v3"3) "v2"4) "v1"127.0.0.1:6379> ZREVRANGE zset1 0 -1 withscores1) "v4"2) "40"3) "v3"4) "30"5) "v2"6) "20"7) "v1"8) "10"

2.4 ZRANGEBYSCORE

获取指定分数范围的元素

ZRANGEBYSCORE key [(]min max [WITHSCORES] [LIMIT offset count]

(:不包含
min:分数from
max:分数to
offset:开始下标
count:数量

例:获取分数区间[20, 30]的元素

127.0.0.1:6379> ZRANGE zset1 0 -1 withscores1) "v1"2) "10"3) "v2"4) "20"5) "v3"6) "30"7) "v4"8) "40"127.0.0.1:6379> ZRANGEBYSCORE zset1 20 301) "v2"2) "v3"127.0.0.1:6379> ZRANGEBYSCORE zset1 20 30 withscores1) "v2"2) "20"3) "v3"4) "30"

例:获取分数区间(20, 40]的元素

127.0.0.1:6379> ZRANGE zset1 0 -1 withscores1) "v1"2) "10"3) "v2"4) "20"5) "v3"6) "30"7) "v4"8) "40"127.0.0.1:6379> ZRANGEBYSCORE zset1 (20 401) "v3"2) "v4"

例:limit限制返回的数量

127.0.0.1:6379> ZRANGE zset1 0 -1 withscores1) "v1"2) "10"3) "v2"4) "20"5) "v3"6) "30"7) "v4"8) "40"127.0.0.1:6379> ZRANGEBYSCORE zset1 10 40 limit 1 21) "v2"2) "v3"

2.5 ZSCORE

获取元素分数

127.0.0.1:6379> zscore zset1 v3"30"

2.6 ZCARD

元素个数

127.0.0.1:6379> zcard zset1 (integer) 4

2.6 ZREM

删除某个元素

127.0.0.1:6379> zrem zset1 v3 (integer) 1

2.7 ZINCRBY

为元素member增加分数increment

ZINCRBY key increment member

例:对元素v1加3分

127.0.0.1:6379> zrange zset1 0 -1 withscores1) "v1"2) "10"3) "v2"4) "20"5) "v4"6) "40"127.0.0.1:6379> zincrby zset1 3 v1"13"127.0.0.1:6379> zrange zset1 0 -1 withscores1) "v1"2) "13"3) "v2"4) "20"5) "v4"6) "40"

2.8 ZCOUNT

获得指定分数范围内的元素个数

min: 最小分数
max: 最大分数

ZCOUNT key min max

例:

127.0.0.1:6379> zrange zset1 0 -1 WITHSCORES 1) "v1" 2) "10" 3) "v2" 4) "20" 5) "v3" 6) "30" 7) "v4" 8) "40" 9) "v5"10) "50"11) "v6"12) "60"13) "v7"14) "70"127.0.0.1:6379> zcount zset1  30 50(integer) 3

2.9 ZMPOP

7.0新特性,在指定的numkeys个集合中,弹出分数最大(MAX)或最小(MIN)的count个元素(分数和值成对),可以实现在一个或多个集合中,取出最小或最大的几个元素

ZMPOP numkeys key [key ...] <MIN | MAX> [COUNT count]

例:在1个zset1集合中,弹出最小的1个元素

127.0.0.1:6379> zrange zset1 0 -1 WITHSCORES 1) "v1" 2) "10" 3) "v2" 4) "20" 5) "v3" 6) "30" 7) "v4" 8) "40" 9) "v5"10) "50"11) "v6"12) "60"13) "v7"14) "70"127.0.0.1:6379> ZMPOP 1 zset1 min count 11) "zset1"2) 1) 1) "v1"      2) "10"127.0.0.1:6379> 

2.10 ZRANK

正序下标,集合的某个元素,正序处于集合第几个

127.0.0.1:6379> zrange zset1 0 -11) "v2"2) "v3"3) "v4"4) "v5"5) "v6"6) "v7"127.0.0.1:6379> zrank zset1 v2(integer) 0127.0.0.1:6379> zrank zset1 v3(integer) 1127.0.0.1:6379> zrank zset1 v4(integer) 2127.0.0.1:6379> zrank zset1 v5(integer) 3

2.11 ZREVRANK

倒序下标,集合的某个元素,倒序处于集合第几个

127.0.0.1:6379> zrange zset1 0 -11) "v2"2) "v3"3) "v4"4) "v5"5) "v6"6) "v7"127.0.0.1:6379> zrevrank zset1 v2(integer) 5127.0.0.1:6379> zrevrank zset1 v3(integer) 4127.0.0.1:6379> zrevrank zset1 v4(integer) 3127.0.0.1:6379> zrevrank zset1 v5(integer) 2

3.总结

排序集合大量应用于项目,例如实时展示热销商品统计,打赏点赞数量排行榜统计。将销量和点赞打赏数作为分数绑定在值上面即可。

  •  

2025–ACM&TC第三次招新赛题解

2025--ACM&TC第三次招新赛题解

题目难度:

签到:9、12

简单:2、5、8、11

中等:3、4、7

困难:1、6、10

题解

1、中位数

题目给出的n为奇数,那么最终的中位数一定是中间的那个数,于是我们可以二分答案,不断地去枚举答案

#include <bits/stdc++.h>
using namespace std;
#define int long long

int a[200005];
int n, k;

bool check(int x) {
    int sum = 0;
    int m = (n + 1) / 2; 
    for (int i = m; i <= n; i++) {
        if (a[i] < x) {
            sum += x - a[i];  
            if (sum > k) return 1; 
        }
    }
    return sum > k;  
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);

    cin >> n >> k;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    sort(a + 1, a + 1 + n); 

    int l = a[(n + 1) / 2], r = 1e18;  
    while (l + 1 < r) {
        int mid = (l + r) / 2;
        if (check(mid)) {
            r = mid; 
        } else {
            l = mid; 
        }
    }
    cout << l << '\n';  

}

2、怕挂科的George

这题考的就是结构体的排序,也是看看一些人补没补题,如果第一场招新赛的第 4 题没写出来但补了的话,这一题也是肯定能写出来的。然后就是平均数记得开float或者double,感觉会有很多人开int然后WA好多发(学长赛前预测),学长出题的时候专门手搓了几个卡 int 的数据,嘿嘿~。

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"

struct stus{
    int s,a;
}stu[105];

bool cmp(stus c,stus d){
    return c.s<d.s;
}

int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
    int n,m;
    cin>>n>>m;
    double sum=0;
    for(int i=1;i<=n;++i){
        cin>>stu[i].s>>stu[i].a;
        sum+=stu[i].a;
        if(stu[i].s==m&&stu[i].a<60)
            stu[i].a=60;
    }
    double avg=sum/n;
    for(int i=1;i<=n;++i){
        if(stu[i].s!=m&&stu[i].a>=avg){
            stu[i].a=max(stu[i].a-2,0);
        }
    }
    sort(stu+1,stu+n+1,cmp);
    for(int i=1;i<=n;++i){
        cout<<stu[i].a<<" ";
    }
    cout<<endl;
    return 0;
}

3、位运算终于来了

这道题你首先要清楚 异或运算是什么,当前位相同为0,不同为1,这样根据这个规律我们可以在a的当前位与b的当前位不同时让a去异或2的当前位次幂(1,10,100,1000....),比如a:(1001), b: (110),当枚举到第1位(从后往前)a为0,b为1那需要让a异或2的1次方,可以让a的当前位变为1,这样你会发现当到达当前位枚举过的位都是相同的,如果枚举到2的k次方大于a,那么不可能让a最终等于b,当然还有别的做法

#include <bits/stdc++.h>
using namespace std;
#define int long long

int ksm(int a, int b){
    int ans = 1;
    while(b){
        if(b % 2){
            ans = ans * a;
        }
        b = b / 2;
        a = a * a;
    }
    return ans;
}
void solve(){
    int a, b;
    cin >> a >> b;
    if(a == b){
        cout << 0 << '\n';
        return;
    }
    int aa = a;
    vector<int> ans;
    string s;
    for(int i = 0;; i++){
        if (a == 0 && b == 0) break;
        int x = ksm(2, i);
        if((a & 1) != (b & 1)){
            ans.push_back(x);
            if(x > aa){
            cout << -1 << '\n';
            return;
        }
        }
        a = a >> 1;
        b = b >> 1;
    }
    cout << ans.size() << '\n';
    for(auto t : ans){
        cout << t << ' ';
    }
    cout << '\n';
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int t;
    cin >> t;
    while(t--){
        solve();
    }
}

4、我要打瓦!颗秒!!棒棒棒棒!!!

这题考的就是你们高中学的n的平方的前n项和通项公式(不知道你们老师有没有让背过,大部分老师应该都让背过)。不要觉得这题很奇怪,数学在计算机及 ACM 中很重要,说白了对于 ACM 来说编程语言只是工具,解题思路大部分都是靠数学思维,包括你们学的高数,和以后学的线代、离散、概率论等都会是 ACM 的考点(我记得去年招新赛,我们学长给我们出了一道二项式定理,感兴趣的自己可以看一下),计算机和数学相关性很大的,比如数学中的离散和计算机中的数据结构重合度很高,内容大部分都一样。这题的题解废话有点多了,主要也是想要让你们了解一下数学和计算机的关系,下面是正式题解:

随便举几个例子画画图就能知道移动的路线尽可能往中间靠才能让 ans 最大化,所以使 ans 最大化的路线就是先向右再向下,这样一直循环到 (n,n) 。通过下面两个重要的数学公式,可以推导出答案为:n * (n + 1) * (4 * n - 1) / 6。需要注意此题除法放在最后,避免前面没除尽,出现小数造成误差,模数 2022 最后可以和分母 6 约分成 337 ;还有就是边运算边取模,因为计算过程中会超出 long long 的范围 1e18 ;以及n也需要开long long ,因为n参与下面ans的计算了。具体计算过程见下图,黄线和就是n的平方的前n项和通项公式,橙线和则可以拆成等差数列的前n项和通项公式n的平方的前n项和通项公式的和。

在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"

const int mod=1e9+7;

int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
    int t;
    cin>>t;
    while(t--){
        long long n;
        cin>>n;
        long long ans=337*n%mod*(n+1)%mod*(4*n-1)%mod;
        cout<<ans<<endl;
    }
    return 0;
}

5、带派不老铁?

起始位置由 ( 0 , 0 ) 到 ( x , y ) ,其结果只有三种情况。
1.输出结果为-1:当x=y或x=y+1或y=1时,此时一定不满足题目要求,输出-1;
2.输出结果为2:满足y>x即可
3.输出结果为3:剩下的情况都是3步

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"

int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
    int t;
    cin>>t;
    while(t--){
        int x,y;
        cin>>x>>y;
        if(x==y||x==y+1||y==1)
            cout<<-1<<endl;
        else if(x<y)
            cout<<2<<endl;
        else
            cout<<3<<endl;
    }
    return 0;
}

//
// ⠀⠀⠀             ⠀⢸⣿⣿⣿⠀⣼⣿⣿⣦⡀
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀ ⠀⢸⣿⣿⡟⢰⣿⣿⣿⠟⠁
// ⠀⠀⠀⠀⠀⠀⠀⢰⣿⠿⢿⣦⣀⠀⠘⠛⠛⠃⠸⠿⠟⣫⣴⣶⣾⡆
// ⠀⠀⠀⠀⠀⠀⠀⠸⣿⡀⠀⠉⢿⣦⡀⠀⠀⠀⠀⠀⠀ ⠛⠿⠿⣿⠃
// ⠀⠀⠀⠀⠀⠀⠀⠀⠙⢿⣦⠀⠀⠹⣿⣶⡾⠛⠛⢷⣦⣄⠀
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣧⠀⠀⠈⠉⣀⡀⠀ ⠀⠙⢿⡇
// ⠀⠀⠀⠀⠀⠀⢀⣠⣴⡿⠟⠋⠀⠀⢠⣾⠟⠃⠀⠀⠀⢸⣿⡆
// ⠀⠀⠀⢀⣠⣶⡿⠛⠉⠀⠀⠀⠀⠀⣾⡇⠀⠀⠀⠀⠀⢸⣿⠇
// ⢀⣠⣾⠿⠛⠁⠀⠀⠀⠀⠀⠀⠀⢀⣼⣧⣀⠀⠀⠀⢀⣼⠇
// ⠈⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⡿⠋⠙⠛⠛⠛⠛⠛⠁
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣾⡿⠋⠀
// ⠀⠀⠀⠀⠀⠀⠀⠀⢾⠿⠋ 
//

6、搜索板子题

和题目名一样,就是搜索板子题, BFS 和 DFS 都能写,这里放的是 BFS 的AC码,主要就是让学会搜索的人都能AC,所以也没涉及任何思维,就纯板子。考虑到这是你们培训讲的最难的算法,估计有好多人还没学,所以归到了困难题。

#include <bits/stdc++.h>
using namespace std;
#define endl "\n"

typedef pair<int, int> pii;

int w, h;
char graph[1001][1001];
bool visited[1001][1001];

int cnt = 0;

void bfs(int startx, int starty) {
    queue<pii> q;
    q.push({startx, starty});
    visited[startx][starty] = true;
    cnt++;

    int dx[] = {-1, 0, 1, 0};
    int dy[] = {0, 1, 0, -1};

    while (!q.empty()) {
        int currentx = q.front().first;
        int currenty = q.front().second;
        q.pop();

        for (int i = 0; i < 4; ++i) {
            int newx = currentx + dx[i];
            int newy = currenty + dy[i];

            if (newx >= 0 && newx < h && newy >= 0 && newy < w && graph[newx][newy] == '.' && !visited[newx][newy]) {
                q.push({newx, newy}); 
                visited[newx][newy] = true;
                cnt++;
            }
        }
    }
}

int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);

    int startx, starty;
    cin >> w >> h;

    for (int i = 0; i < h; ++i) {
        for (int j = 0; j < w; ++j) {
            cin >> graph[i][j];
            if (graph[i][j] == '@') {
                startx = i;
                starty = j;
            }
        }
    }

    bfs(startx, starty);
    cout << cnt << endl;

    return 0;
}

7、数好数字

这题是一个简单数论,虽然难度不高,但由于是数论我们还是把它归为了中等题。

大概就是根据好数的周期性,以210为周期,每个210内的好数数量都一样。

先计算一下 2 到 r 的好数数量,再计算一下 2 到 l 的好数数量,然后一减就是 l 到 r 的好数数量。

注意开long long!!!

#include <iostream>
using namespace std;
#define endl "\n"

const int LCM = 210; // 2*3*5*7=210

// 判断一个数是否不被2、3、5、7中的任何一个整除
bool good(int x) {
    return x % 2 > 0 && x % 3 > 0 && x % 5 > 0 && x % 7 > 0;
}

// 计算[0, x)范围内满足good条件的数字数量
int get_naive(int x) {
    int ans = 0;
    for (int i = 1; i <= x; ++i) {
        if (good(i)) {
            ans++;
        }
    }
    return ans;
}

// 优化计算:利用210是2、3、5、7的最小公倍数,减少重复计算
long long get(long long r) {
    return (r / LCM) *48 + get_naive(r % LCM);
}  

int main() {
    int t;
    cin >> t;

    while (t--) {
        long long l, r;
        cin >> l >> r;
        // 计算[l, r]范围内满足条件的数字数量
        cout << get(r) - get(l-1) << endl;
    }

    return 0;
}

8、什么?你...你竟然是尊贵的传奇王者?

简单数学题,由奇+奇=偶、偶+偶=偶、奇+偶=奇,所以只需统计一下奇偶数量,如果奇偶数量相等,则就能恰好分割,则输出 “ Yes ” ,否则输出 “ No ” 。

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"

int a[205]={};

int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
    int t;
    cin>>t;
    while(t--){
        int cntj=0,cnto=0;
        int n;
        cin>>n;
        for(int i=0;i<2*n;++i){
            cin>>a[i];
            if(a[i]%2==1)
                cntj++;
            else
                cnto++;
        }
        if(cntj==cnto)
            cout<<"Yes"<<endl;
        else
            cout<<"No"<<endl;

    }
    return 0;
}

9、这是什么图形

这是一道小学题,正方形的性质可知对角线相等,那么一半都相等

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int t;
    cin >> t;
    while (t--) {
        int l, r, d, u;
        cin >> l >> r >> d >> u;
        if (l == r && d == u && l == d) {
            cout << "Yes\n";
        } else {
            cout << "No\n";
        }
    }
    return 0;
}

10、haxin学姐的旅程规划

让我们把陈述中给出的等式变形重新写成 $c{i+1}−b{c{i+1}}=c{i}−b{c{i}}$ 。这意味着我们旅行计划中路径上的所有城市将具有相同的 $i−b_{i}$ 值,即索引值减去对应的美丽值,这个性质可以通过上面那种对题目中的等式进行变形得到,也可以通过画图得知,就自己画两个线段然后就能琢磨出来。

这题用到C++的一个map容器,没学过的学一下,这个容器挺重要的,大概就是能将两个量关联起来。这一题的思路就是先遍历一遍数组b,并用map的第一个参数放 $i−b{i}$ 值,将 $i−b{i}$ 值相同的元素的值累加起来存到第二个参数中,这样就把 $i−b{i}$ 值和与它相对应的美丽值之和关联在一起了,通过第一个参数的值就能直接访问到与之对应的第二个参数的值。此外,把数组从头遍历到尾,就是满足了题目中序列严格递增的要求。最后再遍历一遍map,map中最大的第二个参数值就是答案,即最大美丽值。另外,数据范围上,虽然n和 $b{i}$ 都没超过 int 范围,但是答案有可能将 n 个 $b_{i}$ 相加,这就超过了 int 范围,所以要开long long,map的第二个参数和 ans 都要开 long long。

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"

int b[200050]={};

int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
    int n;
    cin>>n;
    map<int,long long>mapp;
    for(int i=1;i<=n;++i){
        cin>>b[i];
    }
    int x;
    for(int i=1;i<=n;++i){
        x=i-b[i];
        mapp[x]+=b[i];
    }
    long long ans=0;
    for(auto p:mapp){
        if(p.second>ans)
            ans=p.second;
    }
    cout<<ans<<endl;
    return 0;
}

11、挑剔的猫

思路:在开始之前,我们先求出所有元素的绝对值,因为它们的符号并不重要。然后,如果数组的第一个元素等于或小于数组的[n/2]+1最小元素,则答案是可能的。否则,答案就是不可能的

#include<bits/stdc++.h>
using namespace std;

const int N=1e5+10;
int a[N];

int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        int n;scanf("%d",&n);
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);
        int f=abs(a[1]);
        int cnt=0;
        for(int i=2;i<=n;i++){
            int z=abs(a[i]);
            if(z<=f)
                cnt++;
        }
        if(cnt+1<=(n/2+1))
            cout<<"YES"<<endl;
        else
            cout<<"NO"<<endl;
    }
    return 0;
}

12、这是啥博弈?

诈骗题,提示里也提醒了,输出获胜者名字的第一个字母,而两个人的第一个字母都是 ’ B ‘ ,所以直接输入n,然后直接输出 ’ B ‘ 就行了。但对于这题来说,也可以不用输入 n ,直接输出 ’ B ‘ ,因为AC的标准就是只看你输出的数据和后台的数据是否能完全吻合,不看输入。不要觉得这种诈骗题很奇怪,这都是我们平常碰到过的,很多比赛都有,比如我们学校前年举办的新生河南省邀请赛里就有一道,如果感兴趣的话,可以看看。

#include<bits/stdc++.h>
using namespace std;
#define endl "\n"

int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
    cout<<'B'<<endl;
    return 0;
}

//
// ⠀⠀⠀             ⠀⢸⣿⣿⣿⠀⣼⣿⣿⣦⡀
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀ ⠀⢸⣿⣿⡟⢰⣿⣿⣿⠟⠁
// ⠀⠀⠀⠀⠀⠀⠀⢰⣿⠿⢿⣦⣀⠀⠘⠛⠛⠃⠸⠿⠟⣫⣴⣶⣾⡆
// ⠀⠀⠀⠀⠀⠀⠀⠸⣿⡀⠀⠉⢿⣦⡀⠀⠀⠀⠀⠀⠀ ⠛⠿⠿⣿⠃
// ⠀⠀⠀⠀⠀⠀⠀⠀⠙⢿⣦⠀⠀⠹⣿⣶⡾⠛⠛⢷⣦⣄⠀
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣧⠀⠀⠈⠉⣀⡀⠀ ⠀⠙⢿⡇
// ⠀⠀⠀⠀⠀⠀⢀⣠⣴⡿⠟⠋⠀⠀⢠⣾⠟⠃⠀⠀⠀⢸⣿⡆
// ⠀⠀⠀⢀⣠⣶⡿⠛⠉⠀⠀⠀⠀⠀⣾⡇⠀⠀⠀⠀⠀⢸⣿⠇
// ⢀⣠⣾⠿⠛⠁⠀⠀⠀⠀⠀⠀⠀⢀⣼⣧⣀⠀⠀⠀⢀⣼⠇
// ⠈⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⡿⠋⠙⠛⠛⠛⠛⠛⠁
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣾⡿⠋⠀
// ⠀⠀⠀⠀⠀⠀⠀⠀⢾⠿⠋ 
//
  •