NoSql 可以翻译做Not Only Sql(不仅仅是SQL),或者是No Sql(非Sql的)数据库。是相对于传统关系型数据库而言,有很大差异的一种特殊的数据库,因此也称之为非关系型数据库。
传统关系型数据库是结构化数据,每一张表都有严格的约束信息:字段名、字段数据类型、字段约束等等信息,插入的数据必须遵守这些约束:
而NoSql则对数据库格式没有严格约束,往往形式松散,自由。
可以是键值型:
可以是文档型:
也可以是列族型:
甚至可以是图格式:
传统数据库的表与表之间往往存在关联,例如外键:
而非关系型数据库不存在关联关系,要维护关系要么靠代码中的业务逻辑,要么靠数据之间的耦合:
{
id: 1,
name: "张三",
orders: [
{
id: 1,
item: {
id: 10, title: "荣耀6", price: 4999
}
},
{
id: 2,
item: {
id: 20, title: "小米11", price: 3999
}
}
]
}
此处要维护“张三”的订单与商品“荣耀”和“小米11”的关系,不得不冗余的将这两个商品保存在张三的订单文档中,不够优雅。还是建议用业务来维护关联关系。
传统关系型数据库会基于Sql语句做查询,语法有统一标准;
而不同的非关系数据库查询语法差异极大,五花八门各种各样。
而非关系型数据库往往不支持事务,或者不能严格保证ACID的特性,只能实现基本的一致性。
除了上述四点以外,在存储方式、扩展性、查询性能上关系型与非关系型也都有着显著差异,总结如下:
Redis 一个开源的基于键值对(Key-Value)NoSQL 数据库。使用 ANSIC 语言编写、支持网络、基于内存但支持持久化。性能优秀,并提供多种语言的 API。
我们要首先理解一点,我们把 Redis 称为 KV 数据库,键值对数据库,那就可以把 Redis 内部的存储视为存在着一个巨大的 Map,对 Map 的操作无非就是get 和 put,然后通过 key 操作这个 key 所对应的 value,而这个 value 的类型可以多种多样,也就是 Redis 为我们提供的那些数据结构,比如字符串(String)、哈希(Hash)等等。
Redis 会将所有数据都存放在内存中,所以它的读写性能非常惊人。不仅如此,Redis 还可以将内存的数据利用快照和日志的形式保存到硬盘上,这样在发生类似断电或者机器故障的时候,内存中的数据不会丢失。
除了上述功能以外,Redis 还提供了键过期、发布订阅、事务、流水线、Lua 脚本等附加功能。
缓存
缓存机制几乎在所有的大型网站都有使用,合理地使用缓存不仅可以加快数据的访问速度,而且能够有效地降低后端数据源的压力。Redis 提供了键值过期时间设置,并且也提供了灵活控制最大内存和内存溢出后的淘汰策略。可以这么说,一个合理的缓存设计能够为一个网站的稳定保驾护航。
排行榜系统
排行榜系统几乎存在于所有的网站,例如按照热度排名的排行榜,按照发布时间的排行榜,按照各种复杂维度计算出的排行榜,Redis 提供了列表和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行榜系统。
计数器应用
计数器在网站中的作用至关重要,例如视频网站有播放数、电商网站有浏览数,为了保证数据的实时性,每一次播放和浏览都要做+1
的操作,如果并发量很大对于传统关系型数据的性能是一种挑战。Redis 天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。
社交网络
赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适合保存这种类型的数据,Redis 提供的数据结构可以相对比较容易地实现这些功能。
消息队列系统
消息队列系统可以说是一个大型网站的必备基础组件,因为其具有业务解耦、 非实时业务削峰等特性。Redis 提供了发布订阅功能和阻塞队列的功能,虽然和专业的消息队列比还不够足够强大,但是对于一般的消息队列功能基本可以满足。
速度快
正常情况下,Redis 执行命令的速度非常快,官方给出的数字是读写性能可以达到 10 万/秒。
基于键值对的数据结构服务器
几乎所有的编程语言都提供了类似字典的功能,例如 Java 里的 map,类似于这种组织数据的方式叫作基于键值的方式,与很多键值对数据库不同的是,Redis 中的值不仅可以是字符串,而且还可以是具体的数据结构,这样不仅能便于在许多应用场景的开发,同时也能够提高开发效率。
Redis 的全称是 Remote Dictionary Server,它主要提供了 5 种数据结构:字符串、哈希、列表、集合、有序集合,同时在字符串的基础之上演变出了位图(Bitmaps)和 HyperLogLog 两种数据结构,并且随着 LBS (Location BasedService,基于位置服务)的不断发展,Redis 中加入有关 GEO(地理信息定位)的功能。
丰富的功能
除了 5 种数据结构,Redis 还提供了许多额外的功能:提供了键过期功能,可以用来实现缓存。
提供了发布订阅功能,可以用来实现消息系统。支持 Lua 脚本功能,可以利用 Lua 创造出新的 Redis 命令。提供了简单的事务功能,能在一定程度上保证事务特性。提供了流水线(Pipeline)功能,这样客户端能将一批命令一次性传到 Redis,减少了网络的开销。
简单稳定
Redis 的简单主要表现在三个方面。
首先,Redis 的源码很少,早期版本的代码只有 2 万行左右,3.0 版本以后由于添加了集群特性,代码增至 5 万行左右。
其次,Redis 使用单线程模型,这样不仅使得 Redis 服务端处理模型变得简单,而且也使得客户端开发变得简单。
最后,Redis 不需要依赖于操作系统中的类库。
Redis 虽然很简单,但是不代表它不稳定。实际的运行中很少出现因为 Redis 自身 bug 而宕掉的情况。
客户端语言多
Redis 提供了简单的 TCP 通信协议,很多编程语言可以很方便地接人到 Redis。
持久化
通常看,将数据放在内存中是不安全的,一旦发生断电或者机器故障,重要的数据可能就会丢失,因此Redis提供了两种持久化方式:RDB 和 AOF,即可以用两种策略将内存的数据保存到硬盘中,这样就保证了数据的可持久性。
主从复制
Redis 提供了复制功能,实现了多个相同数据的 Redis 副本,复制功能是分布式Redis 的基础。
高可用和分布式
Redis Sentinel,它能够保证 Redis 节点的故障发现和故障自动转移。Redis 从 3.0 版本正式提供了分布式实现 Redis Cluster,它是 Redis 真正的分布式实现,提供了高可用、读写和容量的扩展性。
在了解 Redis 的数据结构之前,先了解 Redis 的一些全局命令。
命令 | 说明 |
---|---|
keys * | 查看所有键,同时也支持通配符,如 keys n* |
dbsize | 返回当前数据库中键的总数 |
exists | 检查键是否存在,存在返回 1,不存在返回 0,如 exists name |
del | 删除键,无论值是什么数据结构类型,del 命令都可以将其删除。返回删除键个数,删除不存在键返回 0。同时 del 命令可以支持删除多个键,如 del name age |
expire | Redis 支持对键添加过期时间,当超过过期时间后,会自动删除键,时间单位秒,如 expire name 10 |
ttl | ttl 命令会返回键的剩余过期时间,若返回 -1 则表示键没设置过期时间,-2 键不存在 |
type | 返回键的数据结构类型 |
randomkey | 随机返回一个键 |
rename | 键重命名,为了防止被强行 rename,Redis 提供了 renamenx 命令,确保只有 newKey 不存在时候才被覆盖。由于重命名键期间会执行 del 命令删除旧的键,如果键对应的值比较大,会存在阻塞 Redis 的可能性 |
注意:
Redis是典型的key-value数据库,key一般是字符串,而value包含很多不同的数据类型:
字符串类型的数据结构可以理解为Map
字符串类型是 Redis 最基础的数据结构。首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础。字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如 JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过 512 MB。
设置值 set
set key value [ex seconds] [px milliseconds] [nxlxx]
ex seconds
:为键设置秒级过期时间。px milliseconds
:为键设置毫秒级过期时间。nx
:键必须不存在,才可以设置成功,用于添加。xx
:与 nx 相反,键必须存在,才可以设置成功,用于更新。其中,ex 参数和 expire 命令基本一样。还有一个需要特别注意的地方是如果一个字符串已经设置了过期时间,然后你调用了 set 方法修改了它,它的过期时间会消失。
除了 set 选项,Redis 还提供了 setex 和 setnx 两个命令:
setex key seconds value
setnx key value
setex 和 setnx 的作用和 ex 和 nx 选项是一样的。也就是,setex 为键设置秒级过期时间,setnx 设置时键必须不存在,才可以设置成功。
有什么应用场景吗?
以 setnx 命令为例子,由于 Redis 的单线程命令处理机制,如果有多个客户端同时执行 setnx key value,根据 setnx 的特性只有一个客户端能设置成功,setnx 可以作为分布式锁的一种实现方案。
获取值 get
get key
如果要获取的键不存在,则返回 nil
。
另外,除了单个设置和获取键值,Redis 还支持批量操作。
批量设置值 mset
mset name ayue age 20 sex 男
批量获取值 mget
mget name age sex
如果有些键不存在,那么它的值为 nil
,结果是按照传入键的顺序返回。
批量操作命令可以有效提高效率,假如没有 mget 这样的命令,要执行 n 次 get 命令具体耗时如下:
n 次 get 时间 = n 次网络时间 + n 次命令时间
使用 mget 命令后,要执行 n 次 get 命令操作具体耗时如下:
n 次 get 时间 = 1 次网络时间 + n 次命令时间
Redis 可以支撑每秒数万的读写操作,但是这指的是 Redis 服务端的处理能力,对于客户端来说,一次命令除了命令时间还是有网络时间,假设网络时间为 1 毫秒,命令时间为 0.1 毫秒(按照每秒处理 1 万条命令算),那么执行 1000 次 get 命令需要 1.1 秒(1000*1+1000*0.1=1100ms
),1 次 mget 命令的需要 0.101 秒 (1*1+1000*0.1=101ms
)。
数字运算 incr
incr 命令用于对值做自增操作,返回结果分为三种情况:
incr key
除了 incr 命令,Redis 提供了 decr(自减)、 incrby(自增指定数字)、decrby(自减指定数字)、incrbyfloat(自增浮点数)。
追加指令 append
append 可以向字符串尾部追加值。
append key value
strlen
返回字符串长度。
strlen key
截取字符串 getrange
getrange 截取字符串中的一部分,形成一个子串,需要指明开始和结束的偏移量,截取的范围是个闭区间。
命令 | 说明 | 时间复杂度 |
---|---|---|
get key | 获取值 | O(1) |
del key [key …] | 删除key | O(N)(N是键的个数) |
mset key [key value …] | 批量设置值 | O(N)(N是键的个数) |
mget key [key …] | 批量获取值 | O(N)(N是键的个数) |
incr key | 将 key 中储存的数字值增一 | O(1) |
decr key | 将 key 中储存的数字值减一 | O(1) |
incrby key increment | 将 key 所储存的值加上给定的增量值(increment) | O(1) |
decrby key increment | key 所储存的值减去给定的减量值(decrement) | O(1) |
incrbyfloat key increment | 将 key 所储存的值加上给定的浮点增量值(increment) | O(1) |
append key value | 如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾 | O(1) |
strlen key | 返回 key 所储存的字符串值的长度。 | O(1) |
setrange key offset value | 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始 | O(1) |
getrange key start end | 返回 key 中字符串值的子字符 | O(N)(N是字符串的长度) |
字符串这些命令中,除了 del 、mset、 mget 支持多个键的批量操作,时间复杂度和键的个数相关,为 O(n),getrange 和字符串长度相关,也是 O(n),其余的命令基本上都是 O(1)的时间复杂度,在速度上是非常快的。
字符串类型的使用场景很广泛,如下:
1、缓存功能
Redis 作为缓存层,MySQL 作为存储层,绝大部分请求的数据都是从 Redis 中获取。由于 Redis 具有支撑高并发的特性,所以缓存通常能起到加速读写和降低 后端压力的作用。
2、计数
使用 Redis 作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。
3、共享 Session
一个分布式 Web 服务将用户的 Session 信息(例如用户登录信息)保存在各 自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。
为了解决这个问题, 可以使用 Redis 将用户的 Session 进行集中管理,在这种模式下只要保证 Redis 是高可用和扩展性的,每次用户更新或者查询登录信息都直接从 Redis 中集中获取。
4、限时
很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过 5 次。一些网站限制一个 IP 地址不能在一秒钟之内访问超过 n 次。或者同一 IP 在短时间内多次浏览谋篇文章浏览次数不会一直增加。点赞次数在短时间内不能重复点赞。
hash类型的数据结构可以理解为Map
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
Redis 中每个 hash 可以存储 2^32 - 1 键值对(40多亿)。
基本上,哈希的操作命令和字符串的操作命令很类似,很多命令在字符串类型的命令前面加上了 h 字母,代表是操作哈希类型,同时还要指明要操作的 field 的值。
hset
hset key field value
如果设置成功会返回 1,反之会返回 0。此外 Redis 提供了 hsetnx 命令,它们的关系就像 set 和 setnx 命令一样,只不过作用域由键变为 field。
127.0.0.1:6379> hset hash:test name ayue
(integer) 1
127.0.0.1:6379>
hget
hget key field
获取值
127.0.0.1:6379> hget hash:test name
"ayue"
127.0.0.1:6379>
其他命令:
命令 | 说明 | 时间复杂度 |
---|---|---|
HDEL key field [field] | 删除一个或多个Hash的field | O(N) N是被删除的字段数量 |
HEXISTS key field | 判断field是否存在于Hash中 | O(1) |
HGET key field | 获取Hash中field的值 | O(1) |
HGETALL key | 从Hash中读取全部的域和值 | O(N) N是Hash的长度 |
HINCRBY key field increment | 将Hash中指定域的值增加给定的数字 | O(1) |
HINCRBYFLOAT key field increment | 将Hash中指定域的值增加给定的浮点数 | O(1) |
HKEYS key | 获取Hash的所有字段 | O(N) N是Hash的长度 |
HLEN key | 获取Hash里所有字段的数量 | O(1) |
HMGET key field field | 获取Hash里面指定字段的值 | O(N) N是请求的字段数 |
HMSET key field value [field value …] | 批量设置Hash字段值 | O(N) N是设置的字段数 |
HSET key field value | 设置Hash里面一个字段的值 | O(1) |
HSETNX key field value | 设置Hash的一个字段,只有当这个字段不存在时有效 | O(1) |
HSTRLEN key field | 获取Hash里面指定field的长度 | O(1) |
HVALS key | 获得 Hash 的所有值 | O(N) N是Hash的长度 |
HSCAN key cursor [MATCH pattern] [COUNT count] | 迭代 Hash 里面的元素 |
哈希类型的操作命令中,hdel,hmget,hmset 的时间复杂度和命令所带的 field 的个数相关 O(k),hkeys,hgetall,hvals 和存储的 field 的总数相关,O(N)。其余的命令时间复杂度都是 O(1)。
1、存储对象
Redis哈希对象常常用来缓存一些对象信息,如用户信息、商品信息、配置信息等。
我们以用户信息为例,它在关系型数据库中的结构是这样的:
id | name | age |
---|---|---|
1 | Tom | 15 |
2 | Jerry | 13 |
hmset user:1 name Tom age 15
hmset user:2 name Jerry age 13
相比较于使用Redis字符串存储,其有以下几个优缺点:
原生字符串每个属性一个键。
set user:1:name Tom
set user:1:age 15
优点:简单直观,每个属性都支持更新操作。
缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。
序列化字符串后,将用户信息序列化后用一个键保存
set user:1 serialize(userInfo)
优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。
序列化字符串后,将用户信息序列化后用一个键保存
hmset user:1 name Tom age 15
优点:简单直观,如果使用合理可以减少内存空间的使用。
缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。
2、购物车
购物车主要功能是临时存放欲购买的商品,然后在结算或下订单时,把购物里面的数据全部移除。其数据结构主要包含的字段有:用户ID、商品ID、商品数量等等。通常我们需要实现以下几个功能:
在之前很多电商网站通过 cookie 实现购物车功能,也就是将整个购物车都存储到 cookie里面。
而通过 Redis 定义购物车非常简单:当前登录用户 ID 号做为key,商品 ID 号为 field,加入购物车数量为 value,如下:
hmset cart:001 prod:01 1 prod:02 1
| | |
| | |
| | |
key field value
而对于上述功能,可以通过 Hash 的相关命令来操作。
list类型的数据结构可以理解为Map
列表( list)类型是用来存储多个有序的字符串,a、b、c、d、e 五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element),一个列表最多可以存储 2 ^ 32 - 1 个元素。
在 Redis 中,可以对列表两端插入( push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。
列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
Redis列表对象常用命令如下表(点击命令可查看命令详细说明):
命令 | 说明 | 时间复杂度 |
---|---|---|
BLPOP key [key …] timeout | 删除,并获得该列表中的第一元素,或阻塞,直到有一个可用 | O(1) |
BRPOP key [key …] timeout | 删除,并获得该列表中的最后一个元素,或阻塞,直到有一个可用 | O(1) |
BRPOPLPUSH source destination timeout | 弹出一个列表的值,将它推到另一个列表,并返回它;或阻塞,直到有一个可用 | O(1) |
LINDEX key index | 获取一个元素,通过其索引列表 | O(N) |
LINSERT key BEFORE | AFTER pivot value在列表中的另一个元素之前或之后插入一个元素 | O(N) |
LLEN key | 获得队列(List)的长度 | O(1) |
LPOP key | 从队列的左边出队一个元素 | O(1) |
LPUSH key value [value …] | 从队列的左边入队一个或多个元素 | O(1) |
LPUSHX key value | 当队列存在时,从队到左边入队一个元素 | O(1) |
LRANGE key start stop | 从列表中获取指定返回的元素 | O(S+N) |
LREM key count value | 从列表中删除元素 | O(N) |
LSET key index value | 设置队列里面一个元素的值 | O(N) |
LTRIM key start stop | 修剪到指定范围内的清单 | O(N) |
RPOP key | 从队列的右边出队一个元 | O(1) |
RPOPLPUSH source destination | 删除列表中的最后一个元素,将其追加到另一个列表 | O(1) |
RPUSH key value [value …] | 从队列的右边入队一个元素 | O(1) |
RPUSHX key value | 从队列的右边入队一个元素,仅队列存在时有效 | O(1) |
列表类型的操作命令中,llen,lpop,rpop,blpop 和 brpop 命令时间复杂度都是 O(1),其余的命令的时间复杂度都是 O(n),只不过 n 的值根据命令不同而不同,比如 lset,lindex 时间复杂度和命令后的索引值大小相关,rpush 和 lpush 和插入元素的个数相关等等。
1、消息队列
但使用 Redis 做消息队列存在很多问题,如消息确认 ACK,消息丢失等,所以一般来说还是用比较专业的 MQ 中间件。
2、文章列表
如下面这样的文章列表,当用户和文章都越来越多时,为了加快程序的响应速度,我们可以把用户自己的文章存入到 List 中,因为 List 是有序的结构,所以这样又可以完美的实现分页功能,从而加速了程序的响应速度。
上图可表示为:
# 深圳卫健委发布一条消息,消息ID为 99
lpush mes:001 99
# 武汉本地宝发布一条消息,消息ID为 100
lpush mes:001 100
# 获取消息列表‘
lrange mes:001 0 5
set类型的数据结构可以理解为Map
集合( set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。
一个集合最多可以存储 2 ^ 32 - 1 个元素。Redis 除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题。
Redis Set 对象常用命令如下表(点击命令可查看命令详细说明):
命令 | 说明 | 时间复杂度 |
---|---|---|
SADD key member [member …] | 添加一个或者多个元素到集合(set)里 | O(N) |
SCARD key | 获取集合里面的元素数量 | O(1) |
SDIFF key [key …] | 获得队列不存在的元素 | O(N) |
SDIFFSTORE destination key [key …] | 获得队列不存在的元素,并存储在一个关键的结果集 | O(N) |
SINTER key [key …] | 获得两个集合的交集 | O(N*M) |
SINTERSTORE destination key [key …] | 获得两个集合的交集,并存储在一个关键的结果集 | O(N*M) |
SISMEMBER key member | 确定一个给定的值是一个集合的成员 | O(1) |
SMEMBERS key | 获取集合里面的所有元素 | O(N) |
SMOVE source destination member | 移动集合里面的一个元素到另一个集合 | O(1) |
SPOP key [count] | 删除并获取一个集合里面的元素 | O(1) |
SRANDMEMBER key [count] | 从集合里面随机获取一个元素 | |
SREM key member [member …] | 从集合里删除一个或多个元素 | O(N) |
SUNION key [key …] | 添加多个set元素 | O(N) |
SUNIONSTORE destination key [key …] | 合并set元素,并将结果存入新的set里面 | O(N) |
SSCAN key cursor [MATCH pattern] [COUNT count] | 迭代set里面的元素 | O(1) |
scard,sismember 时间复杂度为 O(1),其余的命令时间复杂度为 O(n),其中 sadd,srem 和命令后所带的元素个数相关,spop,srandmember 和命令后所带 count 值相关,交集运算 O(m*k),k 是多个集合中元素最少的个数,m 是键个数,并集、差集和所有集合的元素个数和相关。
1、抽奖活动
常见的抽奖活动,比如基于 Redis 实现抽奖功能。
SPOP(随机移除并返回集合中一个或多个元素)和 SRANDMEMBER(随机返回集合中一个或多个元素)命令可以帮助我们实现一个抽奖系统,如果允许重复中奖,可以使用SRANDMEMBER 命令。
活动 ID 为 001,则
# Tom userID:01 参加活动
sadd action:001 01
# Jerry userID:02 参加活动
sadd action:001 02
# 开始抽奖1名中奖者
srandmember action:001 1 或 spop action:001 1
# 查看有多少用户参加了本次抽奖
smembers action:001
2、点赞功能
比如设计一个微信点赞功能。
# 张三用户ID 为userId:01
# 张三对消息 ID008点赞啦
sadd zan:008 userId:01
# 张三取消了对消息008的点赞
srem zan:008 userId:01
# 检查用户是否点过赞
sismember zan:008 userId:01
# 获取消息ID008所有的点赞用户列表
smembers zan:008
# 消息ID008的点赞数计算
scard zan:008
3、关系设计
如我们要设计一个微博的共同关注,或者可能认识的人。设计如下:
① A 关注的人
sadd A:cares B C D E
② B 关注的人
sadd B:cares A C D F
③ C 关注的人
sadd C:cares A F
按照以上条件:
④ A 和 B 共同关注的人
# D,C
sinter A:cares B:cares
⑤ 我关注的人也关注他
# A 关注的 B 也关注了 F,返回 1 否则返回 0
sismember B:cares F
⑥ 可能认识的人
# C 可能认识的人 C,D
sdiff B:cares C:cares
4、集合操作
setA={A,B,C} setB={B, C}
① 集合与集合之间的交集
sinter setA setB-->得到集合{B,C}
② 集合与集合之间的并集
sunion setA setB -->得到集合{A,B,C}
③ 集合与集合之间的差集
sdiff setA setB-->得到集合{A}
127.0.0.1:6379> SADD setA A B C
(integer) 3
127.0.0.1:6379> SADD setB B C
(integer) 2
127.0.0.1:6379> SINTER setA setB
1) "C"
2) "B"
127.0.0.1:6379> SUNION setA setB
1) "A"
2) "B"
3) "C"
127.0.0.1:6379> SDIFF setA setB
1) "A"
127.0.0.1:6379>
ZSet,有序集合,相对于哈希、列表、集合来说会有一点点陌生,但既然叫有序集合,那么它和集合必然有着联系,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数( score)作为排序的依据。
有序集合中的元素不能重复,但是 score 可以重复,就和一个班里的同学学号不能重复,但是考试成绩可以相同。
有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。
zadd
向有序集合 top:20211221
添加话题和点击量。
zadd hot:20211220 10 薇娅逃税
zadd 命令还有四个选项 nx、xx、ch、incr 四个选项:
Redis列表对象常用命令如下表:
命令 | 说明 | 时间复杂度 |
---|---|---|
BZPOPMAX key [key …] timeout | 从一个或多个排序集中删除并返回得分最高的成员,或阻塞,直到其中一个可用为止 | O(log(N)) |
BZPOPMIN key [key …] timeout | 从一个或多个排序集中删除并返回得分最低的成员,或阻塞,直到其中一个可用为止 | O(log(N)) |
ZADD key [NXXX] [CH] [INCR] score member [score member …] | 添加到有序set的一个或多个成员,或更新的分数,如果它已经存在 | O(log(N)) |
ZCARD key | ||
ZCOUNT key min max | 返回分数范围内的成员数量 | O(log(N)) |
ZINCRBY key increment member | 增量的一名成员在排序设置的评分 | O(log(N)) |
ZINTERSTORE | 相交多个排序集,导致排序的设置存储在一个新的关键 | O(NK)+O(Mlog(M)) |
ZLEXCOUNT key min max | 返回成员之间的成员数量 | O(log(N)) |
ZPOPMAX key [count] | 删除并返回排序集中得分最高的成员 | O(log(N)*M) |
ZPOPMIN key [count] | 删除并返回排序集中得分最低的成员 | O(log(N)*M) |
ZRANGE key start stop [WITHSCORES] | 根据指定的index返回,返回sorted set的成员列表 | O(log(N)+M) |
ZRANGEBYLEX key min max [LIMIT offset count] | 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。 | O(log(N)+M) |
ZREVRANGEBYLEX key max min [LIMIT offset count] | 返回指定成员区间内的成员,按字典倒序排列,分数必须相同 | O(log(N)+M) |
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] | 返回有序集合中指定分数区间内的成员,分数由低到高排序。 | O(log(N)+M) |
ZRANK key member | 确定在排序集合成员的索引 | O(log(N)) |
ZREM key member [member …] | 从排序的集合中删除一个或多个成员 | O(M*log(N)) |
ZREMRANGEBYLEX key min max | 删除名称按字典由低到高排序成员之间所有成员。 | O(log(N)+M) |
ZREMRANGEBYRANK key start stop | 在排序设置的所有成员在给定的索引中删除 | O(log(N)+M) |
ZREMRANGEBYSCORE key min max | 删除一个排序的设置在给定的分数所有成员 | O(log(N)+M) |
ZREVRANGE key start stop [WITHSCORES] | 在排序的设置返回的成员范围,通过索引,下令从分数高到低 | O(log(N)+M) |
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count] | 返回有序集合中指定分数区间内的成员,分数由高到低排序。 | O(log(N)+M) |
ZREVRANK key member | 确定指数在排序集的成员,下令从分数高到低 | O(log(N)) |
ZSCORE key member | 获取成员在排序设置相关的比分 | O(1) |
ZUNIONSTORE | 添加多个排序集和导致排序的设置存储在一个新的键 | O(N)+O(M log(M)) |
ZSCAN key cursor [MATCH pattern] [COUNT count] | 迭代sorted sets里面的元素 | O(1) |
参考上表。
有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。
如上热搜榜,以日期为 key :
① 点击热搜,每次加 1
zincrby hot:20211220 1 薇娅逃税
② 右侧排行实现,展示今日前 50 排名
# zrange 是从低到高返回,zrevrange 反之
zrevrange hot:20211221 0 49 withscores
什么是缓存?
就像自行车,越野车的避震器!
举个例子:越野车,山地自行车,都拥有"避震器",防止车体加速后因惯性,在酷似"U"字母的地形上飞跃,硬着陆导致的损害,像个弹簧一样;
同样,实际开发中,系统也需要"避震器",防止过高的数据访问猛冲系统,导致其操作线程无法及时处理信息而瘫痪;
这在实际开发中对企业讲,对产品口碑,用户评价都是致命的;所以企业非常重视缓存技术!
缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码(例如:
// 例1:
Static final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>(); // 本地用于高并发
// 例2:
static final Cache<K,V> USER_CACHE = CacheBuilder.newBuilder().build(); // 用于redis等缓存
// 例3:
Static final Map<K,V> map = new HashMap(); // 本地缓存
由于其被Static修饰,所以随着类的加载而被加载到内存之中,作为本地缓存,由于其又被final修饰,所以其引用(例3:map)和对象(例3:new HashMap())之间的关系是固定的,不能改变,因此不用担心赋值(=)导致缓存失效;
缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力
实际开发过程中,企业的数据量,少则几十万,多则几千万,这么大数据量,如果没有缓存来作为"避震器",系统是几乎撑不住的,所以企业会大量运用到缓存技术;
但是缓存也会增加代码复杂度和运营的成本:
实际开发中,会构筑多级缓存来使系统运行速度进一步提升,例如:本地缓存与redis中的缓存并发使用
浏览器缓存:主要是存在于浏览器端的缓存
**应用层缓存:**可以分为tomcat本地缓存,比如之前提到的map,或者是使用redis作为缓存
**数据库缓存:**在数据库中有一片空间是 buffer pool,增改查数据都会先加载到mysql的缓存中
**CPU缓存:**当代计算机最大的问题是 cpu性能提升了,但内存读写速度没有跟上,所以为了适应当下的情况,增加了cpu的L1,L2,L3级的缓存
缓存更新是redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis插入太多数据,此时就可能会导致缓存中的数据过多,所以redis会对部分数据进行更新,或者把他叫为淘汰更合适。
内存淘汰: redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)
过期淘汰: 当我们给redis设置了过期时间ttl之后,redis会将过期的数据进行删除,方便咱们继续使用缓存
主动更新: 我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题
注意:缓存更新策略的选择要基于业务场景来看:
由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在,其后果是:
用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等;怎么解决呢?有如下几种方案
Cache Aside Pattern :缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案
Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理
Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致
操作缓存和数据库时有三个问题需要考虑:
如果采用 Cache Aside Pattern 方案,那么假设我们每次操作数据库后,都操作缓存,但是中间如果没有人查询,那么这个更新动作实际上只有最后一次生效,中间的更新动作意义并不大,我们可以把缓存删除,等待再次查询时,将缓存中的数据加载出来
删除缓存还是更新缓存?
如何保证缓存与数据库的操作的同时成功或失败?
应该具体操作缓存还是操作数据库,我们应当是先操作数据库,再删除缓存,原因在于,如果你选择第一种方案,在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。
缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
缓存空对象思路分析: 当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了
布隆过滤: 布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,
假设布隆过滤器判断这个数据不存在,则直接返回
这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大
解决方案一 使用锁
因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。
假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。
解决方案二 逻辑过期方案
方案分析:我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。
我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。
这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。
进行对比
互斥锁方案: 由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响
逻辑过期方案: 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦
Redis有两种持久化方案:
RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。快照文件称为RDB文件,默认是保存在当前运行目录。
RDB持久化在四种情况下会执行:
1)save命令
执行下面的命令,可以立即执行一次RDB:
save命令会导致主进程执行RDB,这个过程中其它所有命令都会被阻塞。只有在数据迁移时可能用到。
2)bgsave命令
下面的命令可以异步执行RDB:
这个命令执行后会开启独立进程完成RDB,主进程可以持续处理用户请求,不受影响。
3)停机时
Redis停机时会执行一次save命令,实现RDB持久化。
4)触发RDB条件
Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:
# 900秒内,如果至少有1个key被修改,则执行bgsave , 如果是save "" 则表示禁用RDB
save 900 1
save 300 10
save 60 10000
RDB的其它配置也可以在redis.conf文件中设置:
# 是否压缩 ,建议不开启,压缩也会消耗cpu,磁盘的话不值钱
rdbcompression yes
# RDB文件名称
dbfilename dump.rdb
# 文件保存的路径目录
dir ./
Redis会单独创建(fork)一个子进程来进行持久化。
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。
fork采用的是copy-on-write技术:
RDB方式bgsave的基本流程?
RDB会在什么时候执行?save 60 1000代表什么含义?
RDB的缺点?
AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
AOF的命令记录的频率也可以通过redis.conf文件来配:
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
三种策略对比:
因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
如图,AOF原本有三个命令,但是set num 123 和 set num 666
都是对num的操作,第二次会覆盖第一次的值,因此第一个命令记录下来没有意义。
所以重写命令后,AOF文件内容就是:mset name jack num 666
Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置:
# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用。
RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储。
AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AO F命令以 Redis 协议追加保存每次写的操作到文件末尾,Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大。
只做缓存,如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化。
同时开启两种持久化方式:
在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件,那要不要只使用AOF呢?作者建议不要,因为 RDB 更适合用于备份数据库(AOF 在不断变化不好备份),快速重启,而且不会有 AOF 可能潜在的 Bug,留着作为一个万一的手段。
性能建议:
因为 RDB 文件只用作后备用途,建议只在 Slave(从节点) 上持久化 RDB 文件,而且只要 15 分钟备份一次就够了,只保留 save 900 1 这条规则。
如果开启 AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只 load 自己的AOF文件就可以了,代价一是带来了持续的 IO,二是AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF重写的基础大小默认值 64M 太小了,可以设到 5G 以上,默认超过原大小 100% 大小重写可以改到适当的数值。
如果不开启 AOF ,仅靠 Master-Slave Repllcation(主从复制) 实现高可用性也可以,能省掉一大笔IO,也减少了 rewrite 时带来的系统波动。代价是如果 Master/Slave 同时挂掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB 文件,载入较新的那个(微博就是这种架构)。
参考资料:
探索数据库
Redis常用数据结构及应用场景
Redis的RDB与AOF详解