首页 文章 精选 留言 我的

精选列表

搜索[面试],共4915篇文章
优秀的个人博客,低调大师

高频Redis面试题解析:Redis 事务是否具备原子性?

一、Redis 事务的实现原理 一个事务从开始到结束通常会经历以下三个阶段: 1、事务开始 客户端发送 MULTI 命令,服务器执行 MULTI 命令逻辑。 服务器会在客户端状态(redisClient)的 flags 属性打开 REDIS_MULTI 标识,将客户端从非事务状态切换到事务状态。 void multiCommand(redisClient *c) { // 不能在事务中嵌套事务 if (c->flags & REDIS_MULTI) { addReplyError(c,"MULTI calls can not be nested"); return; } // 打开事务 FLAG c->flags |= REDIS_MULTI; addReply(c,shared.ok); } 2、命令入队 接着,用户可以在客户端输入当前事务要执行的多个命令。 当客户端切换到事务状态时,服务器会根据客户端发来的命令来执行不同的操作。 如果客户端发送的命令为 EXEC、DISCARD、WATCH、MULTI 四个命令的其中一个,那么服务器立即执行这个命令。 与此相反,如果客户端发送的命令是 EXEC、DISCARD、WATCH、MULTI 四个命令以外的其他命令,那么服务器并不立即执行这个命令。 首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient)的 flags 属性打开 REDIS_MULTI 标识,并且返回错误信息给客户端。 如果正确将这个命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复。 我们先看看事务队列是如何实现的? 每个 Redis 客户端都有自己的事务状态,对应的是客户端状态(redisClient)的 mstate 属性。 typeof struct redisClient{ // 事务状态 multiState mstate; } redisClient; 事务状态(mstate)包含一个事务队列(FIFO 队列),以及一个已入队命令的计数器。 /* * 事务状态 */ typedef struct multiState { // 事务队列,FIFO 顺序 multiCmd *commands; /* Array of MULTI commands */ // 已入队命令计数 int count; /* Total number of MULTI commands */ int minreplicas; /* MINREPLICAS for synchronous replication */ time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. */ } multiState; 事务队列是一个 multiCmd 类型数组,数组中每个 multiCmd 结构都保存了一个如入队命令的相关信息:指向命令实现函数的指针,命令的参数,以及参数的数量。 /* * 事务命令 */ typedef struct multiCmd { // 参数 robj **argv; // 参数数量 int argc; // 命令指针 struct redisCommand *cmd; } multiCmd; 最后我们再看看入队列的源码: /* Add a new command into the MULTI commands queue * * 将一个新命令添加到事务队列中 */ void queueMultiCommand(redisClient *c) { multiCmd *mc; int j; // 为新数组元素分配空间 c->mstate.commands = zrealloc(c->mstate.commands, sizeof(multiCmd)*(c->mstate.count+1)); // 指向新元素 mc = c->mstate.commands+c->mstate.count; // 设置事务的命令、命令参数数量,以及命令的参数 mc->cmd = c->cmd; mc->argc = c->argc; mc->argv = zmalloc(sizeof(robj*)*c->argc); memcpy(mc->argv,c->argv,sizeof(robj*)*c->argc); for (j = 0; j < c->argc; j++) incrRefCount(mc->argv[j]); // 事务命令数量计数器增一 c->mstate.count++; } 当然了,还有我们上面提到的,如果命令入队出错时,会打开客户端状态的 REDIS_DIRTY_EXEC 标识。 /* Flag the transacation as DIRTY_EXEC so that EXEC will fail. * * 将事务状态设为 DIRTY_EXEC ,让之后的 EXEC 命令失败。 * * Should be called every time there is an error while queueing a command. * * 每次在入队命令出错时调用 */ void flagTransaction(redisClient *c) { if (c->flags & REDIS_MULTI) c->flags |= REDIS_DIRTY_EXEC; } 3、事务执行 客户端发送 EXEC 命令,服务器执行 EXEC 命令逻辑。 如果客户端状态的 flags 属性不包含 REDIS_MULTI 标识,或者包含 REDIS_DIRTY_CAS 或者 REDIS_DIRTY_EXEC 标识,那么就直接取消事务的执行。 否则客户端处于事务状态(flags 有 REDIS_MULTI 标识),服务器会遍历客户端的事务队列,然后执行事务队列中的所有命令,最后将返回结果全部返回给客户端; void execCommand(redisClient *c) { int j; robj **orig_argv; int orig_argc; struct redisCommand *orig_cmd; int must_propagate = 0; /* Need to propagate MULTI/EXEC to AOF / slaves? */ // 客户端没有执行事务 if (!(c->flags & REDIS_MULTI)) { addReplyError(c,"EXEC without MULTI"); return; } /* Check if we need to abort the EXEC because: * * 检查是否需要阻止事务执行,因为: * * 1) Some WATCHed key was touched. * 有被监视的键已经被修改了 * * 2) There was a previous error while queueing commands. * 命令在入队时发生错误 * (注意这个行为是 2.6.4 以后才修改的,之前是静默处理入队出错命令) * * A failed EXEC in the first case returns a multi bulk nil object * (technically it is not an error but a special behavior), while * in the second an EXECABORT error is returned. * * 第一种情况返回多个批量回复的空对象 * 而第二种情况则返回一个 EXECABORT 错误 */ if (c->flags & (REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC)) { addReply(c, c->flags & REDIS_DIRTY_EXEC ? shared.execaborterr : shared.nullmultibulk); // 取消事务 discardTransaction(c); goto handle_monitor; } /* Exec all the queued commands */ // 已经可以保证安全性了,取消客户端对所有键的监视 unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles */ // 因为事务中的命令在执行时可能会修改命令和命令的参数 // 所以为了正确地传播命令,需要现备份这些命令和参数 orig_argv = c->argv; orig_argc = c->argc; orig_cmd = c->cmd; addReplyMultiBulkLen(c,c->mstate.count); // 执行事务中的命令 for (j = 0; j < c->mstate.count; j++) { // 因为 Redis 的命令必须在客户端的上下文中执行 // 所以要将事务队列中的命令、命令参数等设置给客户端 c->argc = c->mstate.commands[j].argc; c->argv = c->mstate.commands[j].argv; c->cmd = c->mstate.commands[j].cmd; /* Propagate a MULTI request once we encounter the first write op. * * 当遇上第一个写命令时,传播 MULTI 命令。 * * This way we'll deliver the MULTI/..../EXEC block as a whole and * both the AOF and the replication link will have the same consistency * and atomicity guarantees. * * 这可以确保服务器和 AOF 文件以及附属节点的数据一致性。 */ if (!must_propagate && !(c->cmd->flags & REDIS_CMD_READONLY)) { // 传播 MULTI 命令 execCommandPropagateMulti(c); // 计数器,只发送一次 must_propagate = 1; } // 执行命令 call(c,REDIS_CALL_FULL); /* Commands may alter argc/argv, restore mstate. */ // 因为执行后命令、命令参数可能会被改变 // 比如 SPOP 会被改写为 SREM // 所以这里需要更新事务队列中的命令和参数 // 确保附属节点和 AOF 的数据一致性 c->mstate.commands[j].argc = c->argc; c->mstate.commands[j].argv = c->argv; c->mstate.commands[j].cmd = c->cmd; } // 还原命令、命令参数 c->argv = orig_argv; c->argc = orig_argc; c->cmd = orig_cmd; // 清理事务状态 discardTransaction(c); /* Make sure the EXEC command will be propagated as well if MULTI * was already propagated. */ // 将服务器设为脏,确保 EXEC 命令也会被传播 if (must_propagate) server.dirty++; handle_monitor: /* Send EXEC to clients waiting data from MONITOR. We do it here * since the natural order of commands execution is actually: * MUTLI, EXEC, ... commands inside transaction ... * Instead EXEC is flagged as REDIS_CMD_SKIP_MONITOR in the command * table, and we do it here with correct ordering. */ if (listLength(server.monitors) && !server.loading) replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc); } 二、为什么很多人说 Redis 事务为何不支持原子性? 1、Redis 事务不支持事务回滚机制 Redis 事务执行过程中,如果一个命令执行出错,那么就返回错误,然后还是会接着继续执行下面的命令。 下面我们演示一下: 正是因为 Redis 事务不支持事务回滚机制,如果事务执行中出现了命令执行错误(例如对 String 类型的数据库键执行 LPUSH 操作),只会返回当前命令执行的错误给客户端,并不会影响下面的命令的执行。所以很多人觉得和关系型数据库(MySQL) 不一样,而 MySQL 的事务是具有原子性的,所以大家都认为 Redis 事务不支持原子性。 2、但是其实 Redis 意义上是支持原子性的。 正常情况下,它也是要不所有命令执行成功,要不一个命令都不执行。 我们下面演示一下: 全部执行成功的: 一个都不执行: 这就是上面提到的,在事务开始后,用户可以输入事务要执行的命令;在命令入事务队列前,会对命令进行检查,如果命令不存在或者是命令参数不对,则会返回错误可客户端,并且修改客户端状态。 当后面客户端执行 EXEC 命令时,服务器就会直接拒绝执行此事务了。 所以说,Redis 事务其实是支持原子性的!即使 Redis 不支持事务回滚机制,但是它会检查每一个事务中的命令是否错误。 但是我们要注意一个点就是:Redis 事务不支持检查那些程序员自己逻辑错误。例如对 String 类型的数据库键执行对 HashMap 类型的操作! 我很赞同 Redis 作者的想法: 首先,MySQL 和 Redis 的定位不一样,一个是关系型数据库,一个是 NoSQL。 MySQL 的 SQL 查询是可以相当复杂的,而且 MySQL 没有事务队列这种说法,SQL 真正开始执行才会进行分析和检查,MySQL 不可能提前知道下一条 SQL 是否正确。所以支持事务回滚是非常有必要的~ 但是,Redis 使用了事务队列来预先将执行命令存储起来,并且会对其进行格式检查的,提前就知道命令是否可执行了。所以如果只要有一个命令是错误的,那么这个事务是不能执行的。 Redis 作者认为基本只会出现在开发环境的编程错误其实在生产环境基本是不可能出现的(例如对 String 类型的数据库键执行 LPUSH 操作),所以他觉得没必要为了这事务回滚机制而改变 Redis 追求简单高效的设计主旨。

优秀的个人博客,低调大师

面试官:MySQL的自增ID用完了,怎么办?

既然这块知识点不清楚,那回头就自己动手实践下。 首先,创建一个最简单的表,只包含一个自增id,并插入一条数据。 createtablet0(idintunsignedauto_incrementprimarykey);insert intot0values(null); 通过show命令 show create table t0; 查看表情况 CREATETABLE`t0`(`id`int(10)unsignedNOTNULLAUTO_INCREMENT, PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=2DEFAULT CHARSET=utf8 可以发现 AUTO_INCREMENT 已经自动变成2,这离用完还有很远,我们可以算下最大当前声明的自增ID最大是多少,由于这里定义的是 intunsigned,所以最大可以达到2的32幂次方 - 1 = 4294967295 这里有个小技巧,可以在创建表的时候,直接声明AUTO_INCREMENT的初始值 createtablet1(idintunsignedauto_incrementprimarykey) auto_increment=4294967295;insertintot1values(null); 同样,通过show命令,查看t1的表结构 CREATETABLE`t1`(`id`int(10)unsignedNOTNULLAUTO_INCREMENT, PRIMARYKEY(`id`))ENGINE=InnoDBAUTO_INCREMENT=4294967295DEFAULT CHARSET=utf8 可以发现,AUTO_INCREMENT已经变成4294967295了,当想再尝试插入一条数据时,得到了下面的异常结果 17:28:03insertintot1values(null)ErrorCode:1062.Duplicate entry'4294967295'forkey'PRIMARY'0.00054sec 说明,当再次插入时,使用的自增ID还是 4294967295,报主键冲突的错误。 4294967295,这个数字已经可以应付大部分的场景了,如果你的服务会经常性的插入和删除数据的话,还是存在用完的风险,建议采用bigint unsigned,这个数字就大了。 不过,还存在另一种情况,如果在创建表没有显示申明主键,会怎么办? 如果是这种情况,InnoDB会自动帮你创建一个不可见的、长度为6字节的row_id,而且InnoDB 维护了一个全局的 dictsys.row_id,所以未定义主键的表都共享该row_id,每次插入一条数据,都把全局row_id当成主键id,然后全局row_id加1 该全局row_id在代码实现上使用的是bigint unsigned类型,但实际上只给row_id留了6字节,这种设计就会存在一个问题:如果全局row_id一直涨,一直涨,直到2的48幂次-1时,这个时候再+1,row_id的低48位都为0,结果在插入新一行数据时,拿到的row_id就为0,存在主键冲突的可能性。 所以,为了避免这种隐患,每个表都需要定一个主键。

优秀的个人博客,低调大师

3年Java开发6个点搞定高并发系统面试疑惑

前言 其实所谓的高并发,如果你要理解这个问题呢,其实就得从高并发的根源出发,为啥会有高并发?为啥高并发就很牛逼?说的浅显一点,很简单,就是因为刚开始系统都是连接数据库的,但是要知道数据库支撑到每秒并发两三千的时候,基本就快完了。所以才有说,很多公司,刚开始干的时候,技术比较 low,结果业务发展太快,有的时候系统扛不住压力就挂了。当然会挂了,凭什么不挂?你数据库如果瞬间承载每秒 5000/8000,甚至上万的并发,一定会宕机,因为比如 mysql 就压根儿扛不住这么高的并发量。所以为啥高并发牛逼?就是因为现在用互联网的人越来越多,很多 app、网站、系统承载的都是高并发请求,可能高峰期每秒并发量几千,很正常的。如果是什么双十一之类的,每秒并发几万几十万都有可能。 那么如此之高的并发量,加上原本就如此之复杂的业务,咋玩儿?真正厉害的,一定是在复杂业务系统里玩儿过高并发架构的人,但是你没有,那么我给你说一下你该怎么回答这个问题:可以分为以下 6 点:**1.系统拆分2.缓存3.MQ4.分库分表5.读写分离6.ElasticSearch** 系统拆分 将一个系统拆分为多个子系统,用 dubbo 来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以扛高并发么。 缓存 缓存,必须得用缓存。大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟人家 redis 轻轻松松单机几万的并发。所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发。 MQ MQ,必须得用 MQ。可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改,疯了。那高并发绝对搞挂你的系统,你要是用 redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数据格式还无比简单,没有事务支持。所以该用 mysql 还得用 mysql 啊。那你咋办?用 MQ 吧,大量的写请求灌入 MQ 里,排队慢慢玩儿,后边系统消费后慢慢写,控制在 mysql 承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。MQ 单机抗几万并发也是 ok 的,这个之前还特意说过。 分库分表 分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高 sql 跑的性能。 读写分离 读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库。 ElasticSearch Elasticsearch,简称 es。es 是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用 es 来承载,还有一些全文搜索类的操作,也可以考虑用 es 来承载。欢迎大家关注我的公种浩【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。 上面的 6 点,基本就是高并发系统肯定要干的一些事儿,大家可以仔细结合之前讲过的知识考虑一下,到时候你可以系统的把这块阐述一下,然后每个部分要注意哪些问题,之前都讲过了,你都可以阐述阐述,表明你对这块是有点积累的。说句实话,毕竟你真正厉害的一点,不是在于弄明白一些技术,或者大概知道一个高并发系统应该长什么样?其实实际上在真正的复杂的业务系统里,做高并发要远远比上面提到的点要复杂几十倍到上百倍。你需要考虑:哪些需要分库分表,哪些不需要分库分表,单库单表跟分库分表如何 join,哪些数据要放到缓存里去,放哪些数据才可以扛住高并发的请求,你需要完成对一个复杂业务系统的分析之后,然后逐步逐步的加入高并发的系统架构的改造,这个过程是无比复杂的,一旦做过一次,并且做好了,你在这个市场上就会非常的吃香。其实大部分公司,真正看重的,不是说你掌握高并发相关的一些基本的架构知识,架构中的一些技术,RocketMQ、Kafka、Redis、Elasticsearch,高并发这一块,你了解了,也只能是次一等的人才。对一个有几十万行代码的复杂的分布式系统,一步一步架构、设计以及实践过高并发架构的人,这个经验是难能可贵的。最后欢迎大家一起交流,喜欢文章记得点个赞哟,感谢支持!

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。