vuex存储什么数据_Redis除了存储数据以外还能做什么?

8854f1f4756fc68ba8eb94b8ce6691da.png

作者:阿茂

前五篇文章我带大家了解了Redis的一些基础知识与架构原理性到的东西,这一篇我们来讨论下Redis除了当做存储服务以外还能做些什么,这也是出去面试会被经常问到的问题。下面我们来了解一下一些常用的应用:

分布式锁

基本上出去面试都会被提及到Redis分布式锁的应用,那么它是怎么实现的呢?分布式系统经常会遇到处理并发的问题,当一个资源有多个线程统一时间读写时,要保证最终数据的正确性,说白了就是要保证操作的原子性。我们总体的原则就是:当别的线程操作共享资源时,发现已经有人声明说这个资源我正在使用且没有用完的时候,此线程只能等待或者放弃。在Redis里面我们一般会使用setnx对资源加锁,当资源使用完毕,发送一条del指令释放锁,方便别的线程使用此资源。说起来好像就这么一句话,但是在实际使用中存在着种种要考虑的问题,比如应用处理逻辑出现异常并未执行del命令,这样就会陷入死锁永远将资源无法释放。在早期的2.8以前版本setnx与expire是两条指令而不是一个原子操作,服务器出现意外就会造成频繁的死锁,那么这时候有同学就会想到用事务来保证setnx与expire指令的原子性,但是expire是依赖于setnx的返回结果的,假如setnx并未抢占到资源锁,expire该不该执行呢?事务的特性就是要么都执行要么都不执行。当2.8以后引入setnx的超时时间参数以后就方便多了,那么通过一条新引入 的指令这样就彻底解决了Redis分布式锁了?那说明你还没考虑到各种问题:

  • 分布式超时:Redis的分布式锁不能解决超时问题,如果在加锁与释放的处理逻辑或者网络抖动造成的耗时大于了锁超时时间,那么这时候资源将不受当前线程的控制,第二个线程就会重新持有了此资源的锁,当第一个线程再将处理结果更新时,此时资源版本与它持有的资源版本已经不一致了,但是线程一还认为这是自己持有的锁将其删除,就会导致马上有第三个线程进入持有锁,这样循环错误下去。为了避免这个问题,我们尽量在设计锁的时候考虑到线程正常处理耗时(排除网络抖动造成的)与锁超时时间的合理值,当偶发的网络抖动造成的超时,我们可以给value设置一个唯一随机ID值(例如:客户端标识+时间戳+随机值),释放锁时先查看是否跟客户端持有的唯一id一致,再删除锁。一般情况下我们可以在客户端做逻辑处理,当发出del命令时候发现跟自己持有的随机数不一致时就认为加锁操作失败,回滚当前操作,再从新尝试获取锁。但是这又存在一个问题,比较操作跟删除操作又不是一个原子操作,这就需要我们通过Redis的Lua脚本来定义一个原子操作,因为Lua脚本的操作可以保证原子性(关于Lua脚本的使用我们这里就不展开解释了,大家可以自行查阅与学习)。
  • 可重入问题:要支持同一个线程的多次加锁,我们需要重写客户端的set方法,例如Java中的ReentrantLock,使用线程Threadlocal将加锁次数保存起来,具体实现可以见JKD的ReentrantLock的实现
  • 分布式节点切换数据延迟问题:在分布式集群环境中经常会遇到主从切换,假如从节点转化为主节点时锁信息还未同步到新的主节点的时候或者在极端情况下锁信息丢失,又会导致资源使用混乱的问题。虽然这是很少见的情况但是为了保证4个9的可用性我们还是需要考虑的。为了解决这个问题我们需要了解下Redlock算法。使用 Redlock,需要提供多个Redis实例,这些实例之前相互独立没有主从关系,跟市面上分布式架构一样,都采用大多数原则,加锁时会向大多数节点发送set(key, value, nx=True, ex=xxx)指令,成功后就认为加锁成功,释放锁时需要向全部节点发送del指令。因为牵扯到多个节点的交互所以这个锁的代价还是很大的。这个算法也并不是完美的,在使用时还是要考虑到系统的有损容忍度,具体Redlock算法大家可以参考如下文章:
How to do distributed locking:' http:// martin.kleppmann.com/20 16/02/08/how-to-do-distributed-locking.html '

事务

我们可以通过Redis的特性来实现一个简单的事务,只要是事务都要满足acid特性,但是在使用Redis事务的时候要注意一些问题。Redis收到一系列事务指令的时候不是马上执行的,它会缓存在Redis的事务队列里面,等待exec指令的到来才开始执行整个事务,因为Redis是单线程的在队列不用担心指令顺序混乱。从我们上面的描述大家应该就能发现问题,真正的事务要么是一起执行,要么一起失败,但是在Redis里面从队列里面出来事务指令会一个个执行,如果一个执行出现异常,那么Redis并没有提供类似Mysql那样的undoLog的机制让事物回滚,仅仅只提供了discard丢弃指令,用于丢弃事务队列里面的事务(在exec之前)。所以它就不含有原子性的特性。只能保证可靠到的隔离性(当前事务不被其他事务打断)。我们可以通过客户端指令合并尽量保证指令的原子性,也就是我们之前所说的管道概念(pipeline),将多次IO压缩成一次。当然我们可以使用另外一种变通的使用方式:Redis的watch机制,在事务开启之前监听某些关键变量,当事务执行时(执行exec指令)比较自己持有的资源版本是否自监听以来有变化,如果有变化那么就执行失败discard丢弃指令并返回客户端,让客户端重试,否则执行。以上都是一些弥补办法,要想使用完整且强大的事务特性还得寻求别的中间件。当然了解这一特性有助于设计出更好的系统。

消息队列

Redis的消息队列也如同它的事务功能,不是专业的消息中间件,并没有像kafka,Rabbitmq这些消息队列那么强大,也不没有ack机制,但是要你只想要一个简单的消息队列,不需要那么多附带功能,那么它就是你的首选。Redis的消息队列是使用list数据结构来实现的,list数据结构它本身就一个链表有着极强的顺序性。可以使用rpush/lpush入队, lpop/rpop出队。如果队列空了我们会让客户端会陷入pop死循环或者sleep后再pop这样的循环中,这样明显的问题就是,当sleep时间设置过长,会导致消息到来时延迟,当sleep时间过短的话又会导致服务端压力过大。这种情况下最好使用blpop/brpop,它们是指使用blocking(阻塞读),在队列里没有数据时候处于blocking状态,当数据到来线程马上就会进入处理数据。需要注意的是当使用blpop/brpop的时候,长时间处于空闲状态服务端会主动断开连接,在客户端需要做个异常处理,再重试。除了以上使用list数据结构还可以使用zset数据结构实现简单的消息队列,把消息内容序列化成字符串存储在value中。它唯一的特点就是可以让消息带有优先级,比如可以实现一个设置一个优先字段作为score,然后客户端优先处理score高的。还有就是可以实现一个延迟队列,执行时间作为score,比如用户下单后2小时发送订单状态消息这样的需求。

位图

这种数据结构一般作为优化数据量很有效果的,比如什么签到,报名,投票这样的统计类的功能,你如果使用数据既要存储用户和对应的操作以及时间等其他的信息,当用户量巨大的的时候,将给你带来很大的烦恼。Redis的位图结构我们在之前的数据结构篇有讲过,这里我们在提一下,它是一个byte数组,我们可以使用get/set直接设置跟读取整个位图的值,也可以使用位图操作getbit/setbit等将byte数组当做位数组来处理。Redis的位数组是自动扩展的,如果设置了阈值且当前超出了阈值会自动将数组进行扩充,还可以使用bitcount实现快速统计,使用位图查找指令bitpos查找某范围的位图值,两个指令也可以组合使用对某个范围的位图值进行统计。比如查找一个用户一年中每周末签到数总和。

漏斗限流

漏斗限流是最常用的限流方法之一,漏斗的剩余空间代表着当前请求最大可接受数量,漏斗的流速代表系统最大限流数量。Funnel对象的make_space方法是漏斗算法的核心,每次有请求进入漏斗前都会被调用请求出漏斗,给漏斗腾出接受新请求的空间。Funnel对象的内容按字段存储在一个hash结构中,有请求进入时会将hash结构的字段拿出来计算,将新的值回填到hash结构中就完成一次行为检测。Redis 4.0提供了一个限流模块叫redis-cell,并提供了原子限流指令。该模块只有 1 条指令 cl.throttle,它的参数和返回值都略显复杂,接下来让我们来看看这个指令具体该如何使用。

cl.throttle key 15 30 60 #key:操作key , 15:漏洞容量 ,30:时间窗,60:请求次数
1.)(integer) 0 # 0 表示允许,1 表示拒绝
2.)(integer) 15 # 漏斗容量 capacity
3.)(integer) 14 # 漏斗剩余空间 left_quota
4.)(integer) -1 # 如果拒绝了,需要多长时间后再试(漏斗有空间了,单位秒)
5.)(integer) 2 # 多长时间后,漏斗完全空出来(left_quota==capacity,单位秒)

上面指令的意思就是运行某个key的请求在30秒内最大请求60次。漏洞的初始容量为15,就是说着15堆积请求处理完了才开始触发限流,你可以0.1秒处理完这15个请求都不会触发限流。等处理完初始容量请求后就会触发限流这个指令的速率为:30/60,也就是只允许0.5秒一个请求的速率处理,大于这个速率的会堆积到漏斗容量中,直到容量被打满。客户端要么重试要么丢弃,你可以可以上面指令的返回参数4或者5来确定sleep时间后重试。

附近的好友

Redis 在 3.2 版本以后增加了地理位置GEO模块,他是用的是GeoHash算法。关于这个算法我只是简单的提一下,GeoHash 算法将二维的经纬度数据映射到一维的整数,这样所有的元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。当我们想要计算「附近的人时」,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行了。那这个映射算法具体是怎样的呢?它将整个地球看成一个二维平面,然后划分成了一系列正方形的方格,就好比围棋棋盘。所有的地图元素坐标都将放置于唯一的方格中。方格越小,坐标越精确。然后对这些方格进行整数编码,越是靠近的方格编码越是接近。那如何编码呢?一个最简单的方案就是切蛋糕法。设想一个正方形的蛋糕摆在你面前,二刀下去均分分成四块小正方形,这四个小正方形可以分别标记为 00,01,10,11 四个二进制整数。然后对 每一个小正方形继续用二刀法切割一下,这时每个小小正方形就可以使用 4bit 的二进制整数予以表示。然后继续切下去,正方形就会越来越小,二进制整数也会越来越长,精确度就会越来越高。上面的例子中使用的是二刀法,真实算法中还会有很多其它刀法,最终编码出来的整数数字也都不一样。编码之后,每个地图元素的坐标都将变成一个整数,通过这个整数可以还原出元素的坐标,整数越长,还原出来的坐标值的损失程度就越小。对于「附近的人」这个功能而言,损失的一点精确度可以忽略不计。GeoHash 算法会继续对这个整数做一次 base32 编码 (0-9,a-z 去掉 a,i,l,o 四个字母) 变一个字符串。在 Redis 里面,经纬度使用52位的整数进行编码,放进了zset里面,zset元素 key,score是GeoHash的52 位整数值。zset的score虽然是浮点数,52 位的整数值,它可以无损存储。在使用 Redis 进行 Geo 查询时,我们要时刻想到它的内部结构实际上只是一个zset(skiplist)。通过 zset 的 score 排序就可以得到坐标附近的其它元素 (实际情况要复杂一些,不过这样理解足够了),通过将 score 还原成坐标值就可以得到元素的原始坐标。

结束

这一节我们大致讲了一些Redis常用的玩法,像布隆过滤器和HyperLogLog我上几篇文章都已经说过了,这里就不在复述了。要是喜欢的话分享给你身边的小伙伴,谢谢。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值