[03] Redis 数据类型
0. Redis 基本指令#
keys *
查询当前库的所有键exists <key>
判断某个键是否存在type <key>
查看键的类型dbsize
查看当前数据库的 key 的数量del <key>
删除某个键flushdb
清空当前库flushall
通杀所有库
1. string#
1.1 简述#
- string 是 Redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value
- string 类型是二进制安全的。意味着 Redis 的 string 可以包含任何数据。比如 jpg 或者序列化的对象
- string 类型是 Redis 最基本的数据类型,一个 Redis 中字符串 value 最多可以是 512M
1.2 常用操作#
- 设置
set <key> <value>
添加键值对setex <key> <过期时间> <value>
设置键值的同时,设置过期时间,单位秒setnx <key> <value>
只有在 key 不存在时设置 key 的值mset <key1> <value1> [<key2> <value2> ...]
同时设置 1 个或多个 key-value 对msetnx <key1> <value1> [<key2> <value2> ...]
同时设置 1 个或多个 key-value 对,当且仅当所有给定 key 都不存在
- 获取
get <key>
查询对应键值mget <key1> [<key2> ...]
同时获取 1 个或多个 valuegetset <key> <value>
以新换旧,设置了新值的同时获取旧值strlen <key>
获取值的长度getrange <key> <起始位置> <结束位置>
获取指定范围的值,类 Java 的 substring,但这里包前也包后ttl <key>
查看还有多少秒过期,-1 代表永不过期,-2 表示已过期
- 修改
expire <key> <seconds>
为键值设置过期时间,单位秒append <key> <value>
将给定的 value 追加到原值的末尾(如果当前 key 不存在,该命令等价于 set)incr <key>
将 key 中储存的数字值增 1,只能对数字值操作,如果为空,新增值为 1decr <key>
将 key 中储存的数字值减 1,只能对数字值操作,如果为空,新增值为 -1incrby / decrby <key> <步长>
将 key 中储存的数字值增减,自定义步长setrange <key> <起始位置> <value>
用 value 覆写 key 所存储的字符串值,从起始位置开始
1.3 incr 操作的原子性#
- 所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)
- 在单线程中,能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间。
- 在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。
- Redis 单命令的原子性主要得益于 Redis 的单线程
- Java 中的
i++
是否是原子操作?不是
2. list#
2.1 简述#
- 单键多值
- Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
- 底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差
2.2 常用操作#
lpush/rpush <key> <value1> <value2> ...
从左/右边插入 1 个或多个值lpop/rpop <key>
从左/右边吐出一个值(值在键在,值光键亡)llen <key>
获得列表长度lindex <key> <index>
按照索引下标获得元素(从左到右)lrange <key> <start> <stop>
按照给定索引范围获得所有元素(从左到右)lset <key> <index> <value>
更新指定索引位置的 valuelinsert <key> before|after <value> <newValue>
在 value 的前|后面插入 newValueltrim <key> [start] [stop]
截取[start, stop]
范围的列表,重新赋值给 keyrpoplpush <key1> <key2>
从 key1 列表右边吐出一个值插到 key2 列表左边lrem <key> <n> <value>
删除 n 个 valuen > 0
从左往右删 n 个n < 0
从右往左删 n 个n = 0
删除全部
3. set#
3.1 简述#
- Redis set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。
- Redis set 是 string 类型的无序集合。它的底层其实是一个 value 为 null 的 hash 表,所以添加 / 删除 / 查找的复杂度都是 O(1)。
3.2 常用操作#
sadd <key> <value1> [<value2> ...]
将 1 个或多个 value 加入到集合 key 当中,已经存在于集合的 value 元素将被忽略srem <key> <value1> [<value2> ...]
删除集合 key 中指定的 valuespop <key>
随机从该集合中移除一个值srandmember <key> <n>
随机从该集合中获取 n 个值(不会从集合中删除)scard <key>
返回集合 key 的元素个数smembers <key>
取出该集合的所有值sismember <key> <value>
判断集合 key 中是否含有指定 value 值;有则返回 1,没有返回 0smove <key1> <key2> <value>
将 key1 中的 value 移动到 key2sinter <key1> <key2>
返回两个集合的交集元素sunion <key1> <key2>
返回两个集合的并集元素sdiff <key1> <key2>
返回两个集合的差集元素
3.3 应用场景#
抽奖 | 相关命令 |
---|---|
立即参与按钮 | SADD <key> 用户ID |
显示已经有多少人参与 | SCARD <key> |
抽奖(任意选取 N 个中奖人) | SRANDMEMBER <key> N |
点赞 | 相关命令 |
---|---|
新增点赞 | SADD <pub:msgID> 用户ID1 用户ID2 |
取消点赞 | SREM <pub:msgID> 用户ID |
展现所有点赞过的用户 | SMEMBERS <pub:msgID> |
点赞用户数统计 | SCARD <pub:msgID> |
判断某个朋友是否对楼主点赞过 | SISMEMBER <pub:msgID> 用户ID |
社交关系 | 相关命令 |
---|---|
共同关注的人 | SINTER <用户ID1> <用户ID2> |
可能认识的人 | SDIFF <用户ID1> <用户ID2> |
4. hash#
4.1 简述#
- Redis hash 是一个键值对集合。K-V 模式不变,但 value 是一个键值对集合,类比
Map<String, String>
。 - string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
4.2 存储 JavaBean#
1. 用户 ID 为 key,value 为 JavaBean 序列化后的字符串。
缺点:每次修改用户的某个属性需要,先反序列化改好后再序列化回去。开销较大
2. {用户ID + 属性名} 作为 key,属性值作为 value。
缺点:用户 ID 数据冗余
3. 通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。
电商项目中,可以用来存储用户购物车~
4.3 常用操作#
hset <key> <field> <value>
给集合 key 中的键 field 赋值 valuehsetnx <key> <field> <value>
将集合 key 的 field 值设为 value,当且仅当 field 不存在hmset <key> <field1> <value1> [<field2> <value2> ...]
批量设置集合 key 的键值对hdel <key> <field>
从集合 key 中删除指定 field(& value)hget <key> <field>
从集合 key 中取出键 field 对应的 valuehlen <key>
获取集合 key 的 field 数目hgetall <key>
查看集合 key 中所有元素(field、value)hkeys <key>
列出集合 key 的所有 fieldhvals <key>
列出集合 key 的所有 valuehexists <key> <field>
查看集合 key 中,给定 field 是否存在hincrby <key> <field> <increment>
为集合 key 中给定 field 的 value 加上增量hdecrby <key> <field> <decrement>
为集合 key 中给定 field 的 value 减去增量
5. zset(sorted set)#
5.1 简述#
- Redis 有序集合 zset 与普通集合 set 非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score) ,这个评分(score) 被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。// 以 value 为键,score 为值的 map
- 因为元素是有序的,所以你也可以很快的根据评分(score) 或者次序(position) 来获取一个范围的元素。访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
5.2 常用操作#
zadd <key> <score1> <value1> [<score2> <value2> ...]
将 1 个或多个 value 及其 score 值加入到有序集 key 中zrem <key> <value>
删除该集合下,指定值的元素zincrby <key> <increment> <value>
为 value 的 score 加上指定增量zrank <key> <value>
返回 value 在集合中的排名,从 0 开始zcard <key>
获取该集合所有的 fieldzcount <key> <min> <max>
统计该集合中指定分数区间内的元素个数zrange/zrevrange <key> <start> <stop> [withscores]
- 返回有序集 key 中,下标在
<start>
和<stop>
之间的元素(升序/降序) - 带
withscores
的,可以让 score 和 value 一起返回到结果集
- 返回有序集 key 中,下标在
zrangebyscore key [(]min [(]max [withscores] [limit offset count]
- 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max)的 value
- 有序集 value 按 score 递增次序排列
(
表示开区间;min/max 除取具体数值外,还可以取值-inf | +inf
zrevrangebyscore key [(]max [(]min [withscores] [limit offset count]
同上,改为降序排列
5.3 应用场景#
如何利用 zset 实现一个文章访问量的排行榜?
根据商品销售对商品进行排序显示。思路:定义商品销售排行榜,key 为 goods:sellsort
,分数为商品销售数量。
操作 | 命令 |
---|---|
商品编号 1101 的销量是 9,商品编号 1002 的销量是 15 | zadd goods:sellsort 9 1101 15 1002 |
客户又买了 2 件编号为 1101 的商品 | zincrby goods:sellsort 2 1101 |
商品销量前 10 名 | ZRANGE goods:sellsort 0 10 withscores |
6. Bitmap#
6.1 常用操作#
位存储!由 0 和 1 状态表现的二进制位的 bit 数组。
Bitmap 的偏移量是从 0 开始算的。
SETbit <key> <offset> <value> # 指定第offset位赋值为value
GETbit <key> <offset> # 查询第offset位置的value
bitCOUNT <key> [start end] # 统计start到end字节中二进制值为1的个数
- bitCOUNT test 0 0 只查找第 1 个字节
- bitCOUNT test 1 1 只查找第 2 个字节
- bitCOUNT test 0 1 查找第1~2个字节
bitOP <op> <destKey> <key1> [<key2> ...] # 对不同的二进制数据位运算(AND、OR、NOT、XOR)
Redis 中 bit 映射被限制在 512MB 之内,所以最大是 2^32 位。建议每个 key 的位数都控制下,因为读取时候时间复杂度 O(n),越大的串读的时间花销越多。
6.2 简单说明#
实质是二进制的 ascii 编码对应,使用 type
命令可以看到实际数据类型是 string。
strlen
返回的不是字符串长度而是占据几个字节,超过 8 位后按照 8 位一组一字节再扩容。
6.3 应用场景#
主要用于状态统计。
- 日活统计
- 连续 N 天签到打卡
- 最近一周的活跃用户
- 统计指定用户一年之中的登陆天数
- 某用户按照一年 365 天,哪几天登陆过?哪几天没有登陆?全年中登录的天数共计多少?
按「年」去存储一个用户的签到情况,365 天只需要 365 / 8 ≈ 46 Byte,1000W 用户量一年也只需要 44 MB 就足够了。假如是亿级的系统,每天使用 1 个 1 亿位的 Bitmap 约占 12MB 的内存(10^8/8/1024/1024),10 天的 Bitmap 的内存开销约为 120MB,内存压力不算太高。在实际使用时,最好对 Bitmap 设置过期时间,让 Redis 自动删除不再需要的签到记录以节省内存开销。
7. HyperLogLog#
7.1 前置说明#
(1)数据量较大、亿级的去重复统计不能用 bitmap
bitmap 是通过用 bit 数组来表示各元素是否出现,每个元素对应一位,所需的总内存为 N 个 bit。基数计数则将每一个元素对应到 bit 数组中的其中一位(对应关系存到表里),比如 bit 数组 010010101(按照从 0 开始下标,有的就是 1、4、6、8)。
新进入的元素只需要将已经有的 bit 数组和新加入的元素进行按位或计算就行。这个方式能大大减少内存占用且位操作迅速。但是,假设一个样本案例就是一亿个基数位值数据,一个样本就是一亿。
如果要统计 1 亿个数据的基数位值,大约需要内存 100000000/8/1024/1024 约等于 12M,内存减少占用的效果显著。这样得到统计一个对象样本的基数值需要 12M。
如果统计 10000 个对象样本(1w 个亿级),就需要 117.1875G 将近 120G,可见使用 bitmap 还是不适用大数据量下(亿级)的基数计数场景,但是 bitmap 方法是精确计算的。
(2)概率算法
通过牺牲准确率来换取空间,对于不要求绝对准确率的场景下可以使用,因为概率算法不直接存储数据本身,通过一定的概率统计方法预估基数值,同时保证误差在一定范围内,由于又不储存数据故此可以大大节约内存。
HyperLogLog 就是一种概率算法的实现。
(3)HyperLogLog
HyperLogLog 就是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
【基数】比如数据集 {1, 3, 5, 7, 5, 7, 8},那么这个数据集的基数集为 {1, 3, 5 ,7, 8},基数(不重复的元素个数)为 5。 基数估计就是在误差可接受的范围内,快速计算基数。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
基于上述所说,已经知道它需要 12KB 的内存来做基数统计,原因就是 HyperLogLog 中“槽=16834 & 桶=6”,因此使用空间为 16834*6/8/1024 = 12KB
。
每个桶为何取 6 位?因为 6 位可以存储的最大值为 64,现在计算机都是 64/32 位操作系统,因此 6 位最节省内存,又能满足需求。
要注意的是它有 0.81% 的错误率。但如果误差可接受的范围内,推荐使用 HyperLogLog,它能快速计算基数!
【补充】https://blog.csdn.net/u010887744/article/details/108041280
(4)为什么 Redis 集群的最大槽数是 16384个?
Redis 集群并没有使用〈一致性 hash〉而是引入了「哈希槽」的概念。Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。但为什么哈希槽的数量是 16384(2^14)个呢?
CRC16 算法产生的 hash 值有 16 bit,该算法可以产生 2^16=65536 个值。换句话说值是分布在 0~65535 之间。那作者在做取模运算的时候,为什么不 mod65536,而选择 mod16384?
- 正常的心跳数据包带有该节点的完整配置,可以用幂等方式用旧的节点替换旧节点,以便更新旧的配置。这意味着它们包含原始节点的插槽配置,所以,如果采用 16384 个插槽,占发送心跳信息的消息头占用空间为 2KB (16384/8),而当 65536 个插槽时,发送心跳信息的消息头占空间 8KB (65536/8)。8KB 的心跳包看似不大,但这是心跳包,每秒都要将本节点的信息同步给集群其他节点的。比起 16384 个插槽,头大小增加了 4 倍,ping 消息的消息头太大了,浪费带宽;
- Redis Cluster 不太可能扩展到超过 1000 个主节点,太多可能导致网络拥堵;
- Redis 主节点的哈希槽配置信息是通过 bitmap 来保存的,传输过程中,会对 bitmap 进行压缩,bitmap 的填充率越低,压缩率越高(bitmap 填充率 = slots / 节点数)。所以,插槽数偏低的话, 填充率会降低,压缩率会升高。
综合下来,从心跳包的大小、网络带宽、心跳并发、压缩率等维度考虑,16384 个插槽更有优势且能满足业务需求。
【再来看下 master 节点间心跳数据包格式】消息由消息头和消息体组成。消息头包含发送节点自身状态数据,接收节点根据消息头就可以获取到发送节点的相关数据。结构体定义如下:
其中,消息头有一个名为 myslots 的 char 类型数组 unsigned char myslots[CLUSTER_SLOTS/8]
,该数组长度为 16384/8 = 2048。底层存储其实是一个 bitmap,存储发送方提供服务的 slot 映射表(如果为从,则为该从所对应的主提供服务的 slot 映射表),每一个位代表一个槽,如果该位为 1 则表示这个槽是属于这个节点。
7.2 使用场景#
PV、UV、DAU、MAU
- PV(Page View,点击量):即页面浏览量或点击量,用户每次刷新即被计算一次;
- UV(Unique Visitor,独立访客即用户数量):访问您网站的一台电脑客户端为一个访客,在一段时间内;
- DAU(Daily Active User,日活跃用户数量):常用于反映网站、互联网应用或网络游戏的运营情况。DAU 通常统计一日(统计日)之内,登录或使用了某个产品的用户数(去除重复登录的用户),这与流量统计工具里的访客(UV)概念相似。
- MAU(Monthly Active Users,月活跃用户人数):是在线游戏的一个用户数量统计名词,数量越大意味着玩这款游戏的人越多。
博客文章访问量(同一个人访问一篇文章多次,但还是只能算 1 次)。用术语说这实际是一个实时数据流统计分析问题。要实现这个统计需求。需要做到如下 3 点:
- 对独立访客做标识
- 在访客点击链接时记录下链接编号及访客标记
- 对每一个要统计的链接维护一个数据结构和一个当前 UV 值,当某个链接发生一次点击时,能迅速定位此用户在今天是否已经点过此链接,如果没有则此链接的 UV 增加 1。
[3] → 如果将每个链接被点击的日志中访客标识字段组成一个集合,那么此链接当前的 UV 也就是这个集合的基数,因此 UV 计算本质上就是一个基数计数问题。
7.3 常用操作#
PFadd
命令将所有元素参数添加到 HyperLogLog 数据结构中。返回值为整型,如果至少有个元素被添加返回 1, 否则返回 0。
PFADD <key> <value1> [<value2> ...]
PFcount
命令返回给定 HyperLogLog 的基数(重复的数不算,求总数)估算值。如果多个 HyperLogLog 则返回基数估值之和。
PFCOUNT <key1> [<key2> ...]
PFmerge
命令将多个 HyperLogLog 合并为一个 HyperLogLog ,合并后的 HyperLogLog 的基数估算值是通过对所有给定 HyperLogLog 进行“并集”计算得出的。
PFMERGE <destkey> <sourcekey1> [<sourcekey2> ...]
8. Geospatial#
Geo 是 Redis 用来处理位置信息的。在 Redis3.2 中正式使用。主要是利用了 Z 阶曲线、Base32 编码和 Geohash 算法。
(1)Z 阶曲线
在 x 轴和 y 轴上将十进制数转化为二进制数,采用 x 轴和 y 轴对应的二进制数依次交叉后得到一个六位数编码。把数字从小到大依次连起来的曲线称为 Z 阶曲线,Z 阶曲线是把多维转换成一维的一种方法。
(2)Base32 编码
Base32 这种数据编码机制,主要用来把二进制数据编码成可见的字符串,其编码规则是:任意给定一个二进制数据,以 5 个 bit 为一组进行切分(base64 以 6 个 bit 为一组),对切分而成的每个组进行编码得到 1 个可见字符。Base32 编码表字符集中的字符总数为 32 个(0-9、b-z 去掉 a、i、l、o),这也是 Base32 名字的由来。
(3)Geohash 算法
Gustavo 在 2008 年 2 月上线了 geohash.org 网站。Geohash 是一种地理位置信息编码方法。 经过 Geohash 映射后,地球上任意位置的经纬度坐标可以表示成一个较短的字符串。可以方便的存储在数据库中,附在邮件上,以及方便的使用在其他服务中。以北京的坐标举例,[39.928167, 116.389550] 可以转换成 wx4g0s8q3jf9 。
8.1 应用场景#
如「附近的人」、「打车距离计算」...
如果用数据库做,当我们查找距离我们(x0,y0) 附近 r 公里范围内部的车辆,使用如下 SQL 即可:
select taxi from position where x0-r < x < x0 + r and y0-r < y < y0+r
这样会有什么问题呢?
- 查询性能问题,如果并发高,数据量大这种查询是要搞垮数据库的;
- 这个查询的是一个矩形访问,而不是以我为中心 r 公里为半径的圆形访问;
- 精准度的问题,我们知道地球不是平面坐标系,而是一个圆球,这种矩形计算在长距离计算时会有很大误差。
8.2 常用操作#
Redis 的 Geo 在 Redis3.2 版本就推出了,这个功能可以推算地理位置的信息,两地之间的距离,方圆几里地人等。
(1)将指定的地理空间位置(纬度、经度、名称)添加到指定的 key 中。这些数据将会存储到 sorted set,这样的目的是为了方便使用 GEORADIUS
或者 GEORADIUSBYMEMBER
命令对数据进行半径查询等操作。
GEOadd <key> <纬度> <经度> <名称>
sorted set 使用一种称为 Geohash 的技术进行填充。经度和纬度的位是交错的,以形成一个独特的 52 位整数。已知一个 sorted set 的 double score 可以代表一个 52 位的整数,而不会失去精度。
有效的经度从 -180 度到 180 度,有效的纬度从 -85.05112878 度到 85.05112878 度。当坐标位置超出上述指定范围时,该命令将会返回一个错误。
(2)从 key 里返回所有给定位置元素的位置(经度和纬度)。具体是返回一个数组,数组中的每个项都由两个元素组成:第 1 个元素为给定位置元素的经度,而第 2 个元素则为给定位置元素的纬度。当给定的位置元素不存在时,对应的数组项为空值。
GEOpos <key> <名称>
(3)计算出的距离会以双精度浮点数的形式被返回。如果给定的位置元素不存在,那么命令返回空值。
GEOdist <key> <名称1> <名称2> [单位]
指定单位的参数 unit 必须是以下单位的其中一个:
- m 表示单位为米(默认)
- km 表示单位为千米
- mi 表示单位为英里
- ft 表示单位为英尺
(4)以给定的经纬度为中心,返回键包含的位置元素当中,与中心的距离不超过给定最大距离的所有位置元素。
GEOradius <key> <纬度> <经度> <半径> [单位] <携带值|升降序|返回数目>
在给定以下可选项时,命令会返回额外的信息:
- 【withDIST】在返回位置元素的同时,将〈位置元素与中心之间的距离〉也一并返回(距离的单位和用户给定的范围单位保持一致);
- 【withCOORD】将〈位置元素的经度和维度〉也一并返回;
- 【withHASH】以 52 位有符号整数的形式,返回〈位置元素经过原始 Geohash 编码的有序集合分值〉。这个选项主要用于底层应用或者调试,实际中的作用并不大。
命令默认返回未排序的位置元素。通过以下两个参数,用户可以指定被返回位置元素的排序方式:
- 【ASC】根据中心的位置,按照从近到远的方式返回位置元素;
- 【DESC】根据中心的位置,按照从远到近的方式返回位置元素。
在默认情况下,GEORADIUS
命令会返回所有匹配的位置元素。虽然用户可以使用 <count>
选项去获取前 N 个匹配元素,但是因为命令在内部可能会需要对所有被匹配的元素进行处理,所以在对一个非常大的区域进行搜索时,即使只使用 COUNT 选项去获取少量元素,命令的执行速度也可能会非常慢。但是从另一方面来说,使用 COUNT 选项去减少需要返回的元素数量,对于减少带宽来说仍然是非常有用的。
在没有给定任何 WITH 选项的情况下,命令只会返回一个像 ["New York", "Milan", "Paris"] 这样的线性列表。但在指定了 WITHCOORD 、 WITHDIST 、 WITHHASH 等选项的情况下,命令返回一个二层嵌套数组,内层的每个子数组就表示一个元素。
(5)该命令将返回 11 个字符的 Geohash 字符串。
GEOhash <key> <名称1> [<名称2> ...]
一个数组,数组的每个项都是一个 Geohash。命令返回的 Geohash 的位置与用户给定的位置元素的位置一一对应。
(6)这个命令和 GEORADIUS
命令一样,都可以找出位于指定范围内的元素,但是 GEORADIUSBYMEMBER
的中心点是由给定的位置元素决定的,而不是像 GEORADIUS
那样,使用输入的经度和纬度来决定中心点。
GEOrediusbymember <key> <名称> <半径> [单位]
8.3 Java 代码#
@RestController
@RequestMapping("/geo")
public class GeoController {
public static final String CITY = "city";
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/add")
public String geoAdd() {
Map<String, Point> map = new HashMap<>(16);
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);
return map.toString();
}
@ApiOperation("获取经纬度坐标")
@GetMapping(value = "/position")
public Point position(String member) {
List<Point> list = this.redisTemplate.opsForGeo().position(CITY, member);
return list.get(0);
}
@ApiOperation("geoHash算法生成的base32编码值")
@GetMapping(value = "/hash")
public String hash(String member) {
List<String> list = this.redisTemplate.opsForGeo().hash(CITY, member);
return list.get(0);
}
@GetMapping(value = "/distance")
public Distance distance(String member1, String member2) {
Distance distance = this.redisTemplate.opsForGeo().distance(
CITY, member1, member2, RedisGeoCommands.DistanceUnit.KILOMETERS);
return distance;
}
@ApiOperation("通过经纬度查找附近50公里的建筑")
@GetMapping(value = "/radius50ByPos")
public GeoResults radiusByPos(Double centerX, Double centerY) {
Circle circle = new Circle(centerX, centerY, Metrics.KILOMETERS.getMultiplier());
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
.newGeoRadiusArgs().includeDistance()
.includeCoordinates().sortAscending()
.limit(50);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = this.redisTemplate
.opsForGeo().radius(CITY, circle, args);
return geoResults;
}
@ApiOperation("通过地标名称查找半径10公里内建筑(50条)")
@GetMapping(value = "/radius10ByMember")
public GeoResults radiusByMember(String member) {
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
.newGeoRadiusArgs().includeDistance()
.includeCoordinates().sortAscending()
.limit(50);
Distance distance = new Distance(10, Metrics.KILOMETERS);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = this.redisTemplate.opsForGeo().radius(CITY, member, distance, args);
return geoResults;
}
}
9. 四种统计#
【实际场景】
- 手机 App 中的每天的用户登录信息:1 天对应一系列用户 ID 或移动设备 ID;
- 电商网站上商品的用户评论列表:1 个商品对应了一系列的评论;
- 用户在手机 App 上的签到打卡信息:1 天对应一系列用户的签到记录;
- 应用网站上的网页访问信息:1 个网页对应一系列的访问点击。
- 在移动应用中,需要统计每天的新增用户数和第 2 天的留存用户数;
- 在电商网站的商品评论中,需要统计评论列表中的最新评论;
- 在签到打卡中,需要统计一个月内连续打卡的用户数;
- 在网页访问记录中,需要统计独立访客(UniqueVisitor,UV)量;
- 类似今日头条、抖音、淘宝这样的额用户访问级别都是亿级的,请问如何处理?
【痛点】
亿级数据的收集 + 统计
一句话就是得做到:存的进 + 取得快 + 多维统计
9.1 聚合统计#
统计多个集合元素的聚合结果,就是交差并等集合统计。
9.2 排序统计#
抖音视频最新评论留言的场景,请你设计一个展现列表。
在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,建议使用 zSet(右侧)。
如果用 List(左侧),则每个商品评价对应一个 List 集合,这个 List 包含了对这个商品的所有评论,而且会按照评论时间保存这些评论,每来一个新评论就用 LPUSH 命令把它插入 List 的队头。但是,如果在演示第 2 页前,又产生了一个新评论,第 2 页的评论不一样了。
原因:List 是通过元素在 List 中的位置来排序的,当有一个新元素插入时,原先的元素在 List 中的位置都后移了一位,原来在第 1 位的元素现在排在了第 2 位,当 LRANGE 读取时,就会读到旧元素。
9.3 二值统计#
集合元素的取值就只有 0 和 1 两种。如签到打卡的场景中,我们只用记录有签到(1)或没签到(0)。
9.4 基数统计#
指统计⼀个集合中不重复的元素个数。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?