- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章2021年最新Redis面试题汇总(4)由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
1)加锁 。
加锁通常使用 set 命令来实现,伪代码如下:
set key value px milliseconds nx 。
几个参数的意义如下:
key、value:键值对 。
px milliseconds:设置键的过期时间为 milliseconds 毫秒.
nx:只在键不存在时,才对键进行设置操作。set key value nx 效果等同于 setnx key value.
px、expiretime 参数则是用于解决没有解锁导致的死锁问题。因为如果没有过期时间,万一程序员写的代码有 bug 导致没有解锁操作,则就出现了死锁,因此该参数起到了一个“兜底”的作用.
nx 参数用于保证在多个线程并发 set 下,只会有1个线程成功,起到了锁的“唯一”性.
2)解锁 。
解锁需要两步操作:
1)查询当前“锁”是否还是我们持有,因为存在过期时间,所以可能等你想解锁的时候,“锁”已经到期,然后被其他线程获取了,所以我们在解锁前需要先判断自己是否还持有“锁” 。
2)如果“锁”还是我们持有,则执行解锁操作,也就是删除该键值对,并返回成功;否则,直接返回失败.
由于当前 redis 还没有原子命令直接支持这两步操作,所以当前通常是使用 lua 脚本来执行解锁操作,redis 会保证脚本里的内容执行是一个原子操作.
脚本代码如下,逻辑比较简单:
1
2
3
4
5
6
|
if
redis.call(
"get"
,keys[
1
]) == argv[
1
]
then
return
redis.call(
"del"
,keys[
1
])
else
return
0
end
|
两个参数的意义如下:
keys[1]:我们要解锁的 key 。
argv[1]:我们加锁时的 value,用于判断当“锁”是否还是我们持有,如果被其他线程持有了,value 就会发生变化.
上述方法是 redis 当前实现分布式锁的主流方法,可能会有一些小优区别,但是核心都是这个思路。看着好像没啥毛病,但是真的是这个样子吗?让我们继续往下看.
为了防止死锁,我们会给分布式锁加一个过期时间,但是万一这个时间到了,我们业务逻辑还没处理完,怎么办?
首先,我们在设置过期时间时要结合业务场景去考虑,尽量设置一个比较合理的值,就是理论上正常处理的话,在这个过期时间内是一定能处理完毕的.
之后,我们再来考虑对这个问题进行兜底设计.
关于这个问题,目前常见的解决方法有两种:
同时,需要进行告警,人为介入验证数据的正确性,然后找出超时原因,是否需要对超时时间进行优化等等.
redisson 使用看门狗(守护线程)“续命”的方案在大多数场景下是挺不错的,也被广泛应用于生产环境,但是在极端情况下还是会存在问题.
问题例子如下:
解决方法:上述问题的根本原因主要是由于 redis 异步复制带来的数据不一致问题导致的,因此解决的方向就是保证数据的一致.
当前比较主流的解法和思路有两种:
1)redis 作者提出的 redlock; 。
2)zookeeper 实现的分布式锁.
首先,该方案也是基于文章开头的那个方案(set加锁、lua脚本解锁)进行改良的,所以 antirez 只描述了差异的地方,大致方案如下.
假设我们有 n 个 redis 主节点,例如 n = 5,这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,为了取到锁,客户端应该执行以下操作
可以看出,该方案为了解决数据不一致的问题,直接舍弃了异步复制,只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 n 个节点,官方建议是 5.
该方案看着挺美好的,但是实际上我所了解到的在实际生产上应用的不多,主要有两个原因:1)该方案的成本似乎有点高,需要使用5个实例;2)该方案一样存在问题.
该方案主要存以下问题:
针对以上问题其实后续也有人给出一些相应的解法,但是整体上来看还是不够完美,所以目前实际应用得不是那么多.
1)先操作数据库 。
案例如下,有两个并发的请求,一个写请求,一个读请求,流程如下:
可能存在的脏数据时间范围:更新数据库后,失效缓存前。这个时间范围很小,通常不会超过几毫秒.
2)先操作缓存 。
案例如下,有两个并发的请求,一个写请求,一个读请求,流程如下:
可能存在的脏数据时间范围:更新数据库后,下一次对该数据的更新前。这个时间范围不确定性很大,情况如下:
结论:通过上述案例可以看出,先操作数据库和先操作缓存都会存在脏数据的情况。但是相比之下,先操作数据库,再操作缓存是更优的方式,即使在并发极端情况下,也只会出现很小量的脏数据.
1)更新缓存 。
案例如下,有两个并发的写请求,流程如下:
分析:数据库中的数据是请求b的,缓存中的数据是请求a的,数据库和缓存存在数据不一致.
2)失效(删除)缓存 。
案例如下,有两个并发的写请求,流程如下:
分析:由于是删除缓存,所以不存在数据不一致的情况.
结论:通过上述案例,可以很明显的看出,失效缓存是更优的方式.
在上文的案例中,无论是先操作数据库,还是先操作缓存,都会存在脏数据的情况,有办法避免吗?
答案是有的,由于数据库和缓存是两个不同的数据源,要保证其数据一致性,其实就是典型的分布式事务场景,可以引入分布式事务来解决,常见的有:2pc、tcc、mq事务消息等.
但是引入分布式事务必然会带来性能上的影响,这与我们当初引入缓存来提升性能的目的是相违背的.
所以在实际使用中,通常不会去保证缓存和数据库的强一致性,而是做出一定的牺牲,保证两者数据的最终一致性.
如果是实在无法接受脏数据的场景,则比较合理的方式是放弃使用缓存,直接走数据库.
保证数据库和缓存数据最终一致性的常用方案如下:
1)更新数据库,数据库产生 binlog.
2)监听和消费 binlog,执行失效缓存操作.
3)如果步骤2失效缓存失败,则引入重试机制,将失败的数据通过mq方式进行重试,同时考虑是否需要引入幂等机制.
兜底:当出现未知的问题时,及时告警通知,人为介入处理.
人为介入是终极大法,那些外表看着光鲜艳丽的应用,其背后大多有一群苦逼的程序员,在不断的修复各种脏数据和bug.
描述:访问一个缓存和数据库都不存在的 key,此时会直接打到数据库上,并且查不到数据,没法写缓存,所以下一次同样会打到数据库上.
此时,缓存起不到作用,请求每次都会走到数据库,流量大时数据库可能会被打挂。此时缓存就好像被“穿透”了一样,起不到任何作用.
解决方案:
1)接口校验。在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的id是正整数,则可以直接对非正整数直接过滤等等.
2)缓存空值。当访问缓存和db都没有查询到值时,可以将空值写进缓存,但是设置较短的过期时间,该时间需要根据产品业务特性来设置.
3)布隆过滤器。使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库.
布隆过滤器的特点是判断不存在的,则一定不存在;判断存在的,大概率存在,但也有小概率不存在。并且这个概率是可控的,我们可以让这个概率变小或者变高,取决于用户本身的需求.
布隆过滤器由一个 bitset 和 一组 hash 函数(算法)组成,是一种空间效率极高的概率型算法和数据结构,主要用来判断一个元素是否在集合中存在.
在初始化时,bitset 的每一位被初始化为0,同时会定义 hash 函数,例如有3组 hash 函数:hash1、hash2、hash3.
写入流程 。
当我们要写入一个值时,过程如下,以“jionghui”为例:
1)首先将“jionghui”跟3组 hash 函数分别计算,得到 bitset 的下标为:1、7、10.
2)将 bitset 的这3个下标标记为1.
假设我们还有另外两个值:java 和 diaosi,按上面的流程跟 3组 hash 函数分别计算,结果如下:
java:hash 函数计算 bitset 下标为:1、7、11 。
diaosi:hash 函数计算 bitset 下标为:4、10、11 。
查询流程 。
当我们要查询一个值时,过程如下,同样以“jionghui”为例::
1)首先将“jionghui”跟3组 hash 函数分别计算,得到 bitset 的下标为:1、7、10.
2)查看 bitset 的这3个下标是否都为1,如果这3个下标不都为1,则说明该值必然不存在,如果这3个下标都为1,则只能说明可能存在,并不能说明一定存在.
其实上图的例子已经说明了这个问题了,当我们只有值“jionghui”和“diaosi”时,bitset 下标为1的有:1、4、7、10、11.
当我们又加入值“java”时,bitset 下标为1的还是这5个,所以当 bitset 下标为1的为:1、4、7、10、11 时,我们无法判断值“java”存不存在.
其根本原因是,不同的值在跟 hash 函数计算后,可能会得到相同的下标,所以某个值的标记位,可能会被其他值给标上了.
这也是为啥布隆过滤器只能判断某个值可能存在,无法判断必然存在的原因。但是反过来,如果该值根据 hash 函数计算的标记位没有全部都为1,那么则说明必然不存在,这个是肯定的.
降低这种误判率的思路也比较简单:
布隆过滤器的误判率还有专门的推导公式,有兴趣的可以去搜相关的文章和论文查看.
描述:某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库.
解决方案:
1)加互斥锁。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存.
关于互斥锁的选择,网上看到的大部分文章都是选择 redis 分布式锁(可以参考我之前的文章:面试必问的分布式锁,你懂了吗?),因为这个可以保证只有一个请求会走到数据库,这是一种思路.
但是其实仔细想想的话,这边其实没有必要保证只有一个请求走到数据库,只要保证走到数据库的请求能大大降低即可,所以还有另一个思路是 jvm 锁.
jvm 锁保证了在单台服务器上只有一个请求走到数据库,通常来说已经足够保证数据库的压力大大降低,同时在性能上比分布式锁更好.
需要注意的是,无论是使用“分布式锁”,还是“jvm 锁”,加锁时要按 key 维度去加锁.
我看网上很多文章都是使用一个“固定的 key”加锁,这样会导致不同的 key 之间也会互相阻塞,造成性能严重损耗.
使用 redis 分布式锁的伪代码,仅供参考:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public
object getdata(string key)
throws
interruptedexception {
object value = redis.get(key);
// 缓存值过期
if
(value ==
null
) {
// lockredis:专门用于加锁的redis;
// "empty":加锁的值随便设置都可以
if
(lockredis.set(key,
"empty"
,
"px"
, lockexpire,
"nx"
)) {
try
{
// 查询数据库,并写到缓存,让其他线程可以直接走缓存
value = getdatafromdb(key);
redis.set(key, value,
"px"
, expire);
}
catch
(exception e) {
// 异常处理
}
finally
{
// 释放锁
lockredis.delete(key);
}
}
else
{
// sleep50ms后,进行重试
thread.sleep(
50
);
return
getdata(key);
}
}
return
value;
}
|
2)热点数据不过期。直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存.
这种方式适用于比较极端的场景,例如流量特别特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还有就是异常情况的处理,不要到时候缓存刷新不上,一直是脏数据,那就凉了.
描述:大量的热点 key 设置了相同的过期时间,导在缓存在同一时刻全部失效,造成瞬时数据库请求量大、压力骤增,引起雪崩,甚至导致数据库被打挂.
缓存雪崩其实有点像“升级版的缓存击穿”,缓存击穿是一个热点 key,缓存雪崩是一组热点 key.
解决方案:
1)过期时间打散。既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效.
2)热点数据不过期。该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况.
3)加互斥锁。该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可.
本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注我的更多内容! 。
原文链接:https://blog.csdn.net/weixin_39709134/article/details/116998066 。
最后此篇关于2021年最新Redis面试题汇总(4)的文章就讲到这里了,如果你想了解更多关于2021年最新Redis面试题汇总(4)的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我怎样才能将 numberGrade 的值调高,如果它是 89.5,它会变成 90。numberGrade 被当作 double ,但将它设为 int 并不会向上或向下舍入。 public class
经过了漫长时间的移植和查询资料,得以解决一下嵌入式docker出现的问题,很多网上的资料全都是复制粘贴复制粘贴,找不到合适的解决方法让人很是苦恼,希望自己总结出的一些解决问题的经验给广大朋友减少一些
之前我是通过脚本来使用库的: 现在我使用 yarn 和 rollup 来构建带有下一个文件的项目。包.json: { "name": "maplib", "version": "1.0.0",
在 R 中,我正在尝试使用不同的窗口宽度对大向量(最多 400k 个元素)进行非常快速的滚动平均值,然后对于每个窗口宽度按每年的最大值汇总数据。下面的例子希望是清楚的。 我尝试了几种方法,到目前为止最
我想问一下我应该如何解决这个问题,因为我已经对这部分感到困惑和困惑。我已经使用这个命令全局安装了汇总 npm install --global rollup 但是,当我尝试运行“汇总”命令时,我应该期
我正在构建 javascript 库(更像是小部件),其中将包含一些 UI。我正在通过 javascript 向 DOM 添加 HTML 元素。要添加此 HTML,我有以下代码: async inse
我在显示一份报告时遇到了一些困难,该报告既可以将所有日期分组到月中,又可以对月中每天的“支出”求和。 我的 SQL 查询创建了这个:(检索所有日期) Date
我正在从事 Angular2 项目。我浏览了 Angular2 aot 文档并且能够生成 ngFactory 文件。我按照文档中的建议使用了 rollup js。我有一些非 es6 npm 包。我已经
我目前正在构建 Ionic 2-RC3 应用程序。但是自从我升级到 RC-1 后,我遇到了以下错误:(不确定它们是否保持不变,但你明白了)。 [15:16:17] rollup: Conflicti
Arabic, Egypt (ar_EG) -----------------------------阿拉伯语,埃及 Arabic, Israel (ar_IL) -----------------
我正在尝试汇总我的完全 es6 模块存储库,该存储库具有项目的本地导入/导出,以及对也是脚本或模块的依赖项的导入。 我也在尝试进行双重构建,通过汇总创建遗留的 iife 模块。 这仅适用于我的项目,没
我有一个由 DayTots 类对象组成的 VBA 集合(见下文) 我正在使用 For Each 遍历集合以创建一个 由汇总记录组成的新集合,基于日期 有什么方法可以用 Linq 做到这一点吗?我怀疑也
这是我第一次尝试理解/使用汇总。 我正在使用 this boilerplate因为它都是基于three.js,我也喜欢使用它。 到目前为止,我目前的(几乎肯定是不正确的)方法是: 从github下载样
我有两个 column_property 列,我想在 grandtotal 列中将它们加在一起。我希望能够根据 grandtotal 列进行排序和过滤。 如何对 subtotal 和 shipping
我收到以下错误消息: Error: Parse Error: Line 29: Unexpected token ILLEGAL 对应的代码行是 mobx 观察者装饰器: @observer clas
我真的坚持这一点,我真的很感激这方面的任何帮助。 目标是计算 Woocommerce 订单上每个类别中的项目数量,以便每个部分都可以以类别名称和产品数量为标题。例如: 汉堡 x 5 在此下方将是该订单
我正在从路由器收集传输数据;它提供每日,每月和每两分钟(间隔为120秒)的摘要。如果我在一天中(因此一个月中)重启路由器,则这些报告将不完整。但是,我仍然会得到间隔数据,并且可以对引导前后的记录进行汇
假设我有一个像这样的数据框: a b c d e f 1. 1 5 5 9 2 3 2. 4 7 3 1 4 6 3. 2 3 8 9
假设我有一个记录列表,我想通过取中位数来总结它。更具体地说,说我有 data Location = Location { x :: Double, y :: Double } 我有一个测量列表,我想将
我刚刚开始使用 AngularJS。我需要从 AngularJS 的书中升级这个购物车示例,以便所有 (items.price*item.quantity) 的总数显示在页面底部。实现它的推荐方法是什
我是一名优秀的程序员,十分优秀!