首页 > 编程知识 正文

redis集群三种方式,redis list应用场景

时间:2023-05-05 17:05:56 阅读:26018 作者:1062

我最近在写一个篮球社区项目。 其中使用redis的场景还很多,请结合项目总结使用redis的场景

一、缓存项目场景:用户登录或注册时验证码的保存、用户名

setcode :1: code 1232 ex 100n x

好了

获取代码:1:代码

“1232”

setuser :1: namebobex 100n x

好了

get User:1:name

鲍勃

缓存是redis镜像率最高的使用场景,只能通过set/get实现,但也有几点需要考虑

1、缓存设置方法

2、如何保持缓存和上游数据的完整性

3、如何解决缓存雪崩、缓存崩溃问题(这两个问题分开写)。

二、消息队列推式用户队列123 4

lpop用户邮件队列

rpop用户邮件队列

1

rpop用户邮件队列

2

redis的队列可以看作分布式队列,作为消息队列时,生产者会填充数据。 虽然消费者又取出了一个数据:(Lpush/RPOP、rpush/lpop ),但存在一些不足,这些不足可以是致命的,但是对于即便去掉几条消息也没有关系的场景可以考虑

1、没有消息确认机制(ack ),可能会丢失消息

2、redis需要持续配置

在redis中实现消息队列

(三)过滤器) dupefilter ) sadd UrlSet http://1

(集成器) 1

sadd UrlSet http://2

(集成器) 1

sadd UrlSet http://2

(集成器) 0

smembers UrlSet

“http://1”“http://2”

scrapy-redis作为分布式爬虫的框架,使用redis的Set这一数据结构对攀登的url进行再处理。 def request _ seen (自, 请求) 3360 ' ' returnstrueifrequestwasalreadyseen.parameters---request 3360 scrapy.http.request returns---- request 3360 self.key,fp ) return added==0,但url过多会导致内存消耗过多

四.分布式锁定分布式锁定,这是KV缓存以外最常用的另一个特征功能。

set lock : user :1008606 be 97 fc-f258-4202-b60b-8d 5412 DD 5605 ex 60 NX发行锁定,LUA脚本ifredis.call('get ' ), kys [1]==argv [1] thenreturnredis.call (' del,KEYS[1] ) else return 0end这是最简单的独立版分布式锁定,具有以下要点

1 ) EX表示锁定过期并释放

2 ) NX保证原子性

比较解除锁定时启用资源时发生的UUID,避免误解锁定

lhdkj使用分布式锁定是为了解决性能问题(如防止分布式调度任务多次运行),并且考虑到单个redis锁定的可能性很低,可以使用此独立版本的分布式锁定举个例子说明:

例如,能干的资深工程师开发效率高,代码质量也高,是团队的明星。 所以,很多产品经理都要给他添麻烦,让他做自己的需求。 如果同一时间有很多产品经理来找他,那个想法就会陷入混乱。 再好的程序员,大脑的同时性也无处不在。 所以,他给自己办公室的门把手挂上了不打扰的品牌。 产品经理来的时候,你可以先看看门把手上有没有这个品牌,如果没有,你可以进工程师谈谈需求。 说话前挂个牌子,说完后摘下牌子。 这样,当其他产品经理也来找他麻烦时,如果你看到这个品牌挂在那里,你可以选择睡觉等待,或者先去做别的事情。 如果这位明星工程师得到了平静。

必须是设置这个过期时间。 由于遇到地震(过程被kill -9,机器停机)等特殊情况,产品经理可能会选择从窗户跳下去。 没有机会摘牌,造成死锁饥饿,这位优秀的工程师成了大闲人,严重的资源浪费。 此外,还必须注意此owner_id。 这表示锁定由谁指定的——产品经理的工作号码。 以防你的钥匙不小心被别人摘下来。 解除锁定时,与此owner_id一致,匹配成功时解除锁定。 此owner_id通常为

是一个随机数,存放在 ThreadLocal 变量里(栈变量)。官方其实并不推荐这种方式,因为它在集群模式下会产生锁丢失的问题 —— 在主从发生切换的时候。官方推荐的分布式锁叫 RedLock,作者认为这个算法较为安全,推荐我们使用。不过我们一直还使用上面最简单的分布式锁。为什么我们不去使用 RedLock 呢,因为它的运维成本会高一些,需要 3 台以上独立的 Redis 实例,用起来要繁琐一些。另外,Redis 集群发生主从切换的概率也并不高,即使发生了主从切换出现锁丢失的概率也很低,因为主从切换往往都有一个过程,这个过程的时间通常会超过锁的过期时间,也就不会发生锁的异常丢失。还有呢就是分布式锁遇到锁冲突的机会也不多,这正如一个公司里明星程序员也比较有限一样,总是遇到锁排队那说明结构上需要优化。

五:定时任务

分布式定时任务有多种实现方式,最常见的一种是 master-workers 模型。
master 负责管理时间,到点了就将任务消息仍到消息中间件里,然后worker们负责监听这些消息队列来消费消息。
著名的 Python 定时任务框架 Celery 就是这么干的。但是 Celery 有一个问题,那就是 master 是单点的,如果这个 master 挂了,整个定时任务系统就停止工作了。

另一种实现方式是 multi-master 模型。这个模型什么意思呢,就类似于 Java 里面的 Quartz 框架,采用数据库锁来控制任务并发。
会有多个进程,每个进程都会管理时间,时间到了就使用数据库锁来争抢任务执行权,抢到的进程就获得了任务执行的机会,然后就开始执行任务,这样就解决了 master 的单点问题。

这种模型有一个缺点,那就是会造成竞争浪费问题,不过通常大多数业务系统的定时任务并没有那么多,所以这种竞争浪费并不严重。
还有一个问题它依赖于分布式机器时间的一致性,如果多个机器上时间不一致就会造成任务被多次执行,这可以通过增加数据库锁的时间来缓解。

现在有了 Redis 分布式锁,那么我们就可以在 Redis 之上实现一个简单的定时任务框架。

#注册定时任务hset tasks name trigger_rule#获取定时任务列表hgetall tasks# 争抢任务set lock:$(name) true nx ex=5# 任务列表变空# 轮询版本号,有变化就重新加载任务列表,重新调度时间有变化的任务set tasks_version $new_versionget tasks_version 六、频率控制

项目的社区功能里,不可避免的总是会遇到垃圾内容,一觉醒来你会发现首页突然会被某些恶意的帖子和广告刷屏了,如果不采取适当的机制来控制就会导致用户体验受到严重的影响
控制广告垃圾贴的策略很多,高级一点的可以通过AI,最简单的方式是通过关键词扫描,还有比较常用的一种方式是频率控制,限制单个用户内容的生产速度,不通等级的用户会有不同的频率控制参数
频率控制就可以使用redis来实现,我们将用户的行为理解为一个时间序列,我们要保证在一定的时间内限制单个用户的时间序列的长度,超过这个长度就禁止用户的行为,它可以是用redis的zset(有序集合,zset详解)来实现

图中绿色的部门就是我们要保留的一个时间段的时间序列信息,灰色的段会被砍掉。统计绿色段中时间序列记录的个数就知道是否超过了频率的阈值。

下面的代码控制用户的ugc行为为每小时最对N次hist_key:"ugc:${user_id}"with redis.pipeline() as pipe:# 记录当前的行为pipe.zadd(hist_key,ts,uuid)#保留1小时内的行为序列pipe.zremrangebyscore(hist_key, 0, now_ts -3600)# 获取这1小时的行为数量pipe.zcard(hist_key)# 设置过期时间,节约内存pipe.expire(hist_key, 3600)# 批量执行_ , _ , count, _ =pipe.exec()return count > N 七、服务发现

如果想要技术成熟度再高一些,有的企业会有服务发现的基础设施。通常我们都会选用zookeeper、etcd,consul等分布式配置数据库来作为服务列表的存储
它们有非常及时的通知机制来通知服务消费者服务列表发生了变更。那我们该如何使用 Redis 来做服务发现呢?

这里我们要再次使用 zset 数据结构,我们使用 zset 来保存单个服务列表。多个服务列表就使用多个 zset 来存储。
zset 的 value 和 score 分别存储服务的地址和心跳的时间。服务提供者需要使用心跳来汇报自己的存活,每隔几秒调用一次 zadd。服务提供者停止服务时,使用 zrem 来移除自己。

zadd service_key heartbeat_ts addrzrem service_key addr

这样还不够,因为服务有可能是异常终止,根本没机会执行钩子,所以需要使用一个额外的线程来清理服务列表中的过期项

zremrangebyscore service_key 0 now_ts -30 # 30s都没来心跳

接下来还有一个重要的问题是如何通知消费者服务列表发生了变更,这里我们同样使用版本号轮询机制,当服务列表变更时,递增版本号。消费者通过轮询版本号的变化来重加载服务列表

if zadd() >0 || zrem() >0 ||zremrangebuscore() >0:incr service_version_key

但是还有一个问题,如果消费者依赖了很多的服务列表,那么它就需要轮询很多的版本号,这样的IO效率会比较低下。

这是我们可以再增加一个全局版本号,在任意的服务类表版本号发生变化时,递增全局版本号
这样在正常情况下消费者只需要轮询全局版本号就可以了。当全局版本号发生变更时再挨个对依赖的服务类表的子版本号,然后加载有变更的服务列表

八、位图

项目里需要做一个球队成员的签到系统,当用户量比较少的时候,设计上比较简单,就是将用户的签到状态用redis的hash结构来存储,签到一次就再hash结构里记录一条,签到有三种状态:未签到,已签到和部签到,分别是0,1,2三个整数值

hset sign:$(user_id) 2019-08-11 1hset sign:$(user_id) 2019-08-12 0hset sign:$(user_id) 2019-08-14 2

这个其实非常浪费用户空间,后来想做全部用户的签到,技术leader指出,这时候的再用hash就有问题了,他讲到当用户过千万的时候,内存可能会飚到 30G+,我们线上实例通常过了 20G 就开始报警,30G 已经属于严重超标了。
这时候我们就开始着手解决这个问题,去优化存储。我们选择使用位图来记录签到信息,一个签到状态需要两个位来记录,一个月的存储空间只需要 8 个字节。这样就可以使用一个很短的字符串来存储用户一个月的签到记录。


但是位图也有一个缺点,它的底层是字符串,字符串是连续存储空间,位图会自动扩展,比如一个很大的位图 8m 个位,只有最后一个位是 1,其它位都是零,这也会占用1m 的存储空间,这样的浪费非常严重。
所以呢就有了咆哮位图这个数据结构,它对大位图进行了分段存储,全位零的段可以不用存。
另外还对每个段设计了稀疏存储结构,如果这个段上置 1 的位不多,可以只存储它们的偏移量整数。这样位图的存储空间就得到了非常显著的压缩。

九、 模糊计数

上面提到的签到系统,如果产品经理需要知道这个签到的日活月活怎么办呢?
通常我们会直接甩锅——请找数据部门。
但是数据部门的数据往往不是很实时,经常前一天的数据需要第二天才能跑出来,离线计算是通常是定时的一天一次。那如何实现一个实时的活跃计数?
最简单的方案就是在 Redis 里面维护一个 set 集合,来一个用户,就 sadd 一下,最终集合的大小就是我们需要的 UV 数字。
但是这个空间浪费严重怎么办?这时候就需要使用redis提供的HyperLogLog模糊计数功能,它是一种概率计数,有一定的误差,大约是0.81%。
但是空间占用很小,其底层是一个位图,它最多只会占用12k的存储空间,而且在计数值比较小的时候,位图使用稀疏存储,空间占用就更小了。

#记录用户pfadd sign_uv_${day} user_id#获取记录数量p count sign_uv_${day}

微信公众号文章的阅读数可以使用它,网页的 UV 统计它都可以完成。但是如果产品经理非常在乎数字的准确性,比如某个统计需求和金钱直接挂钩,那么你可以考虑一下前面提到的咆哮位图。

它使用起来会复杂一些,需要提前将用户 ID 进行整数序列化。Redis 没有原生提供咆哮位图的功能,但是有一个开源的 Redis Module 可以拿来即用。

十、布隆过滤器

如果系统即将会有大量的新用户涌入时,布隆过滤器就会非常有价值,可以显著降低缓存的穿透率,降低数据库的压力
这个新用户的涌入不一定是业务系统的大规模铺开,也可能是因为来自外部的缓存穿透攻击

def get_user_state(user_id):state = cache.get(user_id)if not state:state = db.get(user_id) or{}cache.set(user_id,state)return statedef save_user_state(user_id,state):cache.set(user_id,state)db.set_async(user_id,state)

比如就上面这个业务系统的用户状态查询接口代码,现在一个新用户过来,会先去缓存里查询有没有这个用户的状态数据
因为是新用户,所以肯定缓存里没有,然后它就要去数据库里查,结过数据库也没有,如果这样的新用户大批量瞬间涌入,那么可以遇见数据库的压力会比较大,会存在大量的空查询
我们非常希望redis里面有这样一个set,它存放了所有的用户id,这样通过查询这个set集合就知道是不是新用户来了
当用户量非常庞大的时候,维护这样的一个集合需要的存储空间是很大的
这时候就可以使用布隆过滤器,它相当于一个set,但是又不同于set,它需要的存储空间要小的多
比如存储一个用户id需要64个字节,而布隆过滤器存储一个用户ID只需要1个字节多点,其实它存的不是用户id,而是用户id的指纹,所以会存在一定的小概率误判,它是一个具备模糊过滤能力的容器

当它说用户 id 不在容器中时,那么就肯定不在。当它说用户 id 在容器里时,99% 的概率下它是正确的,还有 1% 的概率它产生了误判。

不过在这个案例中,这个误判并不会产生问题,误判的代价只是缓存穿透而已。
相当于有 1% 的新用户没有得到布隆过滤器的保护直接穿透到数据库查询,而剩下的 99% 的新用户都可以被布隆过滤器有效的挡住,避免了缓存穿透

布隆过滤器的原理有一个很好的比喻,那就是在冬天一片白雪覆盖的地面上,如果你从上面走过,就会留下你的脚印。如果地面上有你的脚印,那么就可以大概率断定你来过这个地方,但是也不一定,也许别人的鞋正好和你穿的一模一样。可是如果地面上没有你的脚印,那么就可以 100% 断定你没来过这个地方

说明:该文写到一半时,偶然看到了下面简书作者的这篇文章,原文写的很好,跟我的项目中的一些业务逻辑很吻合,故将其引用过来,如有疑惑,请去原文阅读Java老王

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。