(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)文章列表。

 

posted @   ShineLe  阅读(44)  评论(0编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示