学习源码是一个由浅入深,再到逐渐简化的过程,即所谓“把书读厚,把书读薄”的过程。
”昨夜西风凋碧树。独上高楼,望尽天涯路”,我们先从Redis的特性、用途及数据类型这几个方面介绍下Redis,对其有个总体上的认知。
1. Redis特性及用途
定义(关键词):C语言、开源、key-value、NoSQL、基于内存
特点:
- 高性能,基于内存,读写高效,并发10W QPS
- 单进程 单/多线程,线程安全,IO多路复用 这里的单线程是指Redis用来处理请求这一过程中使用了一个线程。 严格来说,从Redis4.0之后并不是单线程,除了主线程外,也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等;Redis 6.0引入的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。
- 丰富的数据类型(string/list/hash/set/zset/stream)
- 数据持久化(RDB、AOF)
- 高可用(主从复制、哨兵机制、集群)
用途:
- 数据库
- 缓存
- 分布式锁
- 消息中间件
2. Redis的数据类型
(1) 六种基础数据类型
数据类型 | 底层数据结构 | 常用命令 | 应用场景 | 备注 |
---|---|---|---|---|
String | int, embstr, raw | set, mset, get, append, setbit ... | 【字符串】万物皆可String | 字符串,二进制安全 |
Hash | ht, ziplist | hget, hset, hegetall, hkeys, hexists ... | 【存储对象】操作用户属性 | 散列,键值对集合,map |
List | linkedlist, ziplist | lpush/lpop, rpush/rpop, lrange ... | 【增删快】最新消息排行;【按序】消息队列 | 列表,双向链表 |
Set | intset, ht | sadd, spop, sinter, sunion, sdiff ... | 【支持交/并/差集】共同好友;【不重复】统计访问网站IP | 集合,不重复 |
ZSet | ziplist, skipklist | zadd, zrange, zrem, zcard, zrank ... | 【有序】排行榜;【score】带权重的消息队列、延时队列 | 有序集合,按score排序 |
Stream | listpack, rax | xadd, xrange, xrevrange, xgroup, xread ... | 【消息队列】IRC / 实时聊天系统, IoT数据采集 | 类似于日志的数据结构,本质是一个消息队列 |
(2) 基于基础数据类型的三种扩展数据类型
数据类型 | 依赖的数据结构 | 常用命令 | 应用场景 | 备注 |
---|---|---|---|---|
Bitmap | String | setbit, getbit, bitop, bitcount, bitpos | 活跃用户数统计; 统计某一天的用户登陆数量 | 存储与对象ID关联的节省空间并且高性能的布尔信息 |
Hyperloglog | String | pfadd, pfcount, pfmerge | 基数统计; 统计每日访问IP数/页面UV 数/在线用户数等 | bitmap的升级版;概率算法,不直接存储数据集合本身,概率统计方法预估基数值 |
Geo | ZSet | geoadd, geohash, geopos, geodist, georadius | 基于位置的服务(LBS) | Redis3.2以后版本 |
注意:
- 一般可以bitmap和hyperloglog配合使用,bitmap标识哪些用户活跃,hyperloglog计数
- Geo数据建议使用单独的 Redis 实例部署(在项目开发中,有看到经纬度专用的Redis,与业务使用的Redis分离)
A. Bitmap
bitmap不是实际的数据类型是string类型上的一组面向bit操作的集合。
由于string是二进制安全的,并且它们的最大长度是512m,所以bitmap能最大设置2^32个不同的bit。
8 *1024*1024* 512 = 2^32 = 4294967296 = 40 亿
优点:
存储信息时可以节省大量的空间。例如在一个系统中,不同的用户被一个增长的用户ID表示。40亿(2^32=4*1024*1024*1024≈40亿
)用户只需要512M内存就能记住某种信息,例如用户是否登录过。
操作:
- 恒定时间的单个bit操作,例如把某个bit设置为0或者1。或者获取某bit的值。
- SETBIT:设值
- GETBIT:取值
- 对一组bit的操作。例如给定范围内bit统计(例如人口统计)。
- BITOP:执行两个不同string的位操作,包括AND,OR,XOR和NOT
- BITCOUNT:统计位的值为1的数量
- BITPOS:寻址第一个为0或者1的bit的位置
使用场景:
- 各种实时分析
- 存储与对象ID关联的节省空间并且高性能的布尔信息 例如: 统计访问网站的用户的最长连续时间 统计某一天的用户登陆数量(以当天的日志加固定的前缀作为key,建立一个bitmap,每一位二进制的位做为一个用户ID的标识)
B. Hyperloglog
hyperLogLog是bitmap的升级版。本质上是一种概率算法,不直接存储数据集合本身,而是通过一定的概率统计方法预估基数值。这种方法可以大大节省内存,同时保证误差控制在一定范围内。
被编码成Redis字符串。因此可以通过调用GET命令序列化一个Hyperloglog(HLL),也可以通过调用SET命令将其反序列化到redis服务器。HLL的API类似使用SETS数据结构做相同的任务,SETS结构中,通过SADD命令把每一个观察的元素添加到一个SET集合,用SCARD命令检查SET集合中元素的数量,集合里的元素都是唯一的,已经存在的元素不会被重复添加。
而使用HLL时并不是真正添加项到HLL中(这一点和SETS结构差异很大),因为HLL的数据结构只包含一个不包含实际元素的状态。
操作:
- PFADD:用于添加一个新元素到统计中。
- PFCOUNT:用于获取到目前为止通过PFADD命令添加的唯一元素个数近似值。
- PFMERGE:执行多个HLL之间的联合操作。
使用场景:
- 统计基数数量(大量)
- 统计注册 IP 数
- 统计每日访问 IP 数
- 统计页面实时 UV 数
- 统计在线用户数
- 统计用户每天搜索不同词条的个数
- 一般可以bitmap和hyperloglog配合使用,bitmap标识哪些用户活跃,hyperloglog计数
C. Geo
底层数据类型:zset
Redis的GEO特性在 Redis3.2版本中推出,这个功能可以将用户给定的地理位置(经度和纬度)信息储存起来,并对这些信息进行操作。
GEO相关命令只有6个:
- GEOADD:GEOADD key longitude latitude member [longitude latitude member …],将指定的地理空间位置(纬度、经度、名称)添加到指定的key中
- GEOHASH:GEOHASH key member [member …],返回一个或多个位置元素的标准Geohash值,它可以在http://geohash.org/使用
- GEOPOS:GEOPOS key member [member …],从key里返回所有给定位置元素的位置(经度和纬度)
- GEODIST:GEODIST key member1 member2 [unit],返回两个给定位置之间的距离。GEODIST命令在计算距离时会假设地球为完美的球形。在极限情况下,这一假设最大会造成0.5%的误差。
- GEORADIUS:GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD][WITHDIST] [WITHHASH][COUNT count],以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。这个命令可以查询某城市的周边城市群。
- GEORADIUSBYMEMBER:GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD][WITHDIST] [WITHHASH][COUNT count],这个命令和GEORADIUS命令一样,都可以找出位于指定范围内的元素,但是GEORADIUSBYMEMBER的中心点是由给定的位置元素决定的,而不是像 GEORADIUS那样,使用输入的经度和纬度来决定中心点。
使用场景:
- 基于位置的服务(LBS)
GEO类型的底层数据结构是用Zset实现的。
例如,存储车辆/店铺的经纬度信息时,元素是车辆/店铺ID,元素的权重Sore是本应是经纬度信息,但Sore应该是float类型,因此,需要对一组经纬度进行编码(即GeoHash编码)。
简要步骤:
Step 1:将经度/纬度进行二分拆解,得到二叉树结构,并进行0/1编码,再通过N位bit进行存储(N越大,精度越高);
Step 2:将经度和纬度的N位bit进行交叉组合,得到GeoHash值。
GeoHash编码的基本原理是“二分区间,区间编码”,先对经度和纬度分别编码,再将经纬度各自的编码组合成一个最终编码。简单来说,GeoHash将一个空间分割成一个个小方块,我们可以通过查询给定经纬度所在方格周围的4个或者8个方格,以此进行“周边查找”。【GeoHash值相近,并不一定位置相近,故需计算邻居节点,以提高LBS精度】
注意:
在项目开发中,会看到将一个Redis单独划分出来,用于经纬度的计算。如果数据量过亿,就需要对 Geo 数据进行拆分,按国家/省/市拆分,甚至按区拆分,以降低单个 zset 集合的大小。
【原因:在一个地图应用中,数据可能会有百万千万条,如果使用Geo,位置信息将全部放在一个 zset 集合中。在 Redis 的集群环境中,集合 可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会导致集群的迁移出现卡顿等问题,影响线上服务的正常运行。所以,建议 Geo 的数据使用单独的 Redis 实例部署。】
实例:
代码语言:javascript复制# 新增
127.0.0.1:6379> geoadd city 114.06667 22.61667 "shenzhen" 119.30000 26.08333 "fuzhou"
(integer) 2
# zset类型
127.0.0.1:6379> TYPE city
zset
# 标准Geohash值,可以在http://geohash.org/使用。
127.0.0.1:6379> geohash city shenzhen fuzhou
1) "ws10ethzdh0"
2) "wssu6srd7k0"
# 获取key的经纬度
127.0.0.1:6379> geopos city shenzhen fuzhou
1) 1) "114.06667023897171021"
2) "22.61666928352524764"
2) 1) "119.29999798536300659"
2) "26.08332883679719316"
# 计算距离
127.0.0.1:6379> geodist city shenzhen fuzhou km
"655.5342"
# 计算范围
127.0.0.1:6379> georadius city 116 15 1000 km
1) "shenzhen"
127.0.0.1:6379> georadius city 116 15 2000 km
1) "shenzhen"
2) "fuzhou"
127.0.0.1:6379> georadius city 116 15 2000 km asc
1) "shenzhen"
2) "fuzhou"