返回目录:范文示例
今天小编给各位分享网络管理员面试题的知识,文中也会对其通过含泪整理Redis相关面试题大全和大数据分析师面试题:Redis的耐久化战略等多篇文章进行知识讲解,如果文章内容对您有帮助,别忘了关注本站,现在进入正文!
内容导航:
一、含泪整理Redis相关面试题大全
1、什么是redis?redis有哪些优缺点?redis是一个C语言编写的开源的高性能NOSQL键值对数据库吗,支持5种数据类型:字符串,列表,集合,散列表,有序集合。与传统数据库不一样,Redis数据存储在内存中,读写速度非常快,redis被广泛应用于缓存,每秒可处理超过10w次的读写操作。优点 读写性能优异,读11w次/s, 写8w次/s 支持事务,redis所有操作都是原子性的,还支持几个操作合并也是原子性的
2、Redis有哪些应用场景?1、DB缓存,减轻服务器压力2、提高系统响应3、做Session分离4、做分布式锁 使用setNX5、做乐观锁 Redis的watch+incr
3、为什么要用redis而不用map/guava?本地缓存和分布式缓存的原因
4、说一说缓存的读写模式?1、Cache Aside Pattern(旁路缓存,常用)是最经典的缓存+数据库读写模式。先读缓存,没有则读数据库,去除数据放缓存,同时返回响应。更新的时候,先更新数据库,再删除缓存。为什么 是删除缓存,不是更新缓存呢? 1、缓存的值是一个结构,hash,list,更新需要遍历 2、懒加载,使用的时候才更新缓存(亦可以异步填充)高并发脏读的三种情况 1、先更新数据库,再更新缓存 2、先删除缓存,再更新数据库 3、先更新数据库,再删除缓存(推荐)2、Read/Write Thread Pattern应用只操作缓存,缓存操作数据库ReadThread:应用程序读缓存,缓存没有,由缓存回源数据库,并写入缓存WriteThread:应用程序写缓存,缓存写数据库,比较复杂3、Write Behind Cachiing Parttern应用程序只更新缓存,缓存通过异步将数据批量更新到数据库,不能实时同步,甚至会丢数据
5、Redis为什么是单线程,高并发这么快是为什么呢?1、高并发快的原因 1、redis是基于内存的,内存的读写速度非常快 2、redis是单线程的,省去了很多上下文切换线程的时间 3、redis使用IO多路复用技术(epoll),可以处理并发的连接。多路是指多个网络连接,复用是复用同一个线程。可以让单线程高效处理多个连接请求 4、数据结构为key-value,读取数据快。比如还有压缩表,跳跃表等加快数据读取 5、redis有使用自己的事件分离器,效率比较高,内部采用非阻塞方式,吞吐能力比较大2、为什么是单线程 因为redis是基于内存,CPU不是redis瓶颈,瓶颈可能是机器内存或者网络带宽,既然单线程容易实现且CPU不能成为瓶颈,就顺利采用单线程方式3、单线程的优势 代码清晰,逻辑简单,不用考虑各种锁问题,不需要上下文切换导致消耗CPU
6、说一说Redis有哪些数据类型?1、String类型 可存储字符串,整数,浮点型,数字可以自增自减 2、List类型 是一个双向列表,可以从两端压入或弹出,存储一些列表的数据结构 3、SET类型(无序集合) 用于一些不重复并且不需要顺序的数据结构 4、Hash类型(散列表)5、ZSET类型(有序集合)
7、说一说Redis的RDB持久化和AOF持久化?7.1、简单介绍1、RDB持久化:可以指定的时间间隔能对数据进行快照存储,然后写入内存,以便在REDIS重启时,可以通过RDB还原数据库2、AOF持久化:记录每次对服务器写的操作命令(RESP),当服务器重启时会重新执行这些命令来恢复数据,AOF每次写操作以append方式追加在文件末尾,redis还能在后台对AOF重写,使得AOF达到瘦身的效果3、如果redis开启了AOF,优先使用AOF,只有AOF关闭,才会使用RDB
7.2、RDB持久化7.2.1、RDB文件格式(可用winhex打开)RDB文件是一个经过压缩的二进制文件(默认名:dump.rdb),由多个部分组成,RDB格式为: “REDIS”| RDB_VERSION | AUX_FIELD_KEY_VALUE_PAIRS | DB_NUM | DB_DICT_SIZE | EXPIRE_DICT_SIZE | KEY_VALUE_PAIRS | EOF | CHECK_NUM
1、头部5字节固定为“REDIS”字符串2、4个字节的RDB版本号(不是REDIS的版本号),当前为9,填充为00093、辅助字段,以KEY_VALUE形式,比如:redis-ver(redis版本), redis-bits(64/32),ctime(当期时间戳),used-mem(使用内存)4、存储数据库号码5、字典大小6、过期Key7、主要数据,以key-value形式存储8、结果标志9、校验和,就是看文件是否损坏或被修改
7.2.2、RDB触发方式1、配置参数定期执行 save "" # 不使用RDB存储, 不能主从 save 900 1 # 表示15分钟至少有1个键被更改则进行快照 save 300 10 # 表示5分钟至少有10个键被更改则进行快照 save 60 10000 # 表示1分钟至少有10000个键被更改则进行快照2、命令显式触发 bgsave
7.2.3、RDB执行流程1、流程图
2、流程说明
1、比如使用bgsave触发,Redis父进程首先判断,当前是否执行save,或者bgsave/bgwriteaof(aof重写命令)的紫禁城,如果在执行则bgsave命令直接返回2、父进程执行fork创建子进程,这个过程父进程是阻塞的,redis不能执行来自客户端的命令3、父进程fork后,bgsave命令返回“background saving started”,并可以响应其他命令4、子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换(RDB始终完整)5、子进程发送信号给父进程表示完成,父进程更新统计信息
7.3、AOF持久化7.3.1、AOF原理AOF存储的是redis命令,同步命令到AOF文件整个分为三个阶段:命令传播,缓存追加,文件保存和写入
1、命令传播:通过网络将协议文本发送给Redis,服务器接受后,根据协议内容,选择适当的函数,将参数从字符串文本转换为stringObject,然后命令参数传播到AOF程序2、缓存追加:AOF根据命令以及命令参数,将命令从字符串转换为原来的协议文本,然后追加到redis.h/redisServer的aof_buf末尾3、文件写入和保存:服务器会调用flushAppendOnlyFile函数,条件是WRITE时将aof_buf中的缓存写入AOF文件,条件是SAVE时,调用fsync或fdatasync,将AOF保存到磁盘
7.3.2、AOF保存模式1、AOF_FSYNC_NO:不保存 每次调用flushAppendOnlyFile, WRITE会被执行,SAVE会被略过 2、AOF_FSYNC_EVERTSEC:每秒钟保存一次(默认) 这种模式下,SAVE原则上每隔1s钟就会执行一次,因为SAVE是后台子线程,所以不会引起服务器主进程阻塞 3、AOF_FSYNC_ALWAYS:每执行一个命令保存一次(不推荐) 每执行一个命令之后,WRITE和SAVE都会被执行。SAVE是由主进程执行,主进程被阻塞,不能接受请求
7.3.3、AOF重写AOF记录数据越多,体积就会越大,需要重写瘦身。Redis可以自动在后台队AOF进行重写,重写后包含回复当前数据集所需要的最小命令集合子进程重写期间,主进程还需要继续处理命令,新的命令可能对现有数据进行修改,Redis就增加了AOF重写缓存。fork出子进程后,redis主进程接受命令后,除了将写命令追加到现有的AOF,还会追加到AOF重写缓存中。子进程重写完毕后,会通知主进程,会把AOF重写缓存的内容全部写入新的AOF中
7.3.4、AOF配置(命令bgwriteaof)appendonly yes # 开启aof auto-aof-rewrite-percentage 100 # 表示aof超过上一次aof的百分之多少会进行重写auto-aof-rewrite-min-size 64mb # 限制允许重写的aof文件大小,也就是小于64mb时,不需要优化
7.3.5、混合持久化redis4.0开始支持rdb和aof混合持久化,rdb头+aof身体--》appendonly.aof开启混合持久化 aof-use-rdb-preamble yes
7.4、持久化方式总结与抉择7.4.1、RDB和AOF对比1、RDB是某个时刻的快照数据,使用二进制存储,AOF存操作命令,采用文本存储(混合)2、RDB性能高,AOF性能比较低3、RDB会丢失最后一次快照以后更改的所有数据,AOF设置每秒保存一次,则最多丢2秒的数据4、Redis主服务器模式运行,RDB不会保存过期key,RDB以从服务器运行,会保存过期key,主从同步时,再清空过期key。AOF写入时,过期key会追加一条del命令,执行aof重写时,会忽略过期key和del命令
7.4.2、如何选择内存数据库:rdb+aof 数据不容易丢失缓存服务器:rdb 高性能, 不建议只使用aof(性能差)追求高性能:可以都不开,redis宕机,从数据源恢复字典库:可以选择不驱逐,保证数据完整性
8、Redis过期键删除有哪些策略?redis性能高,官方表示读11w次/s,写81000次/秒,长期使用key不断增加,redis作为缓存,物理内存也会满,内存与磁盘交换虚拟内存,频繁IO导致性能急剧下降.
maxmemory 默认不设置,一般设置物理内存3/4,趋近maxmemory后,会通过缓存淘汰策略,从内存中删除expire数据结构 expire命令在到达过期时间后回自动删除key。删除策略 定时删除:设置过期时间时,创建一个定时器,,让定时器在过期时间来临时立即执行删除,需要创建定时器,耗费CPU,不推荐 惰性删除: 在key被访问时如果发现它已经失效,那么就删除.调用expireIfNeeded函数,读取之前检查一下有没有失效,失效则删除 主动删除: 在redis.conf配置主动删除策略,默认是no-enviction(不删除) 1、allkeys-lru:在不确定时一般采用LRU 2、volatile-lru:比allkeys-lru性能差,存过期时间 3、allkeys-random:随机淘汰,希望请求平均分布时可以选择 4、volatile-ttl 自己控制 5、no-enviction 禁止驱逐(如字典表)
9、谈一谈REDIS中的事务?9.1、事务命令multi:用于标记事务开始,redis会将后续命令逐个放入队列中,然后使用exec原子化的执行这个命令队列exec:执行命令队列discard:清除命令队列watch:监视 keyunwatch:清除监视key
9.2、事务机制1、事务执行:在redisClient中,有flags属性,用来标识是否在事务中 flags=REDIS_MULTI2、命令入队:RedisClient将命令存放在事务队列中(multi, exec,discard,watch除外)3、事务队列:multiCmd *commands 用于存放命令4、执行事务:RedisClient向服务端发送exec命令,RedisServer遍历队列,最后将执行结果一次性返回.如果某条命令发生错误,Redisclient将flags置为REDIS_DIRTY_EXEC,exec命令将会失败返回
9.3、watch执行使用watch监视数据库键,redisDb有一个watch_keys字典,key是某个被监视的数据的key,值是一个链表,记录了所有监视这个key的客户端监视机制触发:数据修改后,监视这个数据的客户端的flags置为REDIS_DIRTY_CAS事务执行:redisclient向服务器发送exec,服务端判断Redisclient的flags,如果是REDIS_DIRTY_CAS,则清空事务队列
9.4、Redis的弱事务性1、redis语法错误,整个事务的命令在队列里都清除2、redis运行错误:在队列里正确的命令可以执行且不支持回滚,因为大多数事务失败都是语法错误或者类型错误,一般开发阶段可预见,redis为了性能就忽略了事务
9.5、lua脚本可以保证事务原子性1、lua命令 1、eval script numkeys key [key ...] arg [arg ... ] 比如:eval"return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 2、redis.call和reids.pcall 比如:eval"return redis.call('set',KEYS[1],ARGV[1])"1 n1 zhaoyun 3、evalsha script load "return redis.call('set',KEYS[1],ARGV[1])" 会返回一个sha的编码,执行evalsha + 编码即可执行脚本 4、直接编写lua脚本,使用redis-client --eval 指定 脚本和参数 2、利用Redis整合Lua,主要是为了性能以及事务的原子性。因为redis帮我们提供的事务功能太差。3、lua脚本复制分为脚本传播模式和命令传播模式 1、脚本传播,脚本包含时间,内部状态、随机函数等不能出现 2、命令传播,所有写命令用事务包裹,复制到AOF文件及服务器。 3、redis.replicate_commands()
9.6、管道,事务,脚本的区别管道无原子性,命令是独立的,脚本事务性强于事务,脚本执行期间,客户端其他脚本或事务无法执行,所以脚本时间应该尽量短
10、REDIS哨兵、复制、集群的设计原理,以及区别?10.1、主从复制原理redis为了单点数据库问题,会把数据复制多个副本到其他节点,以实现redis高可用,数据冗余备份
1、从数据库向主数据库发送sync命令,主数据库接受后创建一个rdb快照文件2、主数据库发送rdb文件给从服务器,从服务器接受并载入该文件3、主服务器将同步阶段接收到的新命令写入缓冲区,然后将缓冲区所有的写命令发送给从服务器执行4、处理完之后,主数据库每写一个命令,就会发送给从服务器注意:2.8之后,会根据从服务器断开之前最新命令的偏移量进行增量同步。2.8之后使用psync发送同步命令,并且带上runid和偏移量offerset,主服务器会判断是否是第一次同步,是的话走全量同步,不是的话根据偏移量进行增量同步
10.2、主从复制存在的问题?
主服务器挂了,从服务器可读不可写,无法实现自动化故障转移
10.3、使用哨兵机制实现自动化故障转移10.3.1、主要功能1、集群监控,负责监控redis master和slave进程是否正常工作2、消息通知:如果某个redis有故障,哨兵负责发送消息作为报警通知给管理员3、故障转移:如果master挂了,会自动转移到slave上4、配置中心:如果故障转移发生了,通知客户端新的master地址
10.3.2、redis哨兵高可用redis简历多个哨兵,共同监控数据节点的运行哨兵之间互相通信,交换对主从节点的监控情况每隔1s每个哨兵会向整个集群方ping命令做心跳检测
10.3.3、哨兵中的主观下线和客观下线主观下线:一个哨兵发现ping主节点时没有响应,主观认为down掉了客观下线:首先发现主节点下线的哨兵将信息发送给其他哨兵,他们也进行ping操作,多个哨兵交换主观判断结果,超过半数以上的哨兵认为主挂了,才判断主节点那客观下线了。投票选举:那个节点最先判断主节点下线,就发起投票机制(raft算法),最终投为主节点的哨兵节点完成主从自动切换过程
10.4、集群为了解决redis单机容量有限问题,将数据进行分片集群处理,存储到多台服务器,内存不受限与单机
10.4.1、edis Cluster采用去中心化模式,使用gossip协议meet:sender向receiver发出,请求receiver加入sender集群ping:节点检测其他节点是否在线pong:receiver收到meet或ping后回复,在Failover后,新的master也会广播pongfail:节点A判断B下线后,A节点广播B的fail消息,其他节点收到后标记B下线publish:节点A收到publish,节点A执行该命令,并广播集群,收到广播的节点执行相同的命令
10.4.2、slot(hash槽)rediscluster把所有的物理节点映射到[0-16384]个slot上,采用平均和连续分配方式。采用crc32算法计算hash槽。为什么是16384个槽,因为作者认为1000台redis之后会出现问题,16384个槽也够用了
10.4.3、集群搭建开启集群
cluster-enabled yes
创建集群:
redis-client --cluster ip1:port1 ipn:portn --cluster-replicas 1 #【指定副本数量,也就是每个几点的副本节点数量】
集群信息可以在node.conf中 迁移:新的master节点加入后者删除,需要进行槽和槽数据的迁移
1、节点B发送状态变更状态命令,将B的slot状态置为importing2、向节点A发送变更命令,将A的slot状态置为migrating3、向A发送migrate命令,告知A将要迁移的slot对应的key迁移到B4、所有key迁移完后,cluster setslot重新设置槽位
扩容:
./redis-cli --cluster add-node 192.168.127.128:7007 192.168.127.128:7001 # 新加入者 (集群发起者)
重新分槽
./redis-cli --cluster reshard 192.168.127.128:7007
添加从节点
./redis-cli --cluster add-node 192.168.127.128:7008 192.168.127.128:7007 --cluster-slave --cluster-master-id 6ff20bf463c954e977b213f0e36f3efc02bd53d6
11、Redis并发竞争key有什么解决方案?并发竞争:redis多个client同时set key引起的并发问题, 如何解决并发竞争key问题?
11.1、使用分布式锁1、整体技术方案:主要是准备一个分布式锁,大家去抢锁,抢到锁就做set操作2、为什么分布式锁:因为传统加锁,只适合单点。不管事zookeeper或redis实现,基本原理都是用一个状态值表示锁,对锁的占用和释放通过状态值来标识3、分布式锁的要求:互斥,无死锁,容错4、分布式锁的实现,数据库,redis(setnx),zookeeper(临时节点)
11.2、使用消息队列并发过大的情况,可以以通过消息队列处理,把并行读写串行化,把redis set操作放在队列里,必须一个一个的执行,这也是高并发的一种通用解决方案
12、REDIS如何实现分布式锁?12.1、使用setnx1、获取锁 使用redis.set(lockKey, requestId, "NX", "PX", expireTime) # 操作是原子性,并发不会有问题 使用result = redis.setnx(lockKey, requestId) ,if(result == 1){ jedis.expire(lockKey, expireTime) } # 并发会有问题2、释放锁 使用del 先查询requestId是否等于查询结果,是的话就删除lockKey,也就是释放锁 # 存在并发问题 使用redis+lua脚本释放锁(推荐) 将操作封装在lua脚本中 ,因为lua脚本具有原子性3、存在问题 单机无法保证高可用。主从无法保证强一致性,主机宕机会造成锁的重复获取。无法续租,超过过期时间后,不能继续使用
12.2、使用使用redission代码实现
实现原理
如果客户端面对的是一个redis集群,根据hash节点随机选择一台,发送lua脚本到redis服务器获取锁原理 锁互斥机制:首先判断锁是否存在,如果已经有了,判断锁的ID是不是自己,此时返回锁的剩余生存时间 自动延时机制:获取锁后,回启东一个watch dog,是一个后台线程,每隔10s检查一下,如果还持有锁,不断延长key的生存时间 可重入锁机制:某个客户端获取锁后,再次获取,使用incrby 进行累加1释放锁原理 每次对锁的加所次数-1,为0后调用del命令删除key
13、谈一谈你对REDIS缓存穿透,缓存击穿,缓存雪崩的理解?13.1、缓存穿透key对应的数据在数据源中不存在,每次针对key的请求从缓存获取不到,请求会到数据源,从而可能压垮数据源解决: 1、布隆过滤器:将所有可能存在的数据hash到一个足够大的bitmap,一个一定不存在的数据会被这个bitmap拦截,从而避免底层查询压力 2、查询空值,我们将空值缓存,但是过期时间很短,最长不超过5分钟
13.2、缓存击穿key对应的数据存在,但是在redis中过期,若此时有大量的并发请求过来,这些请求发现缓存过期会从DB中获取并回设到缓存,这时候可能瞬间把DB压垮解决: 使用互斥锁:在缓存失效的时候,不立即去load db,而是使用互斥锁保护DB资源,只有一个线程去获取DB数据,然后回填到缓存,其他线程排队等待回设完毕或充缓存中获取
13.3、缓存雪崩当服务器重启或大量缓存集中在某个时间段失效, 这也会给后端带来极大的压力解决: 1、加锁或者使用队列串行化读写,真正高并发很少使用,性能低 2、将缓存失效时间分散开,缓存同时失效的重复率会降低
14、REDIS缓存和MYSQL数据一致性的解决方案?读取缓存一般没啥问题,但是涉及到数据更新,就容易出现redis和mysql数据不一致问题
1、如果删除redis缓存,还没来得及写入mysql,此时一个线程俩来读取,缓存为空,则去数据库读取然后写缓存,此缓存中为脏数据2、如果先写库数据库,在删除缓存前,数据库宕机了,没有删掉缓存,此时数据也不一致了
解决方案
14.1、延时双删先删缓存,再写数据库,休眠一段时间,再次删除缓存(休眠时间根据业务定)设置缓存过期时间,可以保证最终一致性弊端:结合双删策略+缓存超时时间,最差就是在超时时间内数据不一致,而且又增加了写请求的耗时
14.2、异步更新缓存mysql binlog增量订阅消费+消息队列+增量数据更新到redis
1、读redis:热数据基本在redis2、写mysql:增删改都操作mysql3、更新redis,mysql操作Binlog,来更新redis 1、将数据全量写入redis 2、增量:实时更新
读取Binlog后,利用消息队列推送更新各台redis缓存数据。可以结合阿里的canal,实现对Binlog的订阅
15、热点数据和冷数据?热数据就是经常会被访问的数据,一般位于redis,命中率尽量要高冷数据是指不经常被访问的数据,一般位于DB中冷热数据交换:可以使用maxmemory+allkeys-lru冷热交换比例:热20w,冷200w
16、什么是缓存热点Key?就是热key突然过期,然后又大量的并发请求过来,可能会将缓存击穿,直达数据库,导致DB压力过大甚至压垮
17、假如Redis有1亿个key,有10w个key以固定前缀开头,如何查询?redis是单线程,keys会阻塞,线上服务会停顿。可以使用scan命令,可以无阻塞的提取指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重即可,整体时间可能比keys长
一、大数据分析师面试题:Redis的耐久化战略
【导读】众所周知,大数据分析师的面试流程与其他行业的不大一样,比如你面试一份文员工作,只需要携带简历就可以了,不过要想面试成功大数据分析师,不仅需要携带简历,还要做好考试的准备,这是每一个大数据分析师的入职必经流程,今天小编就来和大家说说大数据分析师面试题:Redis的耐久化战略,希望对各位考生有所帮助。
一、RDB介绍
RDB 是 Redis
默许的耐久化计划。在指定的时间距离内,实行指定次数的写操作,则会将内存中的数据写入到磁盘中。即在指定目录下生成一个dump.rdb文件。Redis
重启会通过加载dump.rdb文件恢复数据。
可以在redis.windows.conf配备文件中修正save来进行相应的配备
注意事项
60秒内10000条数据则保存
这儿有三个save,只需满意其间任意一条就可以保存
比方:
(1)在redis中保存几条新的数据,用kill
-9粗暴杀死redis进程,模仿redis缺点失常退出,导致内存数据丢掉的场景(或许在,也或许不在,根据save的状况)
(2)手动设置一个save检查点,save 5 1
写入几条数据,等候5秒钟,会发现自动进行了一次dump rdb快照,在dump.rdb中发现了数据
失常停掉redis进程,再从头发起redis,看方才刺进的数据还在
二、AOF介绍
AOF :Redis 默许不打开。它的呈现是为了补偿RDB的缺乏(数据的不一致性),所以它采用日志的方法来记载每个写操作,并追加到文件中。Redis
重启的会根据日志文件的内容将写指令早年到后实行一次以结束数据的恢复作业。(appendonly yes)
注意事项注意事项
可以在redis.windows.conf中进行配备
打开AOF
将appendonly 的no 改为 yes
下面是文件名可以运用默许的文件名,也可以自己改
注意事项
appendfsync运用默许的everysec就可以了
以上就是小编今天给大家整理发送的关于大数据分析师面试题:Redis的耐久化战略的相关内容,希望对各位考生有所帮助,想知道更多关于数据分析师的基本要求有哪些,关注小编持续更新数据分析师岗位解析。
二、大厂面试题详解:如何用Redis实现分布式锁?
说一道常见面试题:一个很简单的答案就是去使用 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的问题有哪些
redis中的数据类型redis集群方案
redis事务处理
redis与db的数据一致性
关于网络管理员面试题的问题,通过《大厂面试题详解:如何用Redis实现分布式锁?》、《面试中关于redis的问题有哪些》等文章的解答希望已经帮助到您了!如您想了解更多关于网络管理员面试题的相关信息,请到本站进行查找!