当前位置:首页 » 硬盘大全 » 缓存与数据库双写一致性保障方案
扩展阅读
webinf下怎么引入js 2023-08-31 21:54:13
堡垒机怎么打开web 2023-08-31 21:54:11

缓存与数据库双写一致性保障方案

发布时间: 2022-11-02 16:37:28

⑴ 秒杀过程中怎么保证redis缓存数据库的一致性

如果要“保证”数据的安全性,那么会带来开销的进一步提升,以至于使用redis带来的性能优势都会丧失。正确的做法是区分不同的业务,使得并不需要“保证”数据一致性的场合,可以使用redis优化。而敏感的场合依然使用mysql

⑵ SQLServer数据库中如何保持数据一致性

根据实现策略的不同,主要有快照复制、事务复制、合并复制等三种类型。这三种复制类型,各有各的特点,分别适用于不同的场合。一般来说,在考虑采用哪种复制类型比较合适的时候,主要考虑的是性能与数据同步的时间间复制是SQLServer数据库中保持数据一致性的一种手段。根据实现策略的不同,主要有快照复制、事务复制、合并复制等三种类型。这三种复制类型,各有各的特点,分别适用于不同的场合。一般来说,在考虑采用哪种复制类型比较合适的时候,主要考虑的是性能与数据同步的时间间隔。那么在什么情形下比较适用快照复制呢?笔者就跟大家来讨论一下这个话题。 为了在恰当的时候采用快照复制,数据库管理员首先需要知道快照复制的特点。快照复制是指将数据以特定时刻的瞬时状态转发,而不坚实对数据的更新。在发生同步时,将生成完整的快照并将其发送到订阅服务器。简单的说,快照复制就是每隔一段时间发生数据同步操作。而不是发布服务器的数据一有更新就出发这个快照复制。显然这种快照复制的数据同步性稍微差一点。在订阅服务器与发布服务器之间有一段时间会存在数据不一致的情况。但是这可以在很大程度上提高订阅服务器与发布服务器的性能。这就好像汽车运输。采用快照复制的话可以将一个集装箱装满后在送货,而不是有多少送多少。掌握这个数据库复快照复制的具体特点之后,数据库管理员就可以来考虑在什么情况下,采用快照复制更加的合理。 一、数据更改比较少的系统中。 快照复制与其他复制相比最主要的缺陷就是数据库中的数据无法及时同发布服务器一致。为此如果发布服务器中的内容很少更改的话,显然此时采用快照复制是比较合理的。此时采用快照复制的话,不仅数据一致性延迟的负面效应会越来越不明显,同时可以提高发布服务器与订阅服务器的性能。如在实际工作中,经常会遇到这样的客户。如一家企业在各地都有办事处或者销售机构,就像肯德基一样,各地的产品价格基本上都是相同的,不怎么会更改。即使更改的话,各地也是统一调整。由于此时产品价格表更改的比较少,那么在企业总部的数据库服务与各地的订阅服务器之间,采用快照复制的形式就会比较合适。其实类似的情况有很多。如不少的服装企业,像李宁、耐克等等,他们不仅自己生产,而且在各地又有自己的销售办事处。在价格方面也是统一的。在这种情况下,采用快照复制往往能够提高数据库复制的性能,同时又不影响其使用。 二、在某个时段内会出现数据大量的更改。 需要补充说明的一点是,上面说到的数据不怎么发生更改,指的是数据的延续性更改。如在一年中,每天或者每个小时更改的数据都比较平均。此时采用快照复制不怎么合适。但是如果数据的更改集中在一个时段内。而其他时间中数据库的内容不会有多大的更改。此时采用快照复制是可行的。如一些决策性系统,往往在起初导入数据的时候,需要进行大量的更改。而等到数据导入完毕,在大家对数据进行分析时,则数据库中的内容基本上保持不变。在这种情况下,笔者认为只要数据的更新集中在一个固定的时段,此时采用快照复制仍然是可行的。 再如上面这个KFC或者服装企业的案例中,如果市场部门维护一个产品的价格,而且这些价格往往在一个固定的时间进行几次更新。如在换季的时候会进行一些促销。此时数据库管理员可以在数据更新完毕后立即执行复制完成的数据快照。所以,以数据更新来判断是否适合采用快照复制,标准并不是数据的更新量。像上面提到的分析决策系统,其起初的数据更新量可能比有些数据库系统几年的数据更新量都要大。笔者认为,主要是根据数据更新的频率来进行判断。如果数据更新的比较频繁,那么即使数据更新的数据不多,像那种细水长流似的更新,则不适合采用快照复制。而那些井喷似的数据更新,所有的更新都集中在一个固定的时刻,那么此时采用快照复制是比较合理的。 三、在一段时间内是否允许具有相对发布服务器已过时的数据副本? 现在不少超市也已经连锁了,如世纪联华等等。为了提高利润,增加市场的份额,这些超市纷纷推出了冲值卡,即消费者先将一定金额的人民币打入到冲值卡中。然后每次消费完成后从卡中扣费。但前些天经常有新闻报道,说一个客户的消费卡在一家联华超市挂失了。但是捡到这张卡的人仍然可以在其他的联华超市中消费。为此消费者就想不明白了,为什么挂失了的消费卡仍然可以在其他超市中消费?挂失后的损失该由谁来承担呢?其实这就使超市在不适当的时候采用了快照复制所造成的。由于采用快照复制,在各个联华超市的数据库之间数据无法在短时间内取得一致。如有些商户说挂失当日之内的损失他们不承担,这就说明他们可能是每天下班后进行一次快照复制。一般情况下这不会有问题。但是像遇到消费卡被偷了等情况,就会遇到类似的问题了。 所以,在考虑是否适合采用快照复制的时候,还需要考虑在一段时间内是否允许具有相对发布服务器来说已过时的数据副本。如果不允许的话,那么就不允许采用这个快照复制。如果允许的话,那么数据库管理员就需要评估这段时间最长是多少。如果是24个小时,那么就需要每隔24小时进行一次快照复制。但是需要注意的是,如果时间的间隔比较短,如才允许十分钟的数据延迟,那么采用快照复制就没有必要了。此时采用事务复制或则和合并复制可能更加的合适。 四、复制少量的数据。 快照复制跟其他复制类型相比,还有一个比较显着的特点,即当发生数据同步时,将生成完整的快照并将其从发布服务器传送到订阅服务器。这是一个什么概念呢?如订阅服务器中有10G的数据,而在一个快照复制的周期内,只有1M的数据发生了更改。此时发生快照复制的话,数据库系统会将10G的数据都传送到订阅服务器上。此时更改的数据只有1M,却需要在网络上传送10G的数据流量,显然会对企业的网络产生比较大的压力。由于在发布服务器上快照复制的连续开销低于事务复制的开销,一次数据库系统不会启用跟踪增量更改。但是像这种情况,如果要复制的数据量非常的大,而平时的更新又不多。此时数据库系统要生成和应用快照,就将耗用大量的资源,包括网络资源和服务器资源。所以说,当发布服务器中的数据比较多时,采用快照复制不怎么合适。因为此时网络传输反而会成为其最重大的瓶颈资源。相反若能够采取细水长流的事务复制策略,那么对于企业网络性能的影响就会小的多,甚至可以忽略不计。 所以在采用快照复制的时候,数据库管理员一定要明白,快照复制会传送整个数据库对象。从而在快照复制传输过程中会侵蚀大量的网络带宽,从而明显的降低企业网络的性能,甚至导致网络拥塞。有时候为了保障快照能够准确、迅速的传递到其他的订阅服务器,还不得不采用VPN等技术来保障传输的准确性。为此,笔者认为只有发布服务器的数据库并不是很大的情况下,才适合采用快照复制。否则的话,采用快照复制是得不偿失。 从以上的分析中,可以得到一个结论。在考虑采用快照复制是否合适时,往往不能够采用一个指标来判断。而需要考虑多个因素,如数据库的大小、数据更新的频率、允许数据延迟的时间等等因素来进行判断。最后在数据的一致性与数据库的性能之间取得一个均衡。说实话,对于大部分数据库管理员来说,要做出一个抉择,确实有困难。因为这没有固定的指标可以拿来参考。如数据库容量小于多少时该采用快照复制。任何一个数据库管理专家都不能够下这个结论。所以在掌握影响其选择的相关因素外,就要依靠数据库管理员的经验了。在遇到类似的选择题时,往往经验可以帮助管理员迅速解决问题。最后需要提醒的是,无论最终采取了什么方案,最好能够持续跟踪一段时间,看看自己的选择是否合理。

⑶ 如何保证数据库缓存的最终一致性

对于互联网业务来说,传统的直接访问数据库方式,主要通过数据分片、一主多从等方式来扛住读写流量,但随着数据量的积累和流量的激增,仅依赖数据库来承接所有流量,不仅成本高、效率低、而且还伴随着稳定性降低的风险。

鉴于大部分业务通常是读多写少(读取频率远远高于更新频率),甚至存在读操作数量高出写操作多个数量级的情况。因此, 在架构设计中,常采用增加缓存层来提高系统的响应能力 ,提升数据读写性能、减少数据库访问压力,从而提升业务的稳定性和访问体验。

根据 CAP 原理,分布式系统在可用性、一致性和分区容错性上无法兼得,通常由于分区容错无法避免,所以一致性和可用性难以同时成立。对于缓存系统来说, 如何保证其数据一致性是一个在应用缓存的同时不得不解决的问题 。

需要明确的是,缓存系统的数据一致性通常包括持久化层和缓存层的一致性、以及多级缓存之间的一致性,这里我们仅讨论前者。持久化层和缓存层的一致性问题也通常被称为双写一致性问题,“双写”意为数据既在数据库中保存一份,也在缓存中保存一份。

对于一致性来说,包含强一致性和弱一致性 ,强一致性保证写入后立即可以读取,弱一致性则不保证立即可以读取写入后的值,而是尽可能的保证在经过一定时间后可以读取到,在弱一致性中应用最为广泛的模型则是最终一致性模型,即保证在一定时间之后写入和读取达到一致的状态。对于应用缓存的大部分场景来说,追求的则是最终一致性,少部分对数据一致性要求极高的场景则会追求强一致性。

为了达到最终一致性,针对不同的场景,业界逐步形成了下面这几种应用缓存的策略。


1

Cache-Aside


Cache-Aside 意为旁路缓存模式,是应用最为广泛的一种缓存策略。下面的图示展示了它的读写流程,来看看它是如何保证最终一致性的。在读请求中,首先请求缓存,若缓存命中(cache hit),则直接返回缓存中的数据;若缓存未命中(cache miss),则查询数据库并将查询结果更新至缓存,然后返回查询出的数据(demand-filled look-aside )。在写请求中,先更新数据库,再删除缓存(write-invalidate)。


1、为什么删除缓存,而不是更新缓存?

在 Cache-Aside 中,对于读请求的处理比较容易理解,但在写请求中,可能会有读者提出疑问,为什么要删除缓存,而不是更新缓存?站在符合直觉的角度来看,更新缓存是一个容易被理解的方案,但站在性能和安全的角度,更新缓存则可能会导致一些不好的后果。

首先是性能 ,当该缓存对应的结果需要消耗大量的计算过程才能得到时,比如需要访问多张数据库表并联合计算,那么在写操作中更新缓存的动作将会是一笔不小的开销。同时,当写操作较多时,可能也会存在刚更新的缓存还没有被读取到,又再次被更新的情况(这常被称为缓存扰动),显然,这样的更新是白白消耗机器性能的,会导致缓存利用率不高。

而等到读请求未命中缓存时再去更新,也符合懒加载的思路,需要时再进行计算。删除缓存的操作不仅是幂等的,可以在发生异常时重试,而且写-删除和读-更新在语义上更加对称。

其次是安全 ,在并发场景下,在写请求中更新缓存可能会引发数据的不一致问题。参考下面的图示,若存在两个来自不同线程的写请求,首先来自线程 1 的写请求更新了数据库(step 1),接着来自线程 2 的写请求再次更新了数据库(step 3),但由于网络延迟等原因,线程 1 可能会晚于线程 2 更新缓存(step 4 晚于 step 3),那么这样便会导致最终写入数据库的结果是来自线程 2 的新值,写入缓存的结果是来自线程 1 的旧值,即缓存落后于数据库,此时再有读请求命中缓存(step 5),读取到的便是旧值。


2、为什么先更新数据库,而不是先删除缓存?

另外,有读者也会对更新数据库和删除缓存的时序产生疑问,那么为什么不先删除缓存,再更新数据库呢?在单线程下,这种方案看似具有一定合理性,这种合理性体现在删除缓存成功。

但更新数据库失败的场景下,尽管缓存被删除了,下次读操作时,仍能将正确的数据写回缓存,相对于 Cache-Aside 中更新数据库成功,删除缓存失败的场景来说,先删除缓存的方案似乎更合理一些。那么,先删除缓存有什么问题呢?

问题仍然出现在并发场景下,首先来自线程 1 的写请求删除了缓存(step 1),接着来自线程 2 的读请求由于缓存的删除导致缓存未命中,根据 Cache-Aside 模式,线程 2 继而查询数据库(step 2),但由于写请求通常慢于读请求,线程 1 更新数据库的操作可能会晚于线程 2 查询数据库后更新缓存的操作(step 4 晚于 step 3),那么这样便会导致最终写入缓存的结果是来自线程 2 中查询到的旧值,而写入数据库的结果是来自线程 1 的新值,即缓存落后于数据库,此时再有读请求命中缓存( step 5 ),读取到的便是旧值。


另外,先删除缓存,由于缓存中数据缺失,加剧数据库的请求压力,可能会增大缓存穿透出现的概率。

3、如果选择先删除缓存,再更新数据库,那如何解决一致性问题呢?

为了避免“先删除缓存,再更新数据库”这一方案在读写并发时可能带来的缓存脏数据,业界又提出了延时双删的策略,即在更新数据库之后,延迟一段时间再次删除缓存,为了保证第二次删除缓存的时间点在读请求更新缓存之后,这个延迟时间的经验值通常应稍大于业务中读请求的耗时。

延迟的实现可以在代码中 sleep 或采用延迟队列。显而易见的是,无论这个值如何预估,都很难和读请求的完成时间点准确衔接,这也是延时双删被诟病的主要原因。


4、那么 Cache-Aside 存在数据不一致的可能吗?

在 Cache-Aside 中,也存在数据不一致的可能性。在下面的读写并发场景下,首先来自线程 1 的读请求在未命中缓存的情况下查询数据库(step 1),接着来自线程 2 的写请求更新数据库(step 2),但由于一些极端原因,线程 1 中读请求的更新缓存操作晚于线程 2 中写请求的删除缓存的操作(step 4 晚于 step 3),那么这样便会导致最终写入缓存中的是来自线程 1 的旧值,而写入数据库中的是来自线程 2 的新值,即缓存落后于数据库,此时再有读请求命中缓存(step 5),读取到的便是旧值。

这种场景的出现,不仅需要缓存失效且读写并发执行,而且还需要读请求查询数据库的执行早于写请求更新数据库,同时读请求的执行完成晚于写请求。足以见得,这种 不一致场景产生的条件非常严格,在实际的生产中出现的可能性较小 。


除此之外,在并发环境下,Cache-Aside 中也存在读请求命中缓存的时间点在写请求更新数据库之后,删除缓存之前,这样也会导致读请求查询到的缓存落后于数据库的情况。


虽然在下一次读请求中,缓存会被更新,但如果业务层面对这种情况的容忍度较低,那么可以采用加锁在写请求中保证“更新数据库&删除缓存”的串行执行为原子性操作(同理也可对读请求中缓存的更新加锁)。 加锁势必会导致吞吐量的下降,故采取加锁的方案应该对性能的损耗有所预期。


2

补偿机制


我们在上面提到了,在 Cache-Aside 中可能存在更新数据库成功,但删除缓存失败的场景,如果发生这种情况,那么便会导致缓存中的数据落后于数据库,产生数据的不一致的问题。

其实,不仅 Cache-Aside 存在这样的问题,在延时双删等策略中也存在这样的问题。针对可能出现的删除失败问题,目前业界主要有以下几种补偿机制。

1、删除重试机制

由于同步重试删除在性能上会影响吞吐量,所以常通过引入消息队列,将删除失败的缓存对应的 key 放入消息队列中,在对应的消费者中获取删除失败的 key ,异步重试删除。这种方法在实现上相对简单,但由于删除失败后的逻辑需要基于业务代码的 trigger 来触发 ,对业务代码具有一定入侵性。


鉴于上述方案对业务代码具有一定入侵性,所以需要一种更加优雅的解决方案,让缓存删除失败的补偿机制运行在背后,尽量少的耦合于业务代码。一个简单的思路是通过后台任务使用更新时间戳或者版本作为对比获取数据库的增量数据更新至缓存中,这种方式在小规模数据的场景可以起到一定作用,但其扩展性、稳定性都有所欠缺。

一个相对成熟的方案是基于 MySQL 数据库增量日志进行解析和消费,这里较为流行的是阿里巴巴开源的作为 MySQL binlog 增量获取和解析的组件 canal(类似的开源组件还有 Maxwell、Databus 等)。

canal sever 模拟 MySQL slave 的交互协议,伪装为 MySQL slave,向 MySQL master 发送 mp 协议,MySQL master 收到 mp 请求,开始推送 binary log 给 slave (即 canal sever ),canal sever 解析 binary log 对象(原始为 byte 流),可由 canal client 拉取进行消费,同时 canal server 也默认支持将变更记录投递到 MQ 系统中,主动推送给其他系统进行消费。

在 ack 机制的加持下,不管是推送还是拉取,都可以有效的保证数据按照预期被消费。当前版本的 canal 支持的 MQ 有 Kafka 或者 RocketMQ。另外, canal 依赖 ZooKeeper 作为分布式协调组件来实现 HA ,canal 的 HA 分为两个部分:


那么,针对缓存的删除操作便可以在 canal client 或 consumer 中编写相关业务代码来完成。这样,结合数据库日志增量解析消费的方案以及 Cache-Aside 模型,在读请求中未命中缓存时更新缓存(通常这里会涉及到复杂的业务逻辑),在写请求更新数据库后删除缓存,并基于日志增量解析来补偿数据库更新时可能的缓存删除失败问题,在绝大多数场景下,可以有效的保证缓存的最终一致性。

另外需要注意的是,还应该隔离事务与缓存,确保数据库入库后再进行缓存的删除操作。 比如考虑到数据库的主从架构,主从同步及读从写主的场景下,可能会造成读取到从库的旧数据后便更新了缓存,导致缓存落后于数据库的问题,这就要求对缓存的删除应该确保在数据库操作完成之后。所以,基于 binlog 增量日志进行数据同步的方案,可以通过选择解析从节点的 binlog,来避免主从同步下删除缓存过早的问题。

3、数据传输服务 DTS


3

Read-Through


Read-Through 意为读穿透模式,它的流程和 Cache-Aside 类似,不同点在于 Read-Through 中多了一个访问控制层,读请求只和该访问控制层进行交互,而背后缓存命中与否的逻辑则由访问控制层与数据源进行交互,业务层的实现会更加简洁,并且对于缓存层及持久化层交互的封装程度更高,更易于移植。


4

Write-Through


Write-Through 意为直写模式,对于 Write-Through 直写模式来说,它也增加了访问控制层来提供更高程度的封装。不同于 Cache-Aside 的是,Write-Through 直写模式在写请求更新数据库之后,并不会删除缓存,而是更新缓存。


这种方式的 优势在于读请求过程简单 ,不需要查询数据库更新缓存等操作。但其劣势也非常明显,除了上面我们提到的更新数据库再更新缓存的弊端之外,这种方案还会造成更新效率低,并且两个写操作任何一次写失败都会造成数据不一致。

如果要使用这种方案, 最好可以将这两个操作作为事务处理,可以同时失败或者同时成功,支持回滚,并且防止并发环境下的不一致 。另外,为了防止缓存扰动的频发,也可以给缓存增加 TTL 来缓解。

站在可行性的角度,不管是 Write-Through 模式还是 Cache-Aside 模式,理想状况下都可以通过分布式事务保证缓存层数据与持久化层数据的一致性,但在实际项目中,大多都对一致性的要求存在一些宽容度,所以在方案上往往有所折衷。

Write-Through 直写模式适合写操作较多,并且对一致性要求较高的场景,在应用 Write-Through 模式时,也需要通过一定的补偿机制来解决它的问题。首先,在并发环境下,我们前面提到了先更新数据库,再更新缓存会导致缓存和数据库的不一致,那么先更新缓存,再更新数据库呢?

这样的操作时序仍然会导致下面这样线程 1 先更新缓存,最后更新数据库的情况,即由于线程 1 和 线程 2 的执行不确定性导致数据库和缓存的不一致。这种由于线程竞争导致的缓存不一致,可以通过分布式锁解决,保证对缓存和数据库的操作仅能由同一个线程完成。对于没有拿到锁的线程,一是通过锁的 timeout 时间进行控制,二是将请求暂存在消息队列中顺序消费。


在下面这种并发执行场景下,来自线程 1 的写请求更新了数据库,接着来自线程 2 的读请求命中缓存,接着线程 1 才更新缓存,这样便会导致线程 2 读取到的缓存落后于数据库。同理,先更新缓存后更新数据库在写请求和读请求并发时,也会出现类似的问题。面对这种场景,我们也可以加锁解决。


另在,在 Write-Through 模式下,不管是先更新缓存还是先更新数据库,都存在更新缓存或者更新数据库失败的情况,上面提到的重试机制和补偿机制在这里也是奏效的。


5

Write-Behind


Write behind 意为异步回写模式,它也具有类似 Read-Through/Write-Through 的访问控制层,不同的是,Write behind 在处理写请求时,只更新缓存而不更新数据库,对于数据库的更新,则是通过批量异步更新的方式进行的,批量写入的时间点可以选在数据库负载较低的时间进行。

在 Write-Behind 模式下,写请求延迟较低,减轻了数据库的压力,具有较好的吞吐性。但数据库和缓存的一致性较弱,比如当更新的数据还未被写入数据库时,直接从数据库中查询数据是落后于缓存的。同时,缓存的负载较大,如果缓存宕机会导致数据丢失,所以需要做好缓存的高可用。显然,Write behind 模式下适合大量写操作的场景,常用于电商秒杀场景中库存的扣减。


6

Write-Around


如果一些非核心业务,对一致性的要求较弱,可以选择在 cache aside 读模式下增加一个缓存过期时间,在写请求中仅仅更新数据库,不做任何删除或更新缓存的操作,这样,缓存仅能通过过期时间失效。这种方案实现简单,但缓存中的数据和数据库数据一致性较差,往往会造成用户的体验较差,应慎重选择。


7

总结


在解决缓存一致性的过程中,有多种途径可以保证缓存的最终一致性,应该根据场景来设计合适的方案,读多写少的场景下,可以选择采用“Cache-Aside 结合消费数据库日志做补偿”的方案,写多的场景下,可以选择采用“Write-Through 结合分布式锁”的方案 ,写多的极端场景下,可以选择采用“Write-Behind”的方案。

⑷ 如何面试后端程序员

  • 计算机网络常见面试点总结

    计算机网络常见问题回顾

    2.1 TCP、UDP 协议的区别

    2.2 在浏览器中输入url地址 ->> 显示主页的过程

    2.3 各种协议与HTTP协议之间的关系

    2.4 HTTP长连接、短连接

    2.5 TCP 三次握手和四次挥手

  • 三 Linux

    3.1-简单介绍一下-linux-文件系统?

    3.2 一些常见的 Linux 命令了解吗?

  • 四 MySQL

    4.1 说说自己对于 MySQL 常见的两种存储引擎:MyISAM与InnoDB的理解

    4.2 数据库索引了解吗?

    4.3 对于大表的常见优化手段说一下

  • 五 Redis

    5.1 redis 简介

    5.2 为什么要用 redis /为什么要用缓存

    5.3 为什么要用 redis 而不用 map/guava 做缓存?

    5.4 redis 和 memcached 的区别

    5.5 redis 常见数据结构以及使用场景分析

    5.6 redis 设置过期时间

    5.7 redis 内存淘汰机制

    5.8 redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)

    5.9 缓存雪崩和缓存穿透问题解决方案

    5.10 如何解决 Redis 的并发竞争 Key 问题

    5.11 如何保证缓存与数据库双写时的数据一致性?

  • 六 Java

    6.1 Java 基础知识

    6.2 Java 集合框架

    6.3 Java多线程

    6.4 Java虚拟机

    6.5 设计模式

  • 七 数据结构

  • 八 算法

    8.1 举个栗子(手写快排)

  • 九 Spring

    9.1 Spring Bean 的作用域

    9.2 Spring 事务中的隔离级别

    9.3 Spring 事务中的事务传播行为

    9.4 AOP

    9.5 IOC

不需要写代码就能衡量候选人的方法可能有一万种。我常用的三个主要方法可以覆盖许多不同的技能。在面试过程中,我们会谈论候选人的经验,要求他们做一些代码审查,并与别人合作设计一个系统。

下面我会详细解释这个过程。

我试图通过这些方法找到真正能够胜任技术工作的候选人,并且他们必须能在单纯的编程技能之外给团队带来价值。通常在一次面试中我能在大约一个小时内覆盖所有三个部分。我有信心这些信息能让我找到好的候选人。

1、深入挖掘他们的经验

许多团队已经这样做了。他们会在面试一开始花几分钟,询问候选人之前的工作,他们对工作的态度,等等。大多时候这就像随意谈话一样。

但这是不对的。

记住这是面试。你需要尽可能地理解他们构建系统时使用的技术。

为了做好这一点,你需要在面试开始之前仔细阅读他们的简历。这不是开玩笑,在面试开始之前至少花上10分钟仔细阅读(不是略读)简历,如果花30分钟时间则最好。要从简历中尽可能多了解些他们之前的项目,Google一下看看能否找到他们项目的公开信息。面试时挖掘背景信息所花的时间越少,就越能获得好的效果。

在面试中,要求候选人谈谈他最近最感兴趣的项目。要练习主动的倾听,要学会参与。假装你是他团队中的一员,或者假装你们是在做架构审查。你要努力了解他们构建的东西以及构建的方法。这样做的好处和坏处是什么?要让候选人知道,不知道答案无所谓,但重要的是能勾起你的好奇心。

下面是我认为能获得好的答案的问题:

  • 你在项目中的职责是什么?这个问题本身并不是决定性的。即使在项目中承担的职责很小,他们也可能很适合你们的团队。你的候选人也许正是因为没能获得重要的职责而在寻找新的机会。因此,知道他们过去的职责会很有帮助。

  • 你从他人那里获得了什么帮助?无法感受他人的帮助是个极其危险的信号。即使是个人项目,也一定需要别人的帮忙。你肯定不想要一个以自我为中心的同事。

  • 给我介绍下那个功能的工作原理。解释下数据的来源和去向、存储方式以及这一切能带给最终用户的好处。这个问题的答案足以吸引你的好奇心。

  • 这个项目中最糟糕的技术债务是什么?好的工程师必须理解他们做出决定时需要付出的代价。问完这个问题,可以继续询问他们怎样改正这些问题,或者尚未改正的理由。

  • 有没有出过生产环境下的bug或服务中断?测试下他们是否理解bug的原因,以及团队解决bug的方法。他们是否提前预期到了bug?下次怎样才能避免同样的问题发生?

  • 这一部分面试能让你直接了解候选人的经验。做好这一部分还能让你了解他们如何感谢别人或责备别人。你将会了解到他们如何在两难的工程问题上做出抉择,他们会与你分享最近的教训,他们与别人沟通技术的能力应该也很明显。

    如果他们选择了不太适合的项目,可以考虑谈论其他项目。所谓不太适合的意思是项目不够复杂或他们记不清的情况。

    注意,这一步要避免询问类似于“告诉我你解决过的最难的bug”之类的问题。要求别人回忆系统的某一部分的具体原理会带来大量的虚假负面判断。人们不可能拥有他们修复的bug相关的一切知识,这种问题会给面试过程带来很大压力。

    2、让他们审查你们的代码

    这项活动一半是代码审查一半是角色扮演。你可以借此筛选出那些能够提升团队整体代码质量并促进办公室氛围的人。

    下面是代码审查过程中需要关注的一些方面:

  • 他们怎样与代码的“作者”交流?交流是否有用?是否高效?是否友善?

  • 他们会着重哪些问题?是否能明确表达出他们的疑问?他们是否会立即指出哪些无关紧要的问题?

  • 他们是否善于阅读自己不熟悉的代码?

  • 这个方法需要提前准备很多东西。你需要找到或编写一段代码供候选人审查。你还需要为你希望候选人找出的问题创建一个优先级列表。不要让面试管当场出题,一定要事先准备好。

    在选择需要审查的代码时,不要选择产品代码。你的候选人没有你所拥有的背景知识,这样做实际上是将候选人与你的同事比较,而不是与其他候选人比较。

    努力降低代码示例中的复杂度。面试的时候,候选人没有太多时间阅读代码,而且很可能他们并没有想到会做代码审查。热身就要花很长时间。

    在代码中加入一两个真实的bug,但不要强调找bug。一般来说,代码审查并不是个好的找bug方法,特别是审查者从来没有见过代码的情况下。能自证的bug(如给需要数组的函数传递字符串)最好。在你的优先级列表中,bug的优先级应该是最低的,bug应该是给极其优秀的人的加分项。

    最后,代码应该做一些实际的事情。如果你的公司很出名,那可以选择你的产品简化版本。但如果你需要花大量时间为候选人提供背景信息的话还是算了。

    最好的选择要么是虚构的代码(也许可以选择本文竭力避免的代码面试中用到的代码),要么是开源代码中的一个拉取请求。

    一旦决定了要审查的代码,你应该期待候选人找出下面这些东西:

  • 过于糟糕的拉取请求的描述或提交信息;

  • 能用但无法自洽的代码;

  • 过于复杂的代码(需要重构的代码);

  • 混乱的变量或方法名;

  • 过度设计的代码(即实际上永远不会用到的功能)。

  • 如果代码中没有足够的问题,就多加一些。

    这里有个潜在的问题,我还没有确定的答案。这个问题是:你是否应该提前将代码发给候选人?

    如果你这样做,就又给那些有空闲时间的人以巨大的优势。如果不这样做,就要面临增加面试压力的风险。

    我倾向于后者。好的面试官可以减轻压力,方法之一就是让面试者提前知道他们将做代码审查,你也可以在审查开始之前介绍你的期望。

⑸ 如何保证缓存与数据库双写时的数据一致性

一般来说,就是如果系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

⑹ Redis 如何保持和 MySQL 数据一致

redis在启动之后,从数据库加载数据。

读请求:

不要求强一致性的读请求,走redis,要求强一致性的直接从mysql读取

写请求:

数据首先都写到数据库,之后更新redis(先写redis再写mysql,如果写入失败事务回滚会造成redis中存在脏数据)

在并发不高的情况下,读操作优先读取redis,不存在的话就去访问MySQL,并把读到的数据写回Redis中;写操作的话,直接写MySQL,成功后再写入Redis(可以在MySQL端定义CRUD触发器,在触发CRUD操作后写数据到Redis,也可以在Redis端解析binlog,再做相应的操作)

在并发高的情况下,读操作和上面一样,写操作是异步写,写入Redis后直接返回,然后定期写入MySQL

1.当更新数据时,如更新某商品的库存,当前商品的库存是100,现在要更新为99,先更新数据库更改成99,然后删除缓存,发现删除缓存失败了,这意味着数据库存的是99,而缓存是100,这导致数据库和缓存不一致。

解决方法:

这种情况应该是先删除缓存,然后在更新数据库,如果删除缓存失败,那就不要更新数据库,如果说删除缓存成功,而更新数据库失败,那查询的时候只是从数据库里查了旧的数据而已,这样就能保持数据库与缓存的一致性。

2.在高并发的情况下,如果当删除完缓存的时候,这时去更新数据库,但还没有更新完,另外一个请求来查询数据,发现缓存里没有,就去数据库里查,还是以上面商品库存为例,如果数据库中产品的库存是100,那么查询到的库存是100,然后插入缓存,插入完缓存后,原来那个更新数据库的线程把数据库更新为了99,导致数据库与缓存不一致的情况

解决方法:

遇到这种情况,可以用队列的去解决这个问,创建几个队列,如20个,根据商品的ID去做hash值,然后对队列个数取摸,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除,如果在更新的过程中,遇到以上场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同商品ID在做更新,如果有也把查询的请求发送到队列里去,然后同步等待缓存更新完成。

这里有一个优化点,如果发现队列里有一个查询请求了,那么就不要放新的查询操作进去了,用一个while(true)循环去查询缓存,循环个200MS左右,如果缓存里还没有则直接取数据库的旧数据,一般情况下是可以取到的。

1、读请求时长阻塞

由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时间内返回,该解决方案最大的风险在于可能数据更新很频繁,导致队列中挤压了大量的更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库,像遇到这种情况,一般要做好足够的压力测试,如果压力过大,需要根据实际情况添加机器。

2、请求并发量过高

这里还是要做好压力测试,多模拟真实场景,并发量在最高的时候QPS多少,扛不住就要多加机器,还有就是做好读写比例是多少

3、多服务实例部署的请求路由

可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过nginx服务器路由到相同的服务实例上

4、热点商品的路由问题,导致请求的倾斜

某些商品的读请求特别高,全部打到了相同的机器的相同丢列里了,可能造成某台服务器压力过大,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是很大,但是确实有可能某些服务器的负载会高一些。

img

搜索微信号(ID:芋道源码),可以获得各种 Java 源码解析。

并且,回复【书籍】后,可以领取笔者推荐的各种 Java 从入门到架构的书籍。

⑺ Cache缓存,怎么实现与数据库同步

使用SQLDependency缓存依赖,以下是一个推SQL缓存依赖的例子,当数据库更新后缓存会自动更新
void Page_Load()
{
DataTable movies=(DataTable)Cache["Movie"];
if(movie=null)
{
SqlDataAdapter adpter=new SqlDataAdatper("Select * From Movie",sqlConnection);
SqlCacheDependency sqlDepend=new SqlCacheDependency(adapter.SelectCommand);
movies=new DataTable();
//注意必须在adpter.Fill()前先建立SqlCacheDependency,否则无效
adpter.Fill(movies);
Cache.Insert("Movie",movies,sqlDepend);
}
}

⑻ Java中高并发下怎么保证数据一致性

以mysql来说,可能出现脏读、不可重复读以及幻读,mysql默认设置是可重复读,即一次事务中不会读取到不同的数据。
可以做如下操作:
1)打开两个客户端,均设置为RR;
2)在一个事务中,查询某个操作查到某份数据;比如是某个字段version=1存在数据;
3)在另一个事务中,删除这份version=1的数据;删除后,在2所属的事务中查询数据是没有变化的,还是存在version=1的数据;
4)当我们在2所属的事务中继续更新数据,那么会发现更新不了,明明我们就看到了这份version=1的数据;
缓存一致性:
缓存一致,与什么一致?是与数据库一致,对外查询每个时刻一致;所以在针对于缓存与数据库之间该先更新哪一个呢?可能有人觉得我先更新数据库,再更新缓存不就行了吗?但是有想过个问题吗?
当用户已经支付成功了,更新到数据库,但是呢?你还在缓存中显示未支付,在用户点击频率很高并且数据库压力过大,来不及同步到缓存时,那你是不是很尴尬,这就是典型的不一致了。此时用户再支付,那你又告诉他已经支付了,那他会把你骂死的
那该怎么来做呢?我们可以这样,先更新缓存再更新数据库,那么存在什么问题呢?
1)缓存更新成功,但是数据库更新失败,而被其它的并发线程访问到
2)缓存淘汰成功,但是数据库更新失败,这也会引发后期数据不一致

⑼ java多线程下如何保证数据的一致性

以mysql来说,可能出现脏读、不可重复读以及幻读,mysql默认设置是可重复读,即一次事务中不会读取到不同的数据。
可以做如下操作:
1)打开两个客户端,均设置为RR;
2)在一个事务中,查询某个操作查到某份数据;比如是某个字段version=1存在数据;
3)在另一个事务中,删除这份version=1的数据;删除后,在2所属的事务中查询数据是没有变化的,还是存在version=1的数据;
4)当我们在2所属的事务中继续更新数据,那么会发现更新不了,明明我们就看到了这份version=1的数据;
缓存一致性:
缓存一致,与什么一致?是与数据库一致,对外查询每个时刻一致;所以在针对于缓存与数据库之间该先更新哪一个呢?可能有人觉得我先更新数据库,再更新缓存不就行了吗?但是有想过个问题吗?
当用户已经支付成功了,更新到数据库,但是呢?你还在缓存中显示未支付,在用户点击频率很高并且数据库压力过大,来不及同步到缓存时,那你是不是很尴尬,这就是典型的不一致了。此时用户再支付,那你又告诉他已经支付了,那他会把你骂死的
那该怎么来做呢?我们可以这样,先更新缓存再更新数据库,那么存在什么问题呢?
1)缓存更新成功,但是数据库更新失败,而被其它的并发线程访问到
2)缓存淘汰成功,但是数据库更新失败,这也会引发后期数据不一致