第一:Redis 是什么?
Redis是基于内存、可持久化的日志型、Key-Value数据库 高性能存储系统,并提供多种语言的API
第二:出现背景
数据结构(Data Structure)需求越来越多, 但memcache中没有, 影响开发效率
性能需求, 随着读 *** 作的量的上升需要解决,经历的过程有:
数据库读写分离(M/S)–>数据库使用多个Slave–>增加Cache (memcache)–>转到Redis
解决写的问题:
水平拆分,对表的拆分,将有的用户放在这个表,有的用户放在另外一个表;
可靠性需求
Cache的"雪崩"问题让人纠结
Cache面临着快速恢复的挑战
开发成本需求
Cache和DB的一致性维护成本越来越高(先清理DB, 再清理缓存, 不行啊, 太慢了!)
开发需要跟上不断涌入的产品需求
硬件成本最贵的就是数据库层面的机器,基本上比前端的机器要贵几倍,主要是IO密集型,很耗硬件;
维护性复杂
一致性维护成本越来越高;
BerkeleyDB使用B树,会一直写新的,内部不会有文件重新组织;这样会导致文件越来越大;大的时候需要进行文件归档,归档的 *** 作要定期做;
这样,就需要有一定的down time;
基于以上考虑, 选择了Redis
第三:Redis 在新浪微博中的应用
Redis简介
1 支持5种数据结构
支持strings, hashes, lists, sets, sorted sets
string是很好的存储方式,用来做计数存储。sets用于建立索引库非常棒;
2 K-V 存储 vs K-V 缓存
新浪微博目前使用的98%都是持久化的应用,2%的是缓存,用到了600+服务器
Redis中持久化的应用和非持久化的方式不会差别很大:
非持久化的为8-9万tps,那么持久化在7-8万tps左右;
当使用持久化时,需要考虑到持久化和写性能的配比,也就是要考虑redis使用的内存大小和硬盘写的速率的比例计算;
3 社区活跃
Redis目前有3万多行代码, 代码写的精简,有很多巧妙的实现,作者有技术洁癖
Redis的社区活跃度很高,这是衡量开源软件质量的重要指标,开源软件的初期一般都没有商业技术服务支持,如果没有活跃社区做支撑,一旦发生问题都无处求救;
Redis基本原理
redis持久化(aof) append online file:
写log(aof), 到一定程度再和内存合并 追加再追加, 顺序写磁盘, 对性能影响非常小
1 单实例单进程
Redis使用的是单进程,所以在配置时,一个实例只会用到一个CPU;
在配置时,如果需要让CPU使用率最大化,可以配置Redis实例数对应CPU数, Redis实例数对应端口数(8核Cpu, 8个实例, 8个端口), 以提高并发:
单机测试时, 单条数据在200字节, 测试的结果为8~9万tps;
2 Replication
过程: 数据写到master–>master存储到slave的rdb中–>slave加载rdb到内存。
存储点(save point): 当网络中断了, 连上之后, 继续传
Master-slave下第一次同步是全传,后面是增量同步;、
3 数据一致性
长期运行后多个结点之间存在不一致的可能性;
开发两个工具程序:
1对于数据量大的数据,会周期性的全量检查;
2实时的检查增量数据,是否具有一致性;
对于主库未及时同步从库导致的不一致,称之为延时问题;
对于一致性要求不是那么严格的场景,我们只需要要保证最终一致性即可;
对于延时问题,需要根据业务场景特点分析,从应用层面增加策略来解决这个问题;
例如:
1新注册的用户,必须先查询主库;
2注册成功之后,需要等待3s之后跳转,后台此时就是在做数据同步。
第四:分布式缓存的架构设计
1架构设计
由于redis是单点,项目中需要使用,必须自己实现分布式。基本架构图如下所示:
2分布式实现
通过key做一致性哈希,实现key对应redis结点的分布。
一致性哈希的实现:
l hash值计算:通过支持MD5与MurmurHash两种计算方式,默认是采用MurmurHash,高效的hash计算
l 一致性的实现:通过java的TreeMap来模拟环状结构,实现均匀分布
3client的选择
对于jedis修改的主要是分区模块的修改,使其支持了跟据BufferKey进行分区,跟据不同的redis结点信息,可以初始化不同的 ShardInfo,同时也修改了JedisPool的底层实现,使其连接pool池支持跟据key,value的构造方法,跟据不同 ShardInfos,创建不同的jedis连接客户端,达到分区的效果,供应用层调用
4模块的说明
l 脏数据处理模块,处理失败执行的缓存 *** 作。
l 屏蔽监控模块,对于jedis *** 作的异常监控,当某结点出现异常可控制redis结点的切除等 *** 作。
整个分布式模块通过hornetq,来切除异常redis结点。对于新结点的增加,也可以通过reload方法实现增加。(此模块对于新增结点也可以很方便实现)
对于以上分布式架构的实现满足了项目的需求。另外使用中对于一些比较重要用途的缓存数据可以单独设置一些redis结点,设定特定的优先级。另外对 于缓存接口的设计,也可以跟据需求,实现基本接口与一些特殊逻辑接口。对于cas相关 *** 作,以及一些事物 *** 作可以通过其watch机制来实现。
声明:所有博客服务于分布式框架,作为框架的技术支持及说明,框架面向企业,是大型互联网分布式企业架构,后期会介绍linux上部署高可用集群项目。
要配置连接容器上的Redis和数据库,可以按照以下步骤进行 *** 作:1 确认容器内的Redis和数据库已经启动并运行正常。
2 在web应用的配置文件中添加Redis和数据库的连接信息,包括主机名、端口号、用户名、密码等。
3 如果Redis和数据库运行在同一容器中,可以使用容器内部的IP地址进行连接。如果Redis和数据库运行在不同的容器中,可以使用Docker网络进行连接。
4 在web应用中使用相应的Redis和数据库客户端库进行连接和 *** 作。
例如,在Java应用中,可以使用Jedis客户端库连接Redis,使用JDBC客户端库连接数据库。以下是一个Java应用连接Redis和MySQL数据库的示例代码:
```
//连接Redis
Jedis jedis = new Jedis("redis_host", 6379);
jedisauth("redis_password");
//连接MySQL数据库
String url = "jdbc:mysql://mysql_host:3306/db_name";
String user = "db_user";
String password = "db_password";
Connection conn = DriverManagergetConnection(url, user, password);
```
需要根据实际情况修改主机名、端口号、用户名、密码等连接信息。1选择spring
目前企业的java应用中,spring框架是必须的,spring的核心是IOC(控制反转),它是一个大容器,方便组装和管理各类系统内外部资源,同时支持AOP(控制反转),这是对面向对象的补充(),目前广泛应用于日志和数据库事物控制,减少了大量重复代码,使得程序更为清晰,而且spring可以使模块解耦,控制对象之间的协作,所以spring框架是目前java最为流行的框架之一。
2选择springMVC
SpringMVC和Struts2虽然都是MVC模式,都有着核心分发器等相同的功能组件,但:
1、Struts2是类级别的拦截, 一个类对应一个request上下文,SpringMVC是方法级别的拦截,一个方法对应一个request上下文,而方法同时又跟一个url对应,所以说从架构本身上SpringMVC就容易实现restful url(),而struts2的架构实现起来要费劲,因为Struts2中Action的一个方法可以对应一个url,而其类属性却被所有方法共享,这也就无法用注解或其他方式标识其所属方法了。
在划水摸鱼之际,突然听到有的用户反映增加了多条一样的数据,这用户立马就不干了,让我们要马上修复,不然就要投诉我们。
这下鱼也摸不了了,只能去看看发生了什么事情。据用户反映,当时网络有点卡,所以多点了几次提交,最后发现出现了十几条一样的数据。
只能说现在的人都太心急了,连这几秒的时间都等不了,惯的。心里吐槽归吐槽,这问题还是要解决的,不然老板可不惯我。
其实想想就知道为啥会这样,在网络延迟的时候,用户多次点击,最后这几次请求都发送到了服务器访问相关的接口,最后执行插入。
既然知道了原因,该如何解决。当时我的第一想法就是用 注解 + AOP 。通过在自定义注解里定义一些相关的字段,比如过期时间即该时间内同一用户不能重复提交请求。然后把注解按需加在接口上,最后在拦截器里判断接口上是否有该接口,如果存在则拦截。
解决了这个问题那还需要解决另一个问题,就是怎么判断当前用户限定时间内访问了当前接口。其实这个也简单,可以使用Redis来做,用户名 + 接口 + 参数啥的作为唯一键,然后这个键的过期时间设置为注解里过期字段的值。设置一个过期时间可以让键过期自动释放,不然如果线程突然歇逼,该接口就一直不能访问。
这样还需要注意的一个问题是,如果你先去Redis获取这个键,然后判断这个键不存在则设置键;存在则说明还没到访问时间,返回提示。这个思路是没错的,但这样如果获取和设置分成两个 *** 作,就不满足原子性了,那么在多线程下是会出错的。所以这样需要把俩 *** 作变成一个原子 *** 作。
分析好了,就开干。
这里为了简单一点,只定义了一个字段 expire ,默认值为3,即3s内同一用户不允许重复访问同一接口。使用的时候也可以传入自定义的值。
我们只需要在对应的接口上添加该注解即可
自定义好了注解,那就该写拦截器了。
拦截器定义的切点是 NoRepeatCommit 注解,所以被 NoRepeatCommit 注解标注的接口就会进入该拦截器。这里我使用了 account + "_" + sessionId + "_" + url + "_" + pg 作为唯一键,表示某个用户访问某个接口。
这样比较关键的一行是 boolean isSuccess = redisLocktryLock(key, key + sessionId, expire); 。可以看看 RedisLock 这个类。
上面讨论过了,获取锁和设置锁需要做成原子 *** 作,不然并发环境下会出问题。这里可以使用Redis的 SETNX 命令。
在加锁的时候,我使用了 String result = jedisset(lockKey, userId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); 。set方法如下
在key不存在的情况下,才会设置key,设置成功则返回OK。这样就做到了查询和设置原子性。
需要注意这里在使用完jedis,需要进行close,不然耗尽连接数就完蛋了,我不会告诉你我把服务器搞挂了。
其实做完这三步差不多了,基本够用。再考虑一些其他情况的话,比如在expire设置的时间内,我这个接口还没执行完逻辑咋办呢?
其实我们不用自己在这整破轮子,直接用健壮的轮子不好吗?比如 Redisson ,来实现分布式锁,那么上面的问题就不用考虑了。有看门狗来帮你做,在键过期的时候,如果检查到键还被线程持有,那么就会重新设置键的过期时间。
当同时满足以下条件时,使用ziplist编码:
SpringBoot—实现n秒内出现x个异常报警
思路:
借助Redis的zSet集合,score存储的是异常时的时间戳,获取一定时间范围内的set集合。判断set个数是否满足条件,若满足条件则触发报警;
注意点:
相关API:
Redis实现延迟队列方法介绍
基于Redis实现DelayQueue延迟队列设计方案
相关API:
SpringBoot2x—使用Redis的bitmap实现布隆过滤器(Guava中BF算法)
布隆过滤器: 是专门用来检测集合中是否存在特定元素的数据结构。
存在误差率: 即将不在集合的元素误判在集合中。
所以布隆过滤器适合查询准确度要求没这么苛刻,但是对时间、空间效率比较高的场景。
实现方式:Redis实现布隆过滤器——借鉴Guava的BF算法:
SpringBoot2x中使用Redis的bitmap结构(工具类)
注意:bitmap使用存在风险,若仅仅计算hash值,会导致bitmap占用空间过大。一般需要对hash值进行取余处理。
根据Redis是否存在key,判断锁是否被获取;
锁应该是一个对象,记录持有锁的线程信息、当前重入次数。所以应该使用Redis的Hash结构来存储锁对象。
31 网络波动造成释放锁失败怎么解决?
需要为锁加上超时时间;
32 任务未执行完毕时,锁由于超时时间被释放?
线程一旦加锁成功,可以启动一个后台线程,每隔多少秒检查一次,如果线程还持有锁,可以不断延长锁的生存时间。
主从切换时,从服务器上没有加锁信息,导致多个客户端同时加锁。
list结构底层是ziplist/quicklist(可看着一个双端队列)。常用命令:
使用list作为对象的缓存池。通过rpush放入对象,通过lpop取出对象。
若是阻塞取,可以使用blpop命令实现。
Redis和Lua脚本(实现令牌桶限流)
数据结构选择hash。
hash里面维护:最后放入令牌时间、当前桶内令牌量、桶内最大数量、令牌放置速度(元数据)。
被动式维护:
命令:incr原子累加;
对一段固定时间窗口内的请求进行计数,如果请求数超过了阈值,则舍弃该请求;如果没有达到设定的阈值,则接受该请求,且计数加1。当窗口时间结束,重置计数器为0。
优点:实现简单,容易理解;
缺点:流量曲线可能不够平滑,有“突刺现象”。
1 一段时间内(不超过时间窗口)系统服务不可用。 比如窗口大小1s,限流为100,恰好某个窗口第1ms来了100个请求,然后2ms-999ms请求都会被拒绝。这段时间用户会感觉系统服务不可用(即不够平滑)。
2 窗口切换时可能会出现两倍于阈值流量的请求。 比如窗口大小1s,限流大小100,然后在某个窗口的第999ms有100个请求,窗口前期没有请求。所以这100个请求都会通过。然后下一个窗口的第1ms又来100个请求,然后全部通过。其实也是1ms内通过的200个请求。
命令:Redis的incr命令
是对固定窗口计数器的优化,解决的是切换窗口两倍阈值流量的场景。
具体解决方案是:将限流窗口分为多个小的限流窗口,各个限流窗口分别计数。当前时间大于窗口最大时间时,将头部的小窗口数据舍弃,尾部新增小窗口来处理新请求。
优点:本质上是对固定窗口的优化
访问Spring Session官方网站:在百度中查询Spring Session即可找到Spring Session的官方站点。
目前版本为102,103版本处于snapshot状态。
Spring指出,Spring Session具有如下能力:
(1) API and implementations for managing a user's session
(2) >说一道常见面试题:
一个很简单的答案就是去使用 Redission 客户端。Redission 中的锁方案就是 Redis 分布式锁得比较完美的详细方案。
那么,Redission 中的锁方案为什么会比较完美呢?
正好,我用 Redis 做分布式锁经验十分丰富,在实际工作中,也 探索 过许多种使用 Redis 做分布式锁的方案,经过了无数血泪教训。
所以,在谈及 Redission 锁为什么比较完美之前,先给大家看看我曾经使用 Redis 做分布式锁是遇到过的问题。
我曾经用 Redis 做分布式锁是想去解决一个用户抢优惠券的问题。这个业务需求是这样的:当用户领完一张优惠券后,优惠券的数量必须相应减一,如果优惠券抢光了,就不允许用户再抢了。
在实现时,先从数据库中先读出优惠券的数量进行判断,当优惠券大于 0,就进行允许领取优惠券,然后,再将优惠券数量减一后,写回数据库。
当时由于请求数量比较多,所以,我们使用了三台服务器去做分流。
这个时候会出现一个问题:
如果其中一台服务器上的 A 应用获取到了优惠券的数量之后,由于处理相关业务逻辑,未及时更新数据库的优惠券数量;在 A 应用处理业务逻辑的时候,另一台服务器上的 B 应用更新了优惠券数量。那么,等 A 应用去更新数据库中优惠券数量时,就会把 B 应用更新的优惠券数量覆盖掉。
看到这里,可能有人比较奇怪,为什么这里不直接使用 SQL:
原因是这样做,在没有分布式锁的协调下,优惠券数量可能直接会出现负数。因为当前优惠券数量为 1 的时候,如果两个用户通过两台服务器同时发起抢优惠券的请求,都满足优惠券大于 0 每个条件,然后都执行这条 SQL 说了句,结果优惠券数量直接变成 -1 了。
还有人说可以用乐观锁,比如使用如下 SQL:
这种方式就在一定几率下,很可能出现数据一直更新不上,导致长时间重试的情况。
所以,经过综合考虑,我们就采用了 Redis 分布式锁,通过互斥的方式,以防止多个客户端同时更新优惠券数量的方案。
当时,我们首先想到的就是使用 Redis 的 setnx 命令,setnx 命令其实就是 set if not exists 的简写。
当 key 设置值成功后,则返回 1,否则就返回 0。所以,这里 setnx 设置成功可以表示成获取到锁,如果失败,则说明已经有锁,可以被视作获取锁失败。
如果想要释放锁,执行任务 del 指令,把 key 删除即可。
利用这个特性,我们就可以让系统在执行优惠券逻辑之前,先去 Redis 中执行 setnx 指令。再根据指令执行结果,去判断是否获取到锁。如果获取到了,就继续执行业务,执行完再使用 del 指令去释放锁。如果没有获取到,就等待一定时间,重新再去获取锁。
乍一看,这一切没什么问题,使用 setnx 指令确实起到了想要的互斥效果。
但是,这是建立在所有运行环境都是正常的情况下的。一旦运行环境出现了异常,问题就出现了。
想一下,持有锁的应用突然崩溃了,或者所在的服务器宕机了,会出现什么情况?
这会造成死锁——持有锁的应用无法释放锁,其他应用根本也没有机会再去获取锁了。这会造成巨大的线上事故,我们要改进方案,解决这个问题。
怎么解决呢?咱们可以看到,造成死锁的根源是,一旦持有锁的应用出现问题,就不会去释放锁。从这个方向思考,可以在 Redis 上给 key 一个过期时间。
这样的话,即使出现问题,key 也会在一段时间后释放,是不是就解决了这个问题呢?实际上,大家也确实是这么做的。
不过,由于 setnx 这个指令本身无法设置超时时间,所以一般会采用两种办法来做这件事:
1、采用 lua 脚本,在使用 setnx 指令之后,再使用 expire 命令去给 key 设置过期时间。
2、直接使用 set(key,value,NX,EX,timeout) 指令,同时设置锁和超时时间。
以上两种方法,使用哪种方式都可以。
释放锁的脚本两种方式都一样,直接调用 Redis 的 del 指令即可。
到目前为止,我们的锁既起到了互斥效果,又不会因为某些持有锁的系统出现问题,导致死锁了。这样就完美了吗?
假设有这样一种情况,如果一个持有锁的应用,其持有的时间超过了我们设定的超时时间会怎样呢?会出现两种情况:
出现第一种情况比较正常。因为你毕竟执行任务超时了,key 被正常清除也是符合逻辑的。
但是最可怕的是第二种情况,发现设置的 key 还存在。这说明什么?说明当前存在的 key,是另外的应用设置的。
这时候如果持有锁超时的应用调用 del 指令去删除锁时,就会把别人设置的锁误删除,这会直接导致系统业务出现问题。
所以,为了解决这个问题,我们需要继续对 Redis 脚本进行改动……毁灭吧,累了……
首先,我们要让应用在获取锁的时候,去设置一个只有应用自己知道的独一无二的值。
通过这个唯一值,系统在释放锁的时候,就能识别出这锁是不是自己设置的。如果是自己设置的,就释放锁,也就是删除 key;如果不是,则什么都不做。
脚本如下:
或者
这里,ARGV[1] 是一个可传入的参数变量,可以传入唯一值。比如一个只有自己知道的 UUID 的值,或者通过雪球算法,生成只有自己持有的唯一 ID。
释放锁的脚本改成这样:
可以看到,从业务角度,无论如何,我们的分布式锁已经可以满足真正的业务需求了。能互斥,不死锁,不会误删除别人的锁,只有自己上的锁,自己可以释放。
一切都是那么美好!!!
可惜,还有个隐患,我们并未排除。这个隐患就是 Redis 自身。
要知道,lua 脚本都是用在 Redis 的单例上的。一旦 Redis 本身出现了问题,我们的分布式锁就没法用了,分布式锁没法用,对业务的正常运行会造成重大影响,这是我们无法接受的。
所以,我们需要把 Redis 搞成高可用的。一般来讲,解决 Redis 高可用的问题,都是使用主从集群。
但是搞主从集群,又会引入新的问题。主要问题在于,Redis 的主从数据同步有延迟。这种延迟会产生一个边界条件:当主机上的 Redis 已经被人建好了锁,但是锁数据还未同步到从机时,主机宕了。随后,从机提升为主机,此时从机上是没有以前主机设置好的锁数据的——锁丢了……丢了……了……
到这里,终于可以介绍 Redission(开源 Redis 客户端)了,我们来看看它怎么是实现 Redis 分布式锁的。
Redission 实现分布式锁的思想很简单,无论是主从集群还是 Redis Cluster 集群,它会对集群中的每个 Redis,挨个去执行设置 Redis 锁的脚本,也就是集群中的每个 Redis 都会包含设置好的锁数据。
我们通过一个例子来介绍一下。
假设 Redis 集群有 5 台机器,同时根据评估,锁的超时时间设置成 10 秒比较合适。
第 1 步,咱们先算出集群总的等待时间,集群总的等待时间是 5 秒(锁的超时时间 10 秒 / 2)。
第 2 步,用 5 秒除以 5 台机器数量,结果是 1 秒。这个 1 秒是连接每台 Redis 可接受的等待时间。
第 3 步,依次连接 5 台 Redis,并执行 lua 脚本设置锁,然后再做判断:
再额外多说一句,在很多业务逻辑里,其实对锁的超时时间是没有需求的。
比如,凌晨批量执行处理的任务,可能需要分布式锁保证任务不会被重复执行。此时,任务要执行多长时间是不明确的。如果设置分布式锁的超时时间在这里,并没有太大意义。但是,不设置超时时间,又会引发死锁问题。
所以,解决这种问题的通用办法是,每个持有锁的客户端都启动一个后台线程,通过执行特定的 lua 脚本,去不断地刷新 Redis 中的 key 超时时间,使得在任务执行完成前,key 不会被清除掉。
脚本如下:
其中,ARGV[1] 是可传入的参数变量,表示持有锁的系统的唯一值,也就是只有持有锁的客户端才能刷新 key 的超时时间。
到此为止,一个完整的分布式锁才算实现完毕。总结实现方案如下:
这个分布式锁满足如下四个条件:
当然,在 Redission 中的脚本,为了保证锁的可重入,又对 lua 脚本做了一定的修改,现在把完整的 lua 脚本贴在下面。
获取锁的 lua 脚本:
对应的刷新锁超时时间的脚本:
对应的释放锁的脚本:
到现在为止,使用 Redis 作为分布式锁的详细方案就写完了。
我既写了一步一坑的坎坷经历,也写明了各个问题和解决问题的细节,希望大家看完能有所收获。
最后再给大家提个醒,使用 Redis 集群做分布式锁,有一定的争议性,还需要大家在实际用的时候,根据现实情况,做出更好的选择和取舍。
原文 >在Redis中按session存储token可以将token作为键,sesssionId作为值存储。首先需要创建一个包含session id的键,例如 "session:1",然后将token与此键进行关联,例如 "token:xxxxxxxxx"。这样就可以使用Redis来存储和获取token的 *** 作,同时确保与session id相关的所有token都属于该session id。在此基础上,还可以设置token在Redis中的过期时间,以保证安全性和快速的检索。此外,通过使用Redis支持的事务,可以保证在存储和获取token的过程中,具有原子性和数据一致性,提高了系统的可靠性和性能。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)