认识redis
NoSQL
Nosql = not only sql,泛指非关系型数据库,与之相对的是RDBMS(Relational Database Management System),即关系型数据库
关系型数据库:列+行,同一个表下数据的结构是一样的。
非关系型数据库:数据存储没有固定的格式,并且可以进行横向扩展。
redis就是当下最好的NoSQL
Nosql和RDBMS的区别
-
RDBMS
- 组织化结构
- 固定SQL
- 数据和关系都存在单独的表中(行列)
- DML(数据操作语言)、DDL(数据定义语言)等
- 严格的一致性(ACID): 原子性、一致性、隔离性、持久性
- 基础的事务
-
NoSQL
- 不仅仅是数据
- 没有固定查询语言
- 键值对存储(redis)、列存储(HBase)、文档存储(MongoDB)、图形数据库(不是存图形,放的是关系)(Neo4j)
- 最终一致性(BASE):基本可用、软状态/柔性事务、最终一致性
redis是什么
Redis(Remote Dictionary Server),即远程字典服务。
数据都是缓存在内存中,查询和修改很快。redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
准备工作
# 安装redis
brew install redis
# 启动redis服务端
redis-server
# 连接redis: IP + 端口号
redis-cli -h 127.0.0.1 -p 6379
五种基本数据结构
string
简介
- String类型是redis的最基础的数据结构,也是最经常使用到的类型。而且其他的四种类型多多少少都是在字符串类型的基础上构建的,所以String类型是redis的基础。
- String 类型的值最大能存储 512MB,这里的String类型可以是简单字符串、复杂的xml/json的字符串、二进制图像或者音频的字符串、以及可以是数字的字符串
底层实现
String 类型的底层的数据结构实现主要是 int 和 SDS(Simple dynamic string,简单动态字符串),SDS相比于C语言的字符串:
- SDS 不仅可以保存文本数据,还可以保存二进制数据。因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
- SDS 获取字符串长度的时间复杂度是 O(1)。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用 len 属性记录了字符串长度,所以复杂度为 O(1)。
- Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。
string对象的内部编码有三种:int、raw、embstr(后两种的实现都是SDS)
- 如果string保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在对象结构的ptr属性里。
- 如果string对象保存的是一个长度<=32byte的字符串,那么字符串对象将使用SDS来保存,并将编码方式设为embstr,这是一种专门优化了保存短字符串的编码方式
- 如果string对象保存的是一个长度<=32byte的字符串,那么字符串对象将使用SDS来保存,并将编码方式设为raw
embstr和raw虽然都用SDS来保存,但是embstr会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject和SDS,而raw编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObject和SDS
- embstr优点:
- embstr编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次
- 释放 embstr编码的字符串对象同样只需要调用一次内存释放函数
- 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存,从而提升性能
- embstr缺点:
- 如果字符串的长度增加需要重新分配内存时,整个redisObject和SDS都需要重新分配空间,即embstr编码的字符串对象实际上是只读的,redis没有为embstr编码的字符串对象编写任何相应的修改程序。
- 当我们对embstr编码的字符串对象执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令
使用
// 设置指定 key 的值。
redis 127.0.0.1:6379> SET test redis
OK
// 删除key
127.0.0.1:6379> del test
(integer) 1
127.0.0.1:6379> get test
(nil)
// 获取指定 key 的值。
redis 127.0.0.1:6379> GET test
"redis"
// 获取key的值的子字符串
127.0.0.1:6379> getrange test 0 1
"re"
// 将key的值更新为新值,返回旧值
127.0.0.1:6379> getset test redis1
"redis"
// 当key不存在时才能设置,即只能新建
127.0.0.1:6379> setnx test redis
(integer) 0
127.0.0.1:6379> setnx qian redis
(integer) 1
// 返回key所存储的值的长度
127.0.0.1:6379> strlen qian
(integer) 5
// setex:将值value关联到key,并将key的过期时间设为seconds(以秒为单位)。
127.0.0.1:6379> set qian redis
OK
127.0.0.1:6379> setex qian 10 redis11
OK
127.0.0.1:6379> get qian // 更新了key的值
"redis11"
127.0.0.1:6379> get qian // 过了10秒就过期了
(nil)
// msetes:同上,但是过期时间为毫秒
msetex qian 10 redis11
应用场景
1、缓存对象
使用 String 来缓存对象有两种方式:
- 直接缓存整个对象的JSON,命令例子:
SET user:1 '{"name":"xiaolin", "age":18}'
。 - 采用将 key 进行分离为user:ID:属性,采用MSET存储,用MGET获取各属性值,命令例子:
MSET user:1:name xiaolin user:1:age 18 user:2:name xiaomei user:2:age 20
。
2、常规计数
因为 Redis 处理命令是单线程,所以执行命令的过程是原子的。因此 String 数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。
# 初始化文章的阅读量
> SET aritcle:readcount:1001 0
OK
#阅读量+1
> INCR aritcle:readcount:1001
(integer) 1
# 获取对应文章的阅读量
> GET aritcle:readcount:1001
"3"
3、分布式锁
SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁:如果key不存在,则插入成功,可以用来表示加锁成功;如果key存在,则加锁失败。
SET lock_key unique_value NX PX 10000
- lock_key 就是 key 键;
- unique_value 是客户端生成的唯一的标识;
- NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
- PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
而解锁的过程就是将 lock_key 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。
4、共享session信息
通常后台系统会使用session来保存用户的会话状态,这些 Session 信息会被保存在服务器端,但这只适用于单系统应用,如果是分布式系统此模式将不再适用。
用户一的 Session 信息被存储在服务器一,但第二次访问时用户一被分配到服务器二,这个时候服务器并没有用户一的 Session 信息,就会出现需要重复登录的问题,问题在于分布式系统每次会把请求随机分配到不同的服务器。
因此,我们需要借助 Redis 对这些 Session 信息进行统一的存储和管理,这样无论请求发送到那台服务器,服务器都会去同一个 Redis 获取相关的 Session 信息,这样就解决了分布式系统下 Session 存储的问题。
Hash
简介
- hash是一个键值对(key-value)集合,它是一个 string 类型的 field 和 value 的映射表
- redis本身就是一个key-value型数据库,因此hash数据结构相当于在value中又套了一层key-value型数据,所以redis中hash数据结构特别适合存储关系型对象
- 可以像数据库中update一个属性一样只修改某一项属性值
- 如果哈希类型元素个数小于512个,redis使用压缩列表作为底层结构;否则的话会使用redis作为底层结构。另外,在Redis 7.0中,压缩列表已经废弃,交由listpack实现
使用
// key = qian,然后设置了他的一些value,“name/age/sex”
127.0.0.1:6379> hmset qian name "qjl" age "23" sex "male"
OK
127.0.0.1:6379> hgetall qian
1) "name"
2) "qjl"
3) "age"
4) "23"
5) "sex"
6) "male"
// 删除key的某个字段
127.0.0.1:6379> hdel qian sex
(integer) 1
// 查看key的某个字段是否存在
127.0.0.1:6379> hexists qian sex
(integer) 0
// 查看key的某个字段
127.0.0.1:6379> hget qian sex
(nil)
// 获取哈希表中的所有字段
127.0.0.1:6379> hkeys qian
1) "name"
2) "age"
// 获取哈希表中字段的数量
127.0.0.1:6379> hlen qian
(integer) 2
// 更改key的值
127.0.0.1:6379> hset qian name spiderman
(integer) 0
127.0.0.1:6379> hgetall qian
1) "name"
2) "spiderman"
3) "age"
4) "23"
// 删除key
127.0.0.1:6379> del qian
(integer) 1
127.0.0.1:6379> hgetall qian
(empty array)
使用场景
1、缓存对象
存储对象时,一般使用用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。
# 存储一个哈希表uid:1的键值
> HMSET uid:1 name qian age 23
2
# 存储一个哈希表uid:2的键值
> HMSET uid:2 name zhou age 24
2
# 获取哈希表用户id为1中所有的键值
> HGETALL uid:1
1) "name"
2) "qian"
3) "age"
4) "23"
2、购物车
以用户 id 为 key,商品 id 为 field,商品数量为 value
# 添加商品:
HSET cart:user1 iphone1 1
# 添加数量:
HINCRBY cart:user1 iphone1 1
# 商品总数:
HLEN cart:user1
# 删除商品:
HDEL cart:user1 iphone1
# 获取购物车所有商品:
HGETALL cart:user1
在回显商品具体信息的时候,还需要拿着商品 id 查询一次数据库,获取完整的商品的信息。
List
简介
- 增删快,提供了操作某一段元素的API
- list类型是用来存储多个有序的字符串的,列表当中的每一个字符看做一个元素
- 一个列表当中可以存储有一个或者多个元素,redis的list支持存储232-1个元素。
- 可以从列表的两端进行插入(pubsh)和弹出(pop)元素,支持读取指定范围的元素集,或者读取指定下标的元素等操作。
- 列表是一种比较灵活的链表数据结构,它可以充当队列或者栈的角色。
- 列表是链表型的数据结构,所以它的元素是有序的,而且列表内的元素是可以重复的。意味着它可以根据链表的下标获取指定的元素和某个范围内的元素集。
底层实现
在Redis 3.2之后,List数据类型底层就只由quicklist实现了,替代了之前的双向链表和压缩列表
使用
// 将值插入到key链表的头部
127.0.0.1:6379> lpush me male
(integer) 1
127.0.0.1:6379> lpush me qian
(integer) 2
// 将值插入到key链表的尾部
127.0.0.1:6379> rpush me 23
(integer) 3
// 查看0-10范围内的元素
127.0.0.1:6379> lrange me 0 10
1) "qian"
2) "male"
3) "23"
// 通过索引获取列表中的元素,下标从零开始
127.0.0.1:6379> lindex me 2
"23"
// 弹出链表尾部元素
127.0.0.1:6379> rpop me
"23"
// 弹出链表头部元素
127.0.0.1:6379> lpop me
"qian"
127.0.0.1:6379> lrange me 0 10
1) "male"
// 通过索引更改链表的值
127.0.0.1:6379> lset me 0 qian
OK
127.0.0.1:6379> lrange me 0 10
1) "qian"
// 只保留指定区间内的元素,其余元素全部删除
127.0.0.1:6379> ltrim me 0 1
OK
127.0.0.1:6379> lrange me 0 10
1) "qian"
2) "male"
// 从一个列表弹出一个值放入另一个list中,如果没有可以弹出的元素会阻塞等待到超时或有元素
127.0.0.1:6379> lrange me 0 10
1) "qian"
127.0.0.1:6379> lpush me male
(integer) 2
127.0.0.1:6379> lrange you 0 10
(empty array)
127.0.0.1:6379> lpush you female
(integer) 1
127.0.0.1:6379> brpoplpush you me 1000
"female"
127.0.0.1:6379> lrange you 0 10
(empty array)
127.0.0.1:6379> lrange me 0 10
1) "female"
2) "male"
3) "qian"
使用场景
1、消息队列
- 为了保证消息接受的顺序,可以使用LPUSH+RPOP/RPUSH+LPOP的命令实现消息队列。生产者向list中写入数据,多个消费者不停发调用pop命令,如果list中有消息,就返回,没有就继续循环。但是这种做法会导致消费者不停执行rpop/lpop命令,浪费CPU资源。
redis还提供了brpop的命令,可以实现阻塞式存取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据,节省CPU开销
- 为了避免重复的消息,还可以在消息中添加一个全局的ID。消费者收到消息后先比较这次收到的消息ID和之前收到过的ID,如果没已经收到过了,就不再处理了。
# 添加唯一ID:20230514
lpush mq "20230514:qian:0039"
(integer) 1
- 为了保证消息可靠性,防止接受了消息但是还没处理就发生意外导致消息丢失,可以使用brpoplpush的命令,从队列中读取消息后加入到备份list中存档,这样,如果消费者没来得及处理消息还可以去备份list中重新读取
- 但是list中一个消息只能有一个消费者接收,也不支持消费者组,通常使用Stream类型实现mq