(2)Redis API理解与应用
学习自:《Redis开发与运维》pdf 60页
2023-08-25
1、全局命令
Redis:Key指令2、数据结构与内部编码
Redis共有5种数据结构string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)。但这些只是Redis对外的数据结构:
但这些数据结构,在底层编码(C语言)实现上,有多种实现方式,这样Redis可以在合适的场景选择合适的内部编码:
可以看到每种数据结构都有两种以上的内部编码实现。有的内部编码,如ziplist,可以作为多种外部数据结构的内部实现。使用object encoding可以查询到内部编码:
之所以有外部类型和内部编码类型之分,有两个好处:
1)可以改进内部编码,对外显示的数据结构和命令没什么影响;这样的话,一旦开发出更优秀的内部编码,无需改动外部数据结构和命令,例如Redis 3.2提供了quicklist,结合了ziplist与linkedlist的优势,为list类型提供了一种更优秀的内部编码实现,但是对外部用户来说基本感知不到;
2)多种编码在多种场景下发挥各自优势;ziplist节省内存,但在元素较多的情况下,性能不如linkedlist,此时就会灵活切换。
3、单线程架构
Redis使用了单线程机制和IO多路复用来实现高性能的内存数据库服务。
1)单线程模型
在同一台机器上开了三个redis-cli客户端,同时执行了如下命令:
1 2 3 4 5 6 | #客户端1 set hello world #客户端2 incr counter #客户端3 incr counter |
现在我们看看这三条指令的执行过程。
Redis C/S模型可以简化为下图:
每次C端调用都会经历发送命令、执行命令、返回结果三个过程。
其中第二步执行命令是要重点讨论的,因为Redis使用单线程处理命令,所以一条命令从C端到达S端不会被立刻执行,所有的命令都会进入一个队列中,然后被逐个执行,因此上边这三条命令的执行顺序是不确定的,但唯一可以肯定的是不会有两条命令同时被执行:
这样,两个incr命令无论如何最终结果都是2,不会产生并发问题,这就是Redis单线程模型。
看起来很简单,但是像发送命令、返回结果、命令排队并不像描述的这么简单,Redis使用了IO多路复用技术来解决IO问题。
为什么使用了单线程还能这么快
通常来说,单线程的处理能力要比多线程差,那为什么Redis用单线程模型会达到每秒万级的处理能力?原因有三点:
1)纯内存访问
Redis所有数据都放在内存中,内存响应时长约为100ns,这是Redis每秒万级访问的重要基础;
2)非阻塞I/O
Redis使用epoll作为IO多路复用技术的实现,再加上Redis自身的事件处理模式会将epoll中的连接、读写、关闭都转换为事件,因此不会在网络I/O上浪费过多时间:
3)单线程避免了线程切换和竞态产生的消耗。
对于S端开发而言,锁和线程切换通常是性能杀手。
但是单线程模式也存在一个问题:对单条命令的执行时间有要求,如果某条命令执行过长,会造成其他命令的阻塞。这对Redis这种高性能服务是致命的,因此Redis是面向快速执行场景的数据库。
4、字符串 string
基本用法见:redis:string类型
还有一些原理方面的内容
1)setnx的作用
由于redis的单线程命令处理机制,如果多个C端同时执行setnx K V,那么此时只有一个C端可以设置成功。这可以作为分布式锁的一种实现方案,Redis官方给出了利用setnx实现分布式锁的方法:http://redis.io/topics/distlock
2)mget与get
mget批获取命令可以提高开发效率,用get获取n个值需要执行n次get命令:
耗时=n次网络时间+n次命令时间
而在用了mget之后,耗时 = 1次网络传输 + n次命令时间
Redis可以支持每秒万次的读写操作,这是针对S端而言的。
对于C端,一次命令=网络时间+命令时间,因为Redis的命令处理能力已经足够,所以对于开发人员的制约瓶颈主要是网络时间。
批处理有助于提高业务效率,但是每次批操作所发送的指令数并非无节制的,如果数量过多可能造成Redis阻塞或者网络阻塞。
很多存储系统和编程语言内部用CAS机制实现计数功能,会有一定的CPU开销,但是在Redis中不存在这个问题,因为Redis是单线程架构,所有命令到了Redis S端都要顺序执行。
内部编码:int、embstr、raw
3)string应用场景
①缓存
一个比较典型的缓存使用场景如下:
在该场景中,Redis作为缓存层,MySQL作为存储层,这样Web服务的绝大多数请求都是从Redis中获取到的。
由于Redis支持高并发,所以缓存通常能起到加速读写、降低后端压力的作用。
下边给出一段模拟上图过程的伪代码:
a、先获取用户基础信息
1 2 3 | UserInfo getUserInfo(long id ){ ... } |
b、从Redis获取用户信息
1 2 3 4 5 6 7 8 9 | // 定义Key userKey = "user:info:" + id ; // 从Redis获取V value=redis.get(userKey); if (value!=null){ // 将V进行反序列化为UserInfo并返回结果 userInfo=deserialize(value); return userInfo; } |
与MySQL不同,Redis没有命名空间,而且对K也没有强制要求。但是设计一个合适的K,有助于防止K冲突和项目的可维护性,推荐的方式是用业务名:对象名:id:属性作为K。
例如MySQL数据库名为vs,表为user,那么对应的K可以写为vs:user:1或vs:user:1:name,如果当前Redis只被一个业务使用,甚至可以去掉vs:。如果K太长,则可以在能描述K含义的前提下适当减少K的长度,例如u:uid:fr:m:mid,从而减少K过长引发的内存浪费。
c、如果没有从Redis获取到用户信息,需要从MySQL中进行获取,并将结果写回Redis,添加1h的过期时间
1 2 3 4 5 | // 从mysql获取用户信息 userInfo=mysql.get( id ); // 将userInfo序列化,存入Redis redis.setex(userKey,3600,serialize(userInfo)); return userInfo; |
全过程的伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | UserInfo getUserInfo(long id ){ userKey= "user:info:" + id // 一个好的Key命名 value=redis.get(userKey); UserInfo userInfo; if (value!=null){ userInfo=deserialize(value); } else { userInfo=mysql.get( id ); if (userInfo!=null) redis.setex(userKey,3600,serialize(userInfo)); } return userInfo; } |
②计数
1 2 | K= "video:playCount:" + id ; return redis.incr(K) |
③共享Session
一个分布式Web服务会将用户的Session信息(例如登录信息)保存在各自的服务器中,通常出于负载均衡的考虑,分布式服务也会将用户访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这是用户所无法容忍的(注意,此时是没有用Redis的情况):
为了解决此问题,可以用Redis将用户的Session进行集中管理,这种情况下只要保证Redis的高可用和扩展性,每次用户更新都可以直接从Redis中集中获取到Session:
④限速
很多应用出于安全考虑会设置登录时的手机验证码,但为了接口不被频繁访问,会限制用户获取验证码的频率,如1min不能超过5次,如下图所示:
该功能可以用Redis来实现,下面给出实现思路的伪代码:
1 2 3 4 5 6 7 8 9 | phoneNum= "138xxxxxxxx" ; K= "shortMsg:limit:" +phoneNum; // 设置过期时间60s,只有K不存在时才允许设置 isExists=redis. set (K,1, "EX 60" , "NX" ) // 这里只是伪代码,Redis中不用加引号 if (isExists!=null||redis.incr(K)<=5){ // 通过 } else { // 限速 } |
上述就利用Redis实现了限速功能,例如一个网站限制一个IP不能在1s内访问超过n次也可以采用类似的思路。
以上只是部分应用场景。
5、哈希hash
redis:hash几乎所有语言都提供了hash类型,它也有可能被叫做哈希、字典、关联数组,但是它本质上是一组K-V映射,注意这里是一组不是一个,也就是说一个数据结构包含了很多K-V映射。
当V是hash时,它的写法就像:V={{f1,v1},{f2,v2},...},这种在V中的K-V对叫做Field-Value对,我常将其简写为f-v对。
使用场景
①用hash缓存用户信息,这种用户信息在关系数据库中有统一的组织形式
用户信息的缓存方式
1)原生字符串,每个K-V存储一个用户属性:
1 2 3 | set user:1:name tom set user:1:age 23 set user:1:city beijing |
优点:简单直观,每个属性都支持更新操作。
缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。
2)序列化字符串,将用户信息序列化后用一个K保存
1 | set K serialize(userInfo) |
优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。
3)hash:每个用户属性用一对f-v,但是只用一个K保存
1 | hmset K name tomage 23 city beijing |
优点:简单直观,如果使用合理可以减少内存空间的使用。
缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。
2023-08-28
《Redis开发与运维》106页
6、List
List用于存储多个有序字符换,List中存放的每个字符串称为元素,一个List最多可以存储2^32-1个元素。
有序:可以通过下表的方式获取元素或者某个范围内的元素List
在Redis中,可以在List两端进行插入(Push)和弹出(Pop)、获取指定位置、指定范围的元素。
使用场景
1、消息队列:lpush+brpop
实现C/S的负载均衡和高可用性
2、文章列表
分页展示文章列表
小结
1、Redis的C/S模型:
每次C端调用都要经历 发送命令→执行命令→返回结果的过程。
由于Redis是单线程处理命令,所以一条指令从C到S不会立刻执行,而是进入队列,然后逐个执行,所以多个客户端命令的执行顺序是不确定的,但是不会有两条指令被同时执行。
2、为什么单线程也能这么快?
1)Redis数据都放在内存中,内存响应时间很短,约为100ns;
2)Redis使用epoll作为IO多路复用技术,并且其自身的事件处理模式会将epoll中的操作都转为事件,因此不会在网络IO上浪费时间;
3)单线程避免了线程切换的消耗。
但是单线程模式不适用于长执行时间单条命令的情况,因此Redis是面向快速执行场景的数据库。
3、string
1)当要取多个数据时,用mget要比get更有效率,因为此时只用消耗一次网络传输时间。
2)Redis支持每秒万次的读写操作,但这是针对S端而言的,对于C端,一次命令=网络时间+命令执行时间。命令执行时间已经很快了,因此网络时间才是对开发人员的制约瓶颈。
3)每次操作发送的指令过多可能造成Redis阻塞或网络阻塞。
4)Redis是单线程,因此不需要CAS计数,所有命令在Redis S端顺序执行。
5)Key命名:业务名:对象名:id:属性
6)使用场景
缓存:Redis作为Web与数据库之间的缓存层,存储了大量数据,这样Web服务绝大多数请求都是从Redis中获取到的。
计数:incr
共享Session:将用户Session集中管理,这样服务器在用户刷新时能直接从Redis中获取到Session
限速
4、hash
1)hash是K-V的映射,其中V是多个Field-Value对
2)用户信息的缓存方式:
- 原生字符串,每个K-V存储一个用户属性
- 序列化字符串,将用户信息序列化后保存为一个K
- hash,每个用户属性用一对f-v,最后组织成一个K-V
5、list
1)使用场景
消息队列:实现C/S负载均衡与高可用性
2)文章列表。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性