当前位置: 首页 > news >正文

Redis 5.x——多种数据类型使用场景

多种数据类型使用场景

    • 1、String数据类型的API和使用场景
      • 1.1、String数据类型的常规操作
        • 1.1.1、设置与获取键的内容
        • 1.1.2、获取子字符串
        • 1.1.3、 返回旧值
      • 1.2、结合应用场景演练
        • 1.2.1、单个值缓存
        • 1.2.2、对象缓存
        • 1.2.3、计数器
        • 1.2.4、唯一自增的ID或者流水号
        • 1.2.5、文章阅读数或者网页浏览数统计
        • 1.2.6、分布式锁
    • 2、String也能做,为什么还要用Hash
      • 2.1、Hash数据类型的常规操作
      • 2.2、结合应用场景演练
        • 2.2.1、作为实时数据库
        • 2.2.2、使用Hash数据类型实现购物车
        • 2.2.3、作为计数器
    • 3、一种类型多种用法,原来List还可以这样做
      • 3.1、List模拟数据结构
        • 3.1.1、先进后出操作
        • 3.1.2、先进先出操作
        • 3.1.3、阻塞队列
        • 3.1.4、模拟消息推送
      • 3.2、List常规操作
      • 3.3、利用列表迅速提升网站首页的并发量
    • 4、开启Set数据类型的正确操作姿势
      • 4.1、明星绯闻和微博瘫痪的那些事
      • 4.2、抽奖逻辑
      • 4.3、文章点赞或者投票
      • 4.4、共同好友统计
    • 5、微服务的限流操作和Zset的那些瓜葛
      • 5.1、利用Zset实现限流
        • 5.1.1、滑动窗口限流方式
        • 5.1.2、令牌桶
      • 5.2、新闻排行榜场景实战
      • 5.3、直播打赏排名场景实战

1、String数据类型的API和使用场景

1.1、String数据类型的常规操作

1.1.1、设置与获取键的内容
#为键设置指定的内容setname zhangsan#获取键的内容get name

运行效果如图所示。

1.1.2、获取子字符串
#设置长字符串setname"my name is zhangsan"#获取长字符串的子字符串,下标从0开始getrange name37

运行效果如图所示。

1.1.3、 返回旧值
#为键设置新的内容,并返回键的旧内容getset name zhangsan

运行效果如图所示。

1.2、结合应用场景演练

1.2.1、单个值缓存

顾名思义,单个值的缓存就是单个字符串的存储。在分布式项目部署的业务中需要一个统一的鉴权和授权中心将相对应的算法生成token,而token本身就是单个字符串,这样完全可以利用Redis中的String数据类型来实现。

图所示的是一个比较简单的分布式架构中的部分环节,网关服务提供的鉴权中心需要判断当前访问的客户端所提供的token是否合法,所以当客户端在第一次请求服务时会带着一些信息,然后由服务根据信息生成一个token,这其中存储的就是经过压缩后的客户端信息和这个信息的过期时间。因为网关服务本身就是分布式部署,所以需要一个分布式的缓存服务器,而Redis刚好可以满足这个要求。

  1. 第一次请求的服务节点生成一个用户token,并存储到Redis服务器,然后可以根据需要设置对应的过期时间,从而保证token过期之后的信息从Redis服务器移除。如果有需要,也可以不设置token在内存中存储的过期时间。
  2. 之后请求的节点只需要通过get命令从缓存中获取token验证合法性,如图所示。

1.2.2、对象缓存

用户信息既可能是一个简单的字符串,也可能是一个对象或者高频率访问但是很少修改的数据,这些数据可以作为一个整体对象,如果想要减轻数据库访问的压力,那么可以将其对象进行json序列化后缓存起来。这样的数据可以采用String数据类型进行存储。

针对这种需求,有两种不同命令的操作方式。

下图所示是将对象一次性转换成json序列化进行存储。


下图所示的是利用mset、mget同时设置多个键、获取多个键的值。

如果我们存储的对象会用到的只是获取某个属性的值,则可以这样去操作。如果我们存储的对象将作为整个对象操作,则不建议这样使用,因为需要把多个键的值序列化,这是需要自己去实现的,没有现成的序列化工具。

1.2.3、计数器

在高并发的业务场景中,可能会遇到各种各样的业务场景,比如接口限流。在双十一或者其他大型节日中,业务服务器的资源有限,但是在高并发的场景下,过多的请求可能会导致服务器宕机等问题,所以会对接口请求做一些限制,比如限制每秒请求总数为200次,超过200次就等待,等下一秒再次请求,这里用Redis作为计数器的模式来实现。

实现流程如下:

1)首先在接到请求之后设置一个键,然后自增1,如果不存在,则值会被初始化为0。

incr mykey#第一次执行返回的结果是1

这里的mykey可能需要特殊处理,因为是根据秒来的,所以当接口得到一个请求之后,应该获取当前时间生成动态的键,如20200101235959:mykey。

2)当接口得到请求后,执行incryyyyMMddHHmmss:mykey,如果返回结果小于200则进行处理,超过200将等待下一秒处理。

3)因为每秒会生成一个键(如下图)​,为了节省内存空间,可能需要一个定时任务,定时删除这些已经使用过的键。

1.2.4、唯一自增的ID或者流水号

在有些业务中,为了提高数据库的读写能力,可能会将一个库根据业务拆分成多个小库、将一张大表拆分成几个小表,比如user01表、user02表,但是又要保证两张表中的ID是全局统一自增的,所以数据库表自带的自增属性是无法使用的。

这时也可以利用Redis的String数据类型实现自增,在新增数据行时,通过调用Redis服务自增功能生成一个自增的数值来实现多张表的唯一自增。

如果数据库表过多,会不会造成Redis的压力过大?这个不必担心,Redis单机的性能还是很不错的,如果读者真的想扩展Redis的服务器,也是可以的,不过这里的实现方式可能需要改变了。

假设目前有5台单独的Redis服务器,那么生成的方式就是这样的:

第1台的生成方式如图所示。第1台服务器从初始值1开始生成的序列号

第2台的生成方式如图所示。第2台服务器从2开始生成的序列号

第3台的生成方式如图所示。第3台服务器从3开始生成的序列号

第4台的生成方式如图所示。第4台服务器从4开始生成的序列号


第5台的生成方式如图所示。第5台服务器从5开始生成的序列号


每一台服务器从初始值1~5开始,然后每次累加数值5,则多个节点生成自增且不重复的序列号。

以上只是利用Redis的自增API处理多个表的唯一自增ID而额外提供的一种方案。如果了解雪花算法或其他更简易的方式,则完全没有必要使用Redis的这种操作。

1.2.5、文章阅读数或者网页浏览数统计

当我们打开浏览器时,会面临各种各样的网页信息,比如技术博客或者新闻博客等,这些网页里面无不充斥着各种统计数量,比如用户的总点赞数、关注数、粉丝数、帖子的评论数、热度、文章的阅读数和收藏数等。实现这些需求并想要减轻数据库压力,而且对数据的实效性和写文章的频率要求高时,则完全可以使用Redis。

实现文章阅读数量统计需要执行如下命令:

#文章第一次被阅读Incr mypageurl#查看文章的阅读数量get mypageurl#访问时每次都自增1Incr mypageurl

1.2.6、分布式锁

随着互联网业务发展和需求的不断变化,应用程序从单体架构一路进化到当下的分布式和微服务架构。在使用单体架构时,多线程操作是通过线程锁来实现资源抢占控制的,但是在分布式系统中需要用其他手段来实现,可以借助于Redis中的setnx命令来完成。

具体操作流程如下:如果键没有值就能新增成功,就表示抢到锁了,否则失败。等到操作成功时释放锁,也就是删除键。

下面是一段秒杀商品的伪代码:

# 给键seckill001赋值,如果之前不存在,则新增成功,返回1,表示抢到了锁setnx seckill001true#当其他客户端进来之后执行如下命令,返回0则表示锁被占用,只能后期继续尝试再次执行setnx seckill001true#当第一个抢到锁的线程执行完业务之后,便可以删除键,让其他线程能抢到锁del seckill001#其他线程执行如下命令时返回的结果是1,表示拿到了锁setnx seckill001true

其中还存在一个问题:如果第一个线程拿到锁之后还没有执行删除键这个线程就宕机了,就会导致这个锁永久被占用,其他线程的业务将无法执行。对于这个问题,可以通过给键设置过期时间来解决,到达过期时间后让Redis服务自动把这个键删除——释放。

#抢到锁setnx seckill001true#设置过期时间为20毫秒,到期之后则键会被删除掉expire seckill00120#查询键还有多久过期,如果超时则返回负数,如果键被释放掉则返回nilttl seckill001

执行结果如图所示。

赋值和设置过期时间是分为两步的。如果要保证原子性,则可以在设置值的同时再次设置过期时间,效果如图所示。

setseckill001trueex20nx

2、String也能做,为什么还要用Hash

哈希(Hash)在很多编程语言中都有着广泛的应用,在Redis中也是如此。在Redis中,哈希数据类型是指Redis键值对中的值本身又是一个键值对(Key-Value Pair)结构,如图所示。

既然使用String可以实现相同的功能,为何还要使用Hash呢?在Redis官网中会看到优先使用Hash的字眼,主要是基于以下三个因素:内存占用率、时间复杂度和使用的简便性。下面就来了解一下其中的缘由。

假如我们想要存储的一些用户信息如表所示。

使用Redis的String和Hash来存储与处理如上信息的优缺点对比如下:

1)利用String存储用户信息的命令如下:

setuserid:1:name zhangsansetuserid:1:age18
  • 优点:简单直观,每个属性都支持更新的操作。
  • 缺点:占用更多的键,内存占用量较大,同时用户信息分散,一般不会在实际的生产环境中使用。

2)利用String来序列化字符串后的命令如下:

setuser:1 serialize(userInfo)
  • 优点:简化编程,合理使用序列化可以提高内存的使用效率。
  • 缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中,操作比较麻烦。

3)利用Hash存储用户信息的命令如下:

hmset user:1 name clay age18
  • 优点:简单直观,使用合理可以减少内存空间的使用。
  • 缺点:要控制Hash在ZipList和HashTable两种内部编码之间的转换,HashTable会消耗更多内存,在后续的数据类型原理章节会对此做更进一步的分析。

2.1、Hash数据类型的常规操作

如下是Hash数据类型基本的操作命令:

// 给myhash 键设置name=clay的键值对192.168.3.202:1>HSET myhash name"clay"(integer)1// 获取myhash键中键值对name的值192.168.3.202:1>HGET myhash name"clay"

2.2、结合应用场景演练

2.2.1、作为实时数据库

根据Hash特性,可以把Redis作为一个实时数据库来处理。比如在硬件相关的系统中,通常关于硬件的相关参数配置或者一些设备的实时状态都会放在内存中,但是参数过多时维护非常麻烦,而且稍微改动一个参数就会重新发布程序。在有些场景下也采用SQLite来存储这些参数,但是对于高效访问和修改,比如多个主机中的每个主机具有不同的属性,每个属性又有对应的值,Redis是相对比较好的选择。

如下是记录设备的实时状态的命令:

// 设置设备001的当前异常码为1hset device:001code1// 设置设备001的温度为10hset device:001temperature10// 设置设备的两种状态hmset device:001s1starts2 stop// 获取设备001的所有属性hgetall device:001

执行结果如图所示。

下图所示的是设备状态界面化管理工具的展示效果。

2.2.2、使用Hash数据类型实现购物车

很多电商网站一般都会使用Cookie来实现购物车,也就是将整个购物车信息都存入Cookie中。这种做法的一大好处就是无须对数据库进行写入就可以实现购物车功能,而且大大提高了购物车的性能;缺点是程序需要重新解析和验证Cookie,还要保证Cookie的格式正确,并且包含的商品都是可购买的商品。用Cookie实现购物车的缺点是因为浏览器每次发送请求都会连同Cookie一起发送,所以如果购物车Cookie的体积比较大,那么请求发送和处理的速度可能会有所降低,对性能有所影响。

购物车的定义非常简单:以每个用户的userid作为Redis的键(Key),每个用户的购物车都是一个哈希表,它存储了商品ID与商品订购数量之间的映射关系。在商品的订购数量出现变化时,操作Redis哈希对购物车进行更新。

如果用户订购某件商品的数量大于0,那么程序会将这件商品的ID以及用户订购该商品的数量添加到Hash中。

购物车操作命令如下,执行结果如图所示。

// 用户1 商品1 数量1 hset userid:1 pid:11// 返回值0代表该field在哈希表中不存在,为新增的键


如果用户购买的商品已经存在于哈希表中,那么新的订购数量会覆盖已有的订购数量。如下是操作命令,执行结果如图所示。

// 用户1 商品1 hset userid:1 pid:15// 如果用户订购某件商品的数量不大于0,那么程序将从哈希表中删除该项 // 用户1 商品1 hdel userid:1 pid:1

2.2.3、作为计数器

在学习Redis中的String数据类型时,知道了String可以用作计数器,实际上Redis的Hash作为计数器的使用也非常广泛,它常被用于记录网站一天、一月、一年的访问数量。每次访问,在对应的field上自增1即可。

如下是记录博客文章每月访问量的命令,执行结果如图所示。

// 记录博客文章每月的访问量 hincrby myBlog2021021hincrby myBlog2021021hincrby myBlog2021021hincrby myBlog2021021


如下是记录商品的好评数量、差评数量的命令,执行结果如图所示。

// 记录商品的好评数量、差评数量 hincrby pid:1 Good1hincrby pid:1 Good1hincrby pid:1 bad1


如下是记录网站实时在线人数的命令,执行结果如图所示。

// 记录实时在线人数 // 登录 hincrby mySite202102011hincrby mySite202102011// 退出 hincrby mySite20210201-1

3、一种类型多种用法,原来List还可以这样做

从常规角度来看,列表(List)只是一系列有序元素,比如(1, 2, 3, 4, 5)。不过,使用Array实现的List属性与使用链表(Linked List)实现的List属性非常不同。

Redis的List数据类型是通过链表实现的。这意味着即使用户在列表中有数百万个元素,在列表的开头或结尾添加新元素的操作也会在固定时间内执行。使用LPUSH命令将新元素添加到具有100个元素的列表开头的速度与将元素添加到具有1000万个元素的列表开头的速度相同,而且一个列表最多可以包含232-1(4294967295,每个列表超过40亿个元素)个元素。

有优点必然也会存在缺点,那它有什么缺点呢?在使用Array实现的列表中,按索引访问元素的速度非常快,而在通过链表实现的列表中访问速度却不是那么快(其中操作需要的工作量与所访问元素的索引大小成比例)​,具体可以看数据结构原理章节来了解。

如前所述,Redis的List数据类型是通过链表实现的,因为对于数据库系统而言,至关重要的是能够以非常快的方式将元素添加到很长的列表中。我们可以用两种不同的方式来操作列表:第一种是队列,按照先进先出的顺序操作;第二种是栈,按照先进后出的顺序操作。接下来我们来了解一下这两种不同的操作方式。

3.1、List模拟数据结构

List数据类型有以下特点:

  • List(列表)中的元素是有序的,可以通过下标(或称为索引)来获取某个元素或者某个范围内的元素列表。
  • List中的元素可以是重复的。
  • 可以实现顺序排队和插队的操作。

下图为利用List数据类型的数据结构来存储数据的结构图。

3.1.1、先进后出操作

用Redis的List数据类型模拟栈的数据结构实现先进后出的操作,执行命令如下:

// 向栈的左侧插入数据a lpush mystack a // 向栈的左侧插入数据b lpush mystack b // 向栈的左侧同时插入多个数据dd ee ff lpush mystackddee ff // 获取栈中从下标0开始到2结束的数据 lrange mystack02// 获取栈中的所有元素 lrange mystack0-1// 从栈中取出一个值,就是从栈中弹出,按照后进先取出的顺序 lpop mystack

执行结果如图所示。

3.1.2、先进先出操作

用Redis的List数据类型模拟队列的数据结构来实现先进先出的操作,执行命令如下:

// 模拟队列,先进先出 // 向队列中插入第一个元素 lpush myqueue a // 向队列中插入第二个元素 lpush myqueue b // 向队列中插入多个元素 lpush myqueue ccddee // 获取队列中从下标0开始到2结束的数据 lrange mystack02// 查看队列中的所有元素 lrange myqueue0-1// 从队列右边取出第一个元素 rpop myqueue // 查看队列中的所有元素 lrange myqueue0-1
3.1.3、阻塞队列

Redis中的列表还具有一项特殊功能,使其适用于实现队列,通常用作进程间通信系统的构建模块:阻止操作。比如,用户想通过一个流程将数据项推入(或压入)列表,然后使用不同的流程来对这些数据项进行某种加工或处理。这是通常的生产者/消费者模式,可以通过以下的简单方式来实现:

  1. 将数据项推入列表,生产者调用lpush命令。
  2. 从列表中提取数据项,消费者调用rpop。有时列表可能为空,没有任何要处理的数据项,此时rpop就返回null。在这种情况下,消费者被迫等待一段时间,然后使用rpop重试,这称为轮询。不过这样会有一个缺点:强制Redis和客户端处理无用的命令(列表为空时的所有请求将不执行任何实际的操作,只会返回null)​。因为消费处理端在收到null之后会等待一段时间,所以会增加数据项处理的延迟。为了减小延迟,我们可以在两次调用rpop之间等待更少的时间,即对Redis的调用变得越来越无用。

按照先进先出的顺序,如果列表没有元素就等待,执行命令如下:

// 客户端1 // 先向队列中推入两个元素 lpush blockmq a b c // 客户端2 // 通过阻塞的方式获取元素 brpop blockmq10// 如果没有元素则需要等待,直到有元素为止,20代表的是没有元素时需要等待的时间 brpop blockmq20


使用brpop要注意以下几点:

  • 客户端以有序的方式服务:第一个被阻塞而等待列表的客户端在某个元素被其他客户端推送到列表中时首先被服务,以此类推。
  • 返回值与rpop相比有所不同:它是一个包含两个元素的数组(还包含键的名称)​,brpop和blpop通过阻塞方式等待来自多个列表的元素。
  • 如果超时了,则返回null。
3.1.4、模拟消息推送

将上面的阻塞队列稍微扩展,就可以实现消息的推送和消费功能,其架构图如图所示。

3.2、List常规操作

如下是Redis的List常规操作:

// 向keyList集合中写入三个元素192.168.3.202:1>lpush keyList mysql mongodb redis(integer)3// 获取集合keyList中的所有元素192.168.3.202:1>lrange keyList01001)"mysql"2)"mongodb"3)"redis"

3.3、利用列表迅速提升网站首页的并发量

以博客站点为例,当用户和文章都越来越多时,为了加快程序的响应速度,我们可以把用户自己的文章存入列表(List)中,因为列表是有序的结构,所以这样又可以实现分页功能,从而加快程序的响应速度。

每篇文章使用哈希结构存储,例如每篇文章有3个属性:title、timestamp、content。使用列表存储文章内容的执行命令如下:

hmset bbs:1 title title1 timestamp123456789content 这是内容 hmset bbs:2 title title2 timestamp123456789content 这是内容 hmset bbs:3 title title3 timestamp123456789content 这是内容

上面的命令仅存储了文章的详细信息,如果要实现分页,就必须把每篇文章存储在哈希表中的键(Key)存储在列表中,执行命令如下:

lpush user:1:bbs bbs:1 lpush user:1:bbs bbs:2 lpush user:1:bbs bbs:3

分页获取用户文章列表的命令如下:

lrange user:1:bbs09

如果每次分页获取的文章数较多,就需要执行多次hgetall操作,此时可以考虑使用Pipeline批量获取,或者考虑将文章数据序列化为字符串类型,然后使用mget批量获取。

在我们访问任何一个网站时,并发最高的可能就是网站的首页,如果首页是列表类信息,那么完全可以使用List来实现。

  • 把首页中的所有信息都存储在List中,即放在内存中,提高用户访问首页的响应效率。
  • 如果数据量特别大,那么肯定不是一个好方案。可以把标题和当前文章的ID主键存储在Redis的List中,当用户点击当前文章并需要查看详情的时候,结合主键ID再去数据库中查询,这算是一个很不错的方案。

4、开启Set数据类型的正确操作姿势

集合(Set)数据类型是其元素无序且唯一的一种键值对(Key-Value Pair)集合。因为是无序的,所以不会按照元素插入的先后顺序进行存储。

集合类型和列表类型的区别如下:

  • 列表可以存储重复元素,集合默认去重。
  • 列表顺序存储,而集合是无序存储。
  • 列表和集合都支持增、删、改、查,同时集合还支持取多个集合的交集、并集、差集。

4.1、明星绯闻和微博瘫痪的那些事

当出现xxx明星和xxx事件的八卦时,可能会造成服务器压力过大,导致服务器宕机。宕机的大多数原因是很多人同时去关注和查看一条热点新闻或者八卦,而对于热点的过多点赞或者评论会导致并发问题。如果将这些点赞和评论利用缓存来解决,然后不定时地把缓存数据刷到数据库中,那么将能提高性能。

4.2、抽奖逻辑

在我们的生活中,不管是公司年会抽奖还是公众号抽奖)​,大部分都是通过程序把手机号码或者其他信息放在一起,然后随机抽取。在这个场景中,很适合使用Redis的Set数据类型。

重复再抽奖品可以执行如下命令,结果如图所示。

// 首先存入所有参与抽奖的用户信息 sadd luckuser user1 // 当存入重复的用户信息时,存入操作会失败并返回结果0 sadd luckuser user1 // 一次性存储多个用户信息 sadd luckuser user1 user2 user3 user3 user4 user5 // 查看所有参与抽奖人的信息,不会有重复的 smembers luckuser // 随机抽取一个人,抽完不删除信息可以再次抽奖 srandmember luckuser // 随机抽取三个人,抽完不删除信息可以再次抽奖 srandmember luckuser3// 验证抽完之后人员信息是否被删除 smembers luckuser

对于一个用户只可以抽一次奖品的抽奖活动,则可以执行如下命令,执行结果如图所示。

// 清理所有数据flushdb// 一次性存储多个用户信息sadd luckuser user1 user2 user3 user3 user4 user5// 随机抽取一个人,抽完从集合中删除获奖用户的信息,不能重抽spop luckuser1// 随机抽取两个人,抽完之后不能重抽spop luckuser2// 验证抽完之后获奖用户的信息是否被删除smembers luckuser

4.3、文章点赞或者投票

在朋友圈经常收到亲戚朋友发来的链接,请求帮忙点赞和投票。就投票而言,如果根据用户ID或者IP地址来进行限制,那么一个IP地址或者一个用户ID只能针对一个信息投票一次。集合数据类型能够体现出它的特色,即自动去重。对于点赞应用也是一样的。

点赞或者投票操作可执行如下命令,结果如图所示。

// 用户根据ip点赞或者投票sadd like:id1 ip1// 用户根据ip点赞或者投票sadd like:id1 ip2// 用户根据ip点赞或者投票sadd like:id1 ip3// 重复投票和点赞会失败并返回0sadd like:id1 ip3// 取消投票或者点赞srem like:id1 ip3// 查看点赞或者投票人的信息smembers like:id1// 查看所有的投票数或者点赞数量

4.4、共同好友统计

利用Redis中的Set类型可以模拟此功能,执行如下命令的结果如图所示。

// 给张三同学加好友 friendasadd zhangsan frienda// 给张三同学加好友 friendbsadd zhangsan friendb// 给张三同学加好友 friendcsadd zhangsan friendc// 给张三同学加好友 frienddsadd zhangsan friendd// 给李四同学加好友 friendasadd lisi frienda// 给李四同学加好友 friendbsadd lisi friendb// 给李四同学加好友 friendesadd lisi friende// 给李四同学加好友 friendfsadd lisi friendf

可以利用如下命令模拟张三和李四的共同好友操作,结果如图所示。

// 张三和李四共同的好友(两个集合的交集)sinter zhangsan lisi// 张三可能认识李四的好友sdiff lisi zhangsan// 李四可能认识张三的好友sdiff zhangsan lisi// 张三和李四两个人的总好友(两个集合的并集)sunion zhangsan lisi

5、微服务的限流操作和Zset的那些瓜葛

有序集(Zset)类型是Redis中一个非常重要的数据类型,类似于集合类型和哈希类型之间的混合类型。像Set集合一样,Zset有序集由唯一、非重复的一组元素所组成,从某种意义上说也是一个集合。Zset类型的数据结构如图所示。


虽然Zset内的元素没有排序,但是排序后集合中的所有元素都与一个称为得分的浮点值相关联(这就是Zset类型类似于哈希类型的原因,因为每个元素都映射到一个值)​。

微服务(架构图如图所示)日益流行,缓存、降级和限流是保护微服务系统运行稳定性的三大利器。缓存的目的是提升系统访问速度和增大系统处理的容量,而降级是当服务出现问题或者影响到核心流程的性能时需要暂时被屏蔽掉,待高峰或者问题解决后再打开。有些场景并不能用缓存和降级来解决,比如稀缺资源、数据库的写操作、频繁的复杂查询,因此需有一种手段来限制这些场景的请求量,这就是限流。Redis的Zset类型的结构可以实现限流功能。

5.1、利用Zset实现限流

限流是对系统的出入流量进行控制,防止大流量出入,从而导致资源不足、系统不稳定。限流的目的应当是通过对并发访问和请求进行限速或者对一段时间内的请求进行限速来保护系统。一旦达到限制速率就可以拒绝服务或者让请求的服务等待。

假设我们上线一个服务,而这个服务提供的最大处理能力是1秒服务2000个QPS,遇到高于2000的QPS该如何解决呢?如果不对其限流,后果可想而知。

下面了解一下两种基本的限流算法。

5.1.1、滑动窗口限流方式

固定窗口限流的效果如图所示。前两个窗口和后两个窗口分别表示的是第一秒和第二秒,它们的长度相同,表示每一秒接收的请求数量相同,达到了限流的效果。这种固定窗口限流会出现一些问题。假设限流设置为1秒2000个请求,而在第一秒的最后100毫秒以及第二秒开始的100毫秒都收到2000次请求,就等于在200毫秒的周期中收到4000次请求,并且限流通过。这样就出现了两倍的配置速率问题。

滑动窗口为固定窗口的改进版,如图所示。

在每个时间片段接收到的请求数量都是相等的,或者不会超过限制数量。接下来我们看一下使用Zset应该如何处理。

  1. 首先利用Zset类型在value处存放随机字符,而在score部分存储时间戳。
  2. 每一次请求进来的时候获取限流key值数量,根据当前时间戳和当前时间减去前一秒的时间戳。这样每次获取的都是一秒内的总请求数量。
  3. 如果没有超过限制,继续处理;如果超过,则等待或者直接返回。

使用Zset实现限流的数据结构图如图所示。


实现代码如下(以C#代码为例)​:

// / <summary> // / 滑动窗口限流,利用Redis的Zset类型 // / </summary> // / <returns></returns> public bool LimitFlow() { DateTime currentTime = DateTime.Now; using (IRedisClient client = redisService.GetRedisClient()) { // 判断是否存在响应的键 if (client.ContainsKey("服务名称")) { // 获取前一秒到当前秒时间内的集合总数量 // 此数量表示的就是请求数量 int count = client.GetRangeFromSortedSetByLowestScore("服务名称", currentTime.AddSeconds(-1).ToFileTimeUtc(), currentTime.ToFileTimeUtc()).Count; // 8表示限流数量 // 如果请求数量大于限流数量,则返回false,可以拒绝服务请求 if (count != null&&count >8) { return false; } } // 如果是第一次获取请求数量,或者满足限流限制, // 则返回true,然后给Zset中添加一次请求数量,score部分存储的就是当前的时间戳 client.AddItemToSortedSet("服务名称", Guid.NewGuid().ToString(), currentTime.ToFileTimeUtc()); return true; } }

当请求数量超过限流数量时,则拒绝业务处理(限流数量为8)​,结果如图所示。

5.1.2、令牌桶

令牌桶算法是和漏桶算法效果一样但方向相反的算法,且更加容易理解。随着时间的流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10毫秒)往桶里加入令牌,如果桶已经满了就不再加了。新请求来临时,会各自拿走一块令牌,如果没有令牌可拿就阻塞或者拒绝服务。令牌桶操作流程示意如图所示。


实现代码如下:

// / <summary> // / 利用Redis中的List类型定时生成令牌 // / </summary> public void SetIntervalTimeTask() { // 定时调用此方法,往key=limit_list的List类型中插入随机值 using (IRedisClient client = redisService.GetRedisClient()) { // 往阻塞列表压入数据 client.PushItemToList("limit_list", Guid.NewGuid().ToString()); } } // / <summary> // / 获取令牌 // / </summary> // / <returns></returns> public bool GetToken() { using (IRedisClient client = redisService.GetRedisClient()) { // 从阻塞列表弹出键 var token = client.PopItemFromList("limit_list"); // 返回结果是空,表示令牌桶中的令牌已经用完,否则表示拿到了令牌 if (!string.IsNullOrWhiteSpace(token)) { return true; } return false; } }

5.2、新闻排行榜场景实战

新闻排行榜效果如图所示。

百度热榜可能是我们经常看到的,针对这样数据量大且实时性高的排名,用传统的数据库来实现性能太差。下面我们使用Redis中的Zset数据类型来模拟一下。

// 清空当前数据库flushdb// 以关注度分值将新闻id是001的数据存储到集合中zadd news601newid001// 以关注度分值将新闻id是002的数据存储到集合中zadd news605newid002// 以关注度分值将新闻id是003的数据存储到集合中zadd news505newid003// 根据关注度的递增获取热榜新闻zrange news010withscores// 对新闻001点赞量加3zincrby news3newid001

5.3、直播打赏排名场景实战

当下直播平台都会给粉丝主播提供打赏功能,并且打赏金额越多,粉丝排位越靠前。这样的功能也是在数据实时性高、大数据和高并发下的场景中实现的,存在一个时间段内大量粉丝打赏和打赏排名更新的情况。为了提高性能和实时性,用Redis实现是一个很不错的选择,下面我们来模拟一下该场景。

模拟直播打赏排名场景需要执行如下命令,执行结果如图所示。

// 清空当前数据库flushdb// 以粉丝打赏的金额作为元素分值存入集合中zadd anchor10fans1 zadd anchor20fans2 zadd anchor200fans3// 按照粉丝的打赏金额排名顺序输出粉丝信息zrange anchor0-1// 按照粉丝的打赏金额排名倒序输出粉丝信息zrevrange anchor0-1// 粉丝再次打赏金额叠加zincrby anchor100fans1// 粉丝信息和金额全部输出zrange anchor0-1withscores

http://www.jsqmd.com/news/1151018/

相关文章:

  • 高精度ADC与STM32L4在工业测量中的优化设计
  • python findwindowex Python findwindowex抓不到窗格?这招让隐藏文本瞬间裸奔
  • 计算机毕业设计之民办高校二手物品交易网的设计与实现
  • LPRNet + YOLOv8 车牌识别系统部署:PyQt5界面与SQLite数据库集成实战
  • 5分钟免费解锁Cursor Pro完整功能:终极机器ID重置指南
  • 视频理解 Pipeline:抽帧策略比模型结构更影响结论
  • 如何在3分钟内实现专业级直播背景替换:OBS背景移除插件终极指南
  • Redis 5.x ——数据持久化(RDB AOF)
  • 构建高效消息监控系统:基于动态注入的实时消息拦截方案
  • 如何将微信聊天记录变成珍贵数字记忆:WeChatMsg实用指南
  • 2026AI问答优化服务商有哪些,从监测到优化全链路谁更完整
  • 终极指南:5步掌握MATLAB数字图像相关分析工具Ncorr
  • 工业负载控制方案:TPD2017FN与MKV44F128VLH16应用解析
  • 计算机大数据毕设实战-基于 Django 框架的淘宝数码产品数据可视化系统的设计与实现 基于多维大数据的淘宝电子产品销量研判系统【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • Linux 内核的 Context Tracking上下文跟踪
  • 双节锂电池主动均衡系统设计与STM32实现
  • 03.01.02.ComfyUI Ubuntu:环境搭建篇(集成 AnythingLLM调用ComfyUI的API 使用Custom Skills方式)
  • 终极Windows更新修复指南:3分钟快速解决更新卡顿问题
  • 如何快速集成移动端PDF预览:pdfh5.js的完整解决方案
  • Vin象棋:三步快速掌握AI象棋分析,免费享受专业级棋局助手
  • 国家中小学智慧教育平台电子课本下载器:如何轻松获取官方教材PDF的完整指南
  • 暗黑破坏神2存档编辑器完整指南:3步实现游戏数据自由修改
  • GPT-4 Turbo 128K 上下文实战:5步构建长文档智能问答系统
  • 如何零基础玩转XUnity.AutoTranslator:让外语游戏秒变中文的终极指南
  • [实战] 2026年工程图纸GDT形位公差识别与数字化检验计划(FAI/PPAP)构建指南
  • 5个高效实用的Stardew Valley模组:让你的农场管理变得轻松简单
  • 5分钟完全解锁索尼相机隐藏功能:OpenMemories-Tweak终极指南
  • VASPsol隐式溶剂模型:5分钟快速上手DFT溶剂化计算的终极指南
  • 数据库规范化理论实战:从1NF到BCNF的5个经典模式分解案例
  • Sublime Text轻量编辑器原理与实战配置指南