招商银行面试:
如何保证MySQL和Redis的双写一致性?
只考虑以下三种更新策略:
先更新数据库,再更新缓存:
1 线程A更新了数据库
2 线程B更新了数据库
3 线程B更新了缓存
4 线程A更新了缓存
这本该请求A先更新缓存,B后更新才对,但是因为网络等原因,B却比A更早更新了缓存,这就导致出现了脏数据,故不考虑。
1 如果你是一个写数据场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据还没读到,缓存就被频繁的更新,浪费性能
2 如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存的,那么,每次写入数据库,都再次计算写入缓存的值,无疑是浪费性能的,显然,删除缓存更为合适。
先删除缓存,再更新数据库
该方案会导致不一致的原因是:同时有一个请求A进行更新操作,另一个请求B进行查询操作,那么会出现以下几种情景:
1、请求A进行写操作,删除缓存
2、请求B进行读操作,发现缓存不存在
3、请求B去数据库查询得到旧值
4、请求B将旧值写入缓存
5、请求A将新值写入数据库,这样的情况就会导致不一致的情形出现,而且,如果不采用给缓存设置过期时间,该数据永远都是脏数据
采用延时双删策略:1、先淘汰缓存2、再写数据库3、休眠1秒,再次淘汰缓存,可以将1秒内所造成的缓存脏数据再次删除。
针对上面的情形,应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
如果你用了mysql的读写分离架构怎么办?
还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。
(1)请求A进行写操作,删除缓存
(2)请求A将数据写入数据库了,
(3)请求B查询缓存发现,缓存没有值
(4)请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
(5)请求B将旧值写入缓存
(6)数据库完成主从同步,从库变为新值 上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。
采用这种同步淘汰策略,吞吐量会降低,那又该怎么办呢?
那可以将第二次删除作为异步,自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后再返回,这样做就可以加大吞吐量。
第二次删除,如果删除失败怎么办?
这会出现下面的请求,一个A请求进行更新操作,另一个请求B进行查询操作,为了方便,假设是单库:
1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
(6)请求A试图去删除请求B写入对缓存值,结果失败了。 ok,这也就是说。如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题。 如何解决呢? 具体解决方案,且看第(3)种更新策略的解析。
先更新数据库,再删除缓存
1**、失效**:应用程序先从cache取数据,没有得到,从数据库中取数据,成功后,放到缓存中。
2**、命中**:应用程序从cache中取数据,取到后返回。
3**、更新**:先把数据存到数据库中,成功后,再让缓存失效。
更新数据路数据
缓存因为种种问题删除失败
将需要删除的key发送至消息队列
自己消费消息,获得需要删除的key
继续充实删除操作,直到成功,然而,该方案有一个缺点对业务线代码造成大量的侵入,于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据,在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作
更新数据库数据
数据库会将操作信息写入binlog日志中
订阅程序提取所需要的数据以及key
另起一段非业务代码,获得该信息
尝试删除缓存操作,发现删除失败
将这些信息发送至消息队列
重新从消息队列中获得该数据,重新操作
如何实现分布式事务?
CPA原则 CA PA的原则 分布式事务保证的可用性和分区性 最终一致性的原则。数据的一致性 。采用的是两阶段的数据提交的方式来保证数据的最终一致性。采用的Redis的锁和zookeeper的锁机制来实现分布式事务。
如何实现分布式锁?
Redis实现分布式的锁
zookeeper实现分布式锁的机制
zk和Redis分布式锁的优缺点?
Redis为什么快?
数据结构简单丰富
多路复用的NIO技术
单线程的线程安全技术
直接对内存的操作原理
redis的单线程模型原理是什么?
采用的是一主多从的模型来实现。
如何实现MySQL的读写分离?
主库用于写操作 其他的库用于读操作,这个数据的就需要保证的数据的一致性。通过利用的数据的异步的写入到读库中。
在读写分离的前提下,如何强行去读主节点?
如何实现单机服务限流?资源隔离是怎么做的?熔断是怎么做的?降级的逻辑是怎么写的?
什么是服务的雪崩?A-B –C –D 但是其中一个坏了导致大量的请求的A中。导致A不用,这样导致其他服务也不可以用。采用就的springcloud的的Hystrix来实现对服务的降级和熔断操作。
在启动类中加入注解:@EnableHystrix
在需要做熔断的类上添加一下注解。其中fallbackMethod属性表示:当方法发生错误时,会跳到我们自己定义的findBasicDataByTypeFallBack方法中进行处理。这样的话当服务发生错误时,就会去执行我们自己定义的方法,返回一个结果,不会导致整个服务不可用。
分布式服务的限流如何做呢?
一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。
限流算法:常见的限流算法有:令牌桶、漏桶。计数器也可以进行粗暴限流实现。
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:
令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.
令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.
Tomcat的限流:
对于一个应用系统来说一定会有极限并发/请求数,即总有一个TPS/QPS阀值,如果超了阀值则系统就会不响应用户请求或响应的非常慢,因此我们最好进行过载保护,防止大量请求涌入击垮系统。如果你使用过Tomcat,其Connector其中一种配置有如下几个参数:
acceptCount:如果Tomcat的线程都忙于响应,新来的连接会进入队列排队,如果超出排队大小,则拒绝连接;
maxConnections:瞬时最大连接数,超出的会排队等待;
maxThreads:Tomcat能启动用来处理请求的最大线程数,如果请求处理量一直远远大于最大线程数则可能会僵死
详细的配置请参考官方文档。另外如MySQL(如max_connections)、Redis(如tcp-backlog)都会有类似的限制连接数的配置。
池化技术限流:
如果有的资源是稀缺资源(如数据库连接、线程),而且可能有多个系统都会去使用它,那么需要限制应用;可以使用池化技术来限制总资源数:连接池、线程池。比如分配给每个应用的数据库连接是100,那么本应用最多可以使用100个资源,超出了可以等待或者抛异常。
限流某个接口的总并发/请求数
如果接口可能会有突发访问情况,但又担心访问量太大造成崩溃,如抢购业务;这个时候就需要限制这个接口的总并发/请求数总请求数了;因为粒度比较细,可以为每个接口都设置相应的阀值。可以使用Java中的AtomicLong进行限流:
try {if(atomic.incrementAndGet() > 限流数) {//拒绝请求}
catch{//处理请求}} finally {atomic.decrementAndGet();}
分布式限流:
分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使使用redis+lua或者nginx+lua技术进行实现,通过这两种技术可以实现的高并发和高性能。
首先我们来使用redis+lua实现时间窗内某个接口的请求数限流,实现了该功能后可以改造为限流总并发/请求数和限制总资源数。
有人会纠结如果应用并发量非常大那么redis或者nginx是不是能抗得住;不过这个问题要从多方面考虑:你的流量是不是真的有这么大,是不是可以通过一致性哈希将分布式限流进行分片,是不是可以当并发量太大降级为应用级限流;对策非常多,可以根据实际情况调节;像在京东使用Redis+Lua来限流抢购流量,一般流量是没有问题的。
对于分布式限流目前遇到的场景是业务上的限流,而不是流量入口的限流;流量入口限流应该在接入层完成,而接入层笔者一般使用Nginx。
基于Redis功能的实现限流
简陋的设计思路:假设一个用户(用IP判断)每分钟访问某一个服务接口的次数不能超过10次,那么我们可以在Redis中创建一个键,并此时我们就设置键的过期时间为60秒,每一个用户对此服务接口的访问就把键值加1,在60秒内当键值增加到10的时候,就禁止访问服务接口。在某种场景中添加访问时间间隔还是很有必要的。
基于令牌桶算法的实现:
令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。
传送到令牌桶的数据包需要消耗令牌。不同大小的数据包,消耗的令牌数量不一样。
令牌桶这种控制机制基于令牌桶中是否存在令牌来指示什么时候可以发送流量。令牌桶中的每一个令牌都代表一个字节。如果令牌桶中存在令牌,则允许发送流量;而如果令牌桶中不存在令牌,则不允许发送流量。因此,如果突发门限被合理地配置并且令牌桶中有足够的令牌,那么流量就可以以峰值速率发送。
kafka如何保证消息的不重复消费或不丢失?
kafka的消息发送机制分为同步和异步机制。可以通过producer.type属性进行配置。使用同步模式的时候,有三种状态来保证消息的安全生产。可以通过配置request.required.acks属性。三个属性分别如下:
0—表示不进行消息接收是否成功的确认;
1—表示当Leader接收成功时确认;
-1—表示Leader和Follower都接收成功时确认
当acks = 0的时候,不和Kafka集群进行消息接收确认,则当网络异常、缓冲区满了等情况时,消息可能丢失;当acks=1的时候,只保证leader写入成功。当leader partition挂了的时候,数据就有可能发生丢失。另外还有一种情况,使用异步模式的时候,当缓冲区满了,acks=0的时候,不需要进行消息接受是否成功的确认,所以会自动清空缓冲池里的消息。
同步模式下只需要将确认机制设置为-1,让消息写入leader和所有的副本,就可以保证消息安全生产。
异步模式下,则需要在配置文件中,将阻塞超时的时间设置为不限制。这样生产端会一直阻塞。可以保证数据不丢失。
我们需要设置block.on.buffer.full = true。 这样producer将一直等待缓冲区直至其变为可用。缓冲区满了就阻塞
acks=all。所有的follwoer都响应了消息就认为消息提交成功。
retries=MAX。无限重试。
max.in.flight.requests.per.connnection = 1限制客户端在单个连接上能够发送的未响应的请求的个数。设置为1表示kafka broker在响应请求之前client不能再向broker发送请求了。通过此举可以保证消息的顺序性。
消息的接受机制
kafka有个offset的概念,当每个消息被写进去后,都有一个offset,代表他的序号,然后consumer消费该数据之后,隔一段时间,会把自己消费过的消息的offset提交一下,代表已经消费过了。下次我要重启,就会继续从上次消费到的offset来继续消费。
但是当我们直接kill进程了,再重启。这会导致consumer有些消息处理了,但是没来得及提交offset。等重启之后,少数消息就会再次消费一次。其他MQ也会有这种重复消费的问题,那么针对这种问题,我们需要从业务角度,考虑它的幂等性。
消息的重复消费如何解决
重复消费的问题,一方面需要消息中间件来进行保证。另一方面需要自己的处理逻辑来保证消息的幂等性。极有可能代码消费了消息,但服务器突然宕机,未来得及提交offset。所以我们可以在代码保证消息消费的幂等性。至于方法可以通过redis的原子性来保证,也可以通过数据库的唯一id来保证。
如何保证Kafka的消息一定发送成功呢?sequenceNumber实现精准一次性,ISR中所有节点同步来保证消息不会丢失。