Redis面试经典案例
缓存
缓存三兄弟(穿透,击穿,雪崩)
1.缓存穿透:进行查询操作的时候,先查询redis,没有命中的话查询db,db如果查询到了数据,先写入redis进行缓存重构,然后返回数据,但是如果恶意攻击,大量请求不存在的数据,那么请求会一直打到数据库,数据库并发量小,会导致宕机,这就是缓存穿透 解决办法:1.使用布隆过滤器,布隆过滤器实际上是一个bitmap的数组,在启动redis时,先对布隆过滤器进行初始化,将key存储到布隆过滤器中,(原理是,会使用三个不同的hash算法,计算出三个不同的值,并将bitmap中的值改为1),查询请求到业务层的时候,会先进行判断,如果布隆过滤器中没有对应的key,那么直接返回null,但是布隆过滤器存在误判和内存占用率的问题,首先,如果计算出的hash值,正好与其他两个key计算出的值对应上了,那么也会判断为存在,进行查询,第二,想要误判率小,那么就要扩大bitmap这个数组的长度,增大了内存的占用,在使用布隆过滤器时,可以对数组长度和误判率进行初始化调整 2.将空值也缓存到redis中,即便数据库查询为空,也将对应的key和null存储到redis中,让下次请求不打到数据库,直接命中redis,但是会造成脏数据占用多的问题, 个人觉得,小项目中使用方法二,大项目使用方法一
2.缓存击穿:在进行查询操作的时候,如果热点数据突然过期,第一个请求会进行缓存重建,但是在缓存重建的过程中,如果有大量请求并发,会有大量并发请求到数据库,此时数据库承受不住高并发,会导致宕机,这就是缓存击穿。
解决办法:1.使用互斥锁,在第一个请求到达业务层时,先进行判断,如果此时redis未命中,那么先获取锁,获取到锁之后查询数据库,并将数据库中的数据在redis中进行缓存重建,然后释放锁,返回数据。别的请求到达业务层后,redis未命中,同时获取锁失败,就会进行自旋,重新查询redis,直到获取到redis中的数据。优点是有强一致性,缺点是性能差。
2.使用逻辑过期,也就是在redis中构建数据时,加上一个expire的过期字段,设置上过期时间,在第一个请求redis中命中数据之后,先检查一下逻辑过期时间,如果过期,那么获取锁,同时,新开一个线程,并返回过期数据,在新线程中,完成缓存重建。其他请求命中redis后,尝试获取锁,获取锁失败,直接返回当前旧数据。优点是性能好,高可用,但是不能保证数据强一致性
个人觉得,使用逻辑过期更为合理,因为这样用户体验较好
3.缓存雪崩:是指在同一时段,redis中有大量的key过期或者redis宕机,那么大量请求同时打到数据库,会给数据库带来巨大压力,可能导致数据库直接宕机,这就是缓存雪崩
解决办法:1.给不同的key增加随机的TTL随机值
2.如果是redis宕机,那么解决方案就是搭建redis集群提高服务的可用性(哨兵模式,集群模式)
3.给缓存业务增加降级限流策略(nginx或者是spring cloud gateway)降级限流可以作为缓存所有问题的保底策略
4.给业务添加多级缓存(Guava或Caffeine)但是这两个我都没了解过,多级缓存的话,我知道jvm有一个缓存机制,还有数据库也有自己的缓存机制

双写一致性
redis作为缓存,mysql中的数据如何与redis中数据进行同步,这就是双写一致性
首先,可以采用延迟双删的策略,就是先删除缓存,然后更改数据库,然后延时再删除一次缓存,这样后面查询的时候,拿到的就是数据库中最新的数据,但是延时多久拿,这个不太好确定
还有一个问题就是,先删除缓存还是先删除数据库,都会有脏数据的出现,所以其实先后不重要
1.允许延时一致的业务
可以使用MQ等中间件,更新数据之后,通知缓存删除
可以使用canal中间件,不需要修改业务代码,伪装为mysql的一个从节点,canal可以通过监听mysql的binlog文件,当所监听的表数据发生了修改操作时,会通知缓存进行删除
2.要求数据强一致性的业务
采用redisson提供的读写锁
共享锁:读锁readlock,加锁之后,别的线程也可以进行读操作,但是不能进行写操作
排他锁:writelock,也叫独占锁,加锁之后,别的线程无法进行读写操作,实际上底层也是 一个setnx,保证同一时间只有一个线程操作锁
缓存的持久化
redis作为缓存,提供了两种数据持久化的方式,RDB和AOF
RDB是一个二进制快照文件,将redis内存存储的数据写到磁盘上,当redis宕机需要新的实例时,从RDB快照文件中读取数据,但是这也会造成短时间的数据丢失,优点是文件小,数据恢复的速度快,但是占用的cpu和内存资源也较多
AOF是一个追加文件,也就是在redis进行写操作的时候,会将redis的操作命令记录下来,存储到磁盘中,当redis宕机时,会从这个文件中再执行一次命令,优点是数据基本不会丢失,但是恢复的速度慢
缓存数据过期策略
1.惰性删除: 在需要使用到key的时候,先判断下其是否过期,如果过期,那么就删除当前key,这样的好处是,不会浪费cpu的资源来查询没有过期的key,坏处是,如果过期的key过多但是没有使用到,会占用内存
2.定期删除:每隔一段时间,就对redis中的key进行检查,删除其中过期的key
SLOW模式:一个定时任务,执行的默认频率为10hz,每次不超过25ms,可以通过修改redis的配置文件的hz选项来进行调整
FAST模式:执行频率不固定,但是两次之间间隔不低于2ms,每次耗时不超过1ms
优点:可以通过限制删除操作的时长和频率来减少操作对cpu的影响,定期删除,也能减少过期key对内存的占用
缺点:难以确定删除操作的时长和频率
redis 的过期策略是,惰性删除+定期删除配合使用
缓存数据淘汰策略
当redis中的内存不够用时,往redis中放入新的key,这时redis会按照某一种规则将内存中的数据删除掉,这就是缓存数据淘汰策略
redis中支持8种不同的策略来选择要删除的key
| 策略名称 | 淘汰范围 | 淘汰算法 | 适用场景 | 特点说明 |
|---|---|---|---|---|
| noeviction | 不淘汰 | 无淘汰 | 关键数据存储,数据安全优先 | 内存不足时拒绝写入,确保数据不丢失 |
| allkeys-lru | 所有键 | LRU(最近最少使用) | 通用缓存场景,全部数据可淘汰 | 近似LRU算法,采样淘汰最久未访问的键 |
| volatile-lru | 仅有过期时间的键 | LRU(最近最少使用) | 缓存与持久数据混合存储 | 只淘汰设置了过期时间的键中的LRU键 |
| allkeys-random | 所有键 | 随机选择 | 访问模式均匀的场景 | 从所有键中随机选择淘汰 |
| volatile-random | 仅有过期时间的键 | 随机选择 | 缓存数据随机访问模式 | 从有过期时间的键中随机选择淘汰 |
| volatile-ttl | 仅有过期时间的键 | TTL(存活时间) | 希望尽快释放过期键内存 | 淘汰剩余生存时间最短的键 |
| allkeys-lfu | 所有键 | LFU(最不经常使用) | 热点数据明显的场景 | 淘汰访问频率最低的键,保留热点数据 |
| volatile-lfu | 仅有过期时间的键 | LFU(最不经常使用) | 缓存数据中有明显热点 | 从有过期时间的键中淘汰访问频率最低的键 |
分布式锁
redisson实现分布式锁,其底层原理其实是setnx和lua脚本,可以保证原子性
在redisson提供的分布式锁中,可以通过看门狗机制来有效延长锁的持有时间,一个线程获取锁成功后,watchdog会给持有锁的线程进行续期,默认是每十秒续一次,避免业务还没完成,但是锁已经释放的情况
redisson的锁是可以实现重入的,因为redisson的锁在redis中使用的是hash结构,有一个大key,一个小key,大key可以自定义,但是小key就是当前线程的唯一id,如果是同一线程,那么就可以进行重入,一般需要使用到重入的情况,都是业务比较复杂
红锁可以解决redis主从数据一致的问题,但是性能很差,在redis集群中,通常使用的是主从集群结构,主节点一般负责写数据,从节点一般负责读数据,当有一台redis宕机之后,从节点成为主节点,当有一个线程获取到锁之后,主节点宕机,主节点还没来得及将数据同步到从节点,此时从节点成为了主节点,又有一个线程来获取锁,那么就会发生一把锁,两个线程持有的情况,不满足互斥锁的特性,会导致有脏数据,如果是需要主从数据一致性强的业务,建议使用zookeeper,zookeeper能保证数据的一致性
集群方案
单节点redis的并发能力是有限的,如果要提供redis的并发能力,那么就需要构建redis集群,实现读写分离,一般都是一主多从,主节点负责写数据,从节点负责写数据,这就是redis的主从同步
1.主从复制
全量同步:从节点执行replicaof命令,与主节点建立连接,从节点会向主节点发送一个replid和offset,replid是数据集id,id一致说明是同一个数据集,主节点先判断id是否与自己一致,如果不一致,那么说明是第一次同步,主节点返回自己的relid和当前的偏移量offset从节点,同时主节点执行bgsave,生成一个RDB文件,发送给从节点,在生成RDB的过程中,可能还会有命令执行,这时主节点会用一个repl_baklog的日志文件来记录RDB期间所有的命令,并发送给从节点,从节点将RDB文件加载后,再根据日志文件执行命令,offset就是日志文件中的偏移量,如果从节点的offset低于了主节点的offset,那么说明需要更新
增量同步:从节点请求同步数据,如果不是第一次请求,那么获取到从节点的offset值,然后将repl_baklog中获取到offset值后的数据,发送给从节点,从节点执行命令,进行数据同步
2.哨兵模式
哨兵的作用,来实现主从集群的自动故障恢复 (监控,自动故障恢复,通知)
哨兵模式的主要作用其实就是为了保证redis集群的高可用, 每一个sentinel都会去监听所有的实例,如果超过一般的哨兵都发现实例没有响应,那么哨兵就会认为这个实例已经宕机
集群脑裂问题:当某一个时间段,sentinel和主节点和从节点不在一个网络分区,此时哨兵没有监测到主节点master的心跳,那么哨兵就会将从节点升为主节点,但是此时客户端的服务还在之前的主节点写入数据,当网络恢复之后,之前的主节点会强制降为从节点,清除数据,然后跟新的主节点同步数据,此时,就会造成大量数据的丢失,因为在同一时间段,出现了两个master,就像大脑分裂了一样,这就是集群脑裂问题
解决办法:可以通过修改redis的配置,可以设置最少的从节点数量,也就是如果当前master没有从节点,那么就拒绝请求,以及缩短主从数据同步的延迟时间,也就是master与slave之间的数据同步时间要短,如果出现脑裂,那么是没有办法同步数据的,如果达不到要求,那么也拒绝请求
3.分片集群
解决海量存储问题,高并发写的问题
通过redis集群的分片,实现大量数据存储,集群中有多个master节点,每个master存储不同的数据,每个master还可以拥有自己的slave节点,可以形成主从集群的关系,因为有多个master,可以解决高并发写的问题,多个slave,也可以解决高并发读的问题,并且master之间还可以通过ping检测彼此健康状态,相当于自带了哨兵,客户端请求可以发送到任意节点,集群会自动路由,最终请求会转发到正确的节点
因为redis分片集群引入了hash槽的概念,redis集群有16384个哈希槽,每个key通过crc16校验后对16384取模,决定放置于哪个槽,集群的每个节点负责一部分哈希槽
redis是单线程的,但是为什么还那么快
因为redis是纯内存操作,并且完成基于C语言完成,执行速度非常快,采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题,使用了多路复用IO模型,非阻塞IO
多路复用IO模型:使用单个线程同时监听多个socket,并在某个socket可读和可写时,得到通知,从而避免无效的等待,充分利用cpu资源,目前I/O多路复用普通采用的都是epoll模式,它会在通知用户进程socket准备就绪的同时,把已就绪的socket写入用户空间,不需要遍历socket来确定是否就绪,提升了性能
redis的网络模型:redis的网络模型就是使用了多路复用IO和任务派发的机制来应对多个socket请求
连接应答处理器
命令恢复处理器 在 redis 6.0之后,使用了多线程
命令请求处理器 在redis 6.0之后,将命令的转换使用了多线程,增加命令转换的速度,但是命令的执行还是单线程
MySQL面试经典案例
mysql的优化
定位慢查询
如聚合查询,多表查询,表数据量过大查询,深度分页查询(表象:查询时间过长,接口返回数据时间过慢)
方案1:开源工具
调试工具:Arthas 运维工具:Prometheus,Skywalking
方案2:mysql自带的慢日志(一般测试阶段使用,生产环境中会损失mysql的性能)
分析慢sql
MySQL EXPLAIN 工具字段详解表
| 字段 | 说明 | 常见值/含义 | 优化建议 |
|---|---|---|---|
| id | 查询标识符 | 1. 数字:执行顺序(越大越先执行) 2. 相同id:从上到下执行 3. 不同id:id大的先执行 | 用于理解复杂查询的执行顺序 |
| select_type | 查询类型 | 1. SIMPLE:简单查询(无子查询/UNION) 2. PRIMARY:主查询 3. SUBQUERY:子查询 4. DERIVED:派生表(FROM子句中的子查询) 5. UNION:UNION中的第二个及以后查询 6. UNION RESULT:UNION结果 | 识别复杂查询结构,优化子查询和派生表 |
| table | 访问的表 | 表名或别名, |
确认查询涉及的具体表 |
| partitions | 匹配的分区 | 分区表使用的分区名称 | 分区裁剪优化 |
| type | 访问类型(关键指标) | 性能从好到差: 1. system:系统表,仅一行 2. const:通过主键/唯一索引查找 3. eq_ref:关联查询,使用唯一索引 4. ref:使用非唯一索引查找 5. range:索引范围扫描 6. index:全索引扫描 7. ALL:全表扫描 | 尽量避免ALL和index,优化为range或ref |
| possible_keys | 可能使用的索引 | 查询可能使用的索引列表 | 检查是否有合适的索引未被使用 |
| key | 实际使用的索引 | 实际选择的索引,NULL表示未使用索引 | 对比possible_keys,确认索引选择是否合理 |
| key_len | 索引长度 | 使用的索引字节数,可判断索引使用情况 | 复合索引中查看是否充分利用索引 |
| ref | 索引引用 | 显示索引的哪一列被使用 | 检查关联查询的索引使用 |
| rows | 预估扫描行数 | 预估需要检查的行数 | 数值越大性能越差,考虑优化索引 |
| filtered | 过滤百分比 | 存储引擎返回数据在服务器层过滤的比例(0-100) | 值越小表示过滤效果越好,但大量数据过滤可能需优化 |
| Extra | 额外信息(关键指标) | 常见值: 1. Using index:覆盖索引 2. Using where:服务器层过滤 3. Using temporary:使用临时表 4. Using filesort:文件排序 5. Using join buffer:使用连接缓冲区 6. Impossible WHERE:WHERE条件不可能满足 | 关注Using filesort/temporary,这些通常需要优化 |
如果一条sql执行很慢的话,我们通常会使用mysql自动的执行计划explain来去查看这条sql的执行情况,比如在这里面可以通过key和key_len检查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况。第二个,可以通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描,第三个可以通过extra建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复
索引
什么是索引:索引 (index) 是帮助MysoL高效获取数据的数据结构 (有序) 。在数据之外,数据库系统还维护着满足特定查找算法的数据结构 (B+树) ,这些数据结构以某种方式引用 (指向) 数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。
索引的底层数据结构是什么:
MySQL的默认的存储引擎InnoDB采用的B+树的数据结构来存储索引,选择B+树的主要的原因是:第一阶数更多,路径更短,第二个磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据,第三是B+树便于扫库和区间查询,叶子节点是一个双向链表
B树和B+树的区别是什么呢?
第一:在B树中,非叶子节点和叶子节点都会存放数据,而B+树的所有的数据都会出现在叶子节点,在查询的时候,B+树查找效率更加稳定
第二:在进行范围查询的时候,B+树效率更高,因为B+树都在叶子节点存储,并且叶子节点是一个双向链表
聚集索引和非聚集索引(二级索引)
什么是聚簇索引什么是非聚簇索引?
- 聚簇索引(聚集索引):数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个
- 非聚簇索引(二级索引):数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个
什么是回表查询?
通过二级索引找到对应的主键值,到聚集索引中查找整行数据,这个过程就是回表
覆盖索引:覆盖索引是指查询使用了索引,返回的列,必须在索引中全部能够找到
- 使用id查询,直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。
- 如果返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select *
mysql超大分页怎么处理:
在数据量比较大时,limit分页查询,需要对数据进行排序,效率低,此时使用覆盖索引+子查询,先在子查询中查询id,因为id是覆盖索引,所以在索引中查询效率快,底层是B+树,所以其实范围查询效率是快的,然后将返回的id集合,再到原来的表中做关联查询,能提高很多效率
索引的创建原则:
- 针对于数据量较大,且查询比较频繁的表建立索引。
- 针对于常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。
- 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
- 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引。
- 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率。
- 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率。
- 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。
索引失效的场景:
索引失效情况及其解释:
| 索引失效情况 | 解释 |
|---|---|
| 违反最左前缀法则 | 在使用联合索引时,查询条件必须从索引的最左列开始,否则索引将无法生效。 |
| 范围查询右边的列,不能使用索引 | 当查询条件中使用了范围查询(如 >、<、BETWEEN)后,其右边的列将无法使用索引进行进一步筛选。 |
| 不要在索引列上进行运算操作,索引将失效 | 如果在索引列上进行函数运算、算术运算或表达式操作,数据库将无法直接使用该索引进行查询。 |
| 字符串不加单引号,造成索引失效(类型转换) | 如果字符串类型的列在查询时未加引号,数据库可能会进行隐式类型转换,导致索引无法使用。 |
以 % 开头的 Like 模糊查询,索引失效 |
如果使用 LIKE '%xxx' 这种以通配符开头的模糊查询,索引将无法被有效利用,通常只支持 LIKE 'xxx%' 使用索引。 |
sql优化
| 优化方向 | 具体措施 |
|---|---|
| 1. 表的设计与数据类型选择 | - 合理设计表结构,遵循范式与反范式平衡 - 选择合适数据类型(如用 INT 代替 BIGINT) - 尽量使用 NOT NULL 约束 - 合理使用分区表 |
| 2. 索引优化 | - 在 WHERE、ORDER BY、GROUP BY 相关列创建索引 - 优先使用联合索引 - 为区分度高的列建索引 - 控制索引数量,定期清理冗余索引 |
| 3. SQL 语句优化 | - 避免 SELECT *,只取所需字段 - 使用 EXPLAIN 分析执行计划 - 避免 WHERE 中对字段进行函数运算 - 用 INNER JOIN 替代子查询(合适时) - 优化分页查询,避免大数据量翻页 |
| 4. 主从复制与读写分离 | - 配置主从复制,读操作分流至从库 - 使用读写分离中间件或框架 - 避免写操作阻塞读操作 - 监控主从同步延迟 |
| 5. 分库分表 | - 水平分表(按时间、ID 范围等) - 垂直分表(按业务模块拆分字段) - 分库(按业务模块分库) - 使用分库分表中间件(如 ShardingSphere) - 合理设计分片键,处理跨库查询 |
事务
什么是事务:事务是一组操作的集合,它是一个不可分割的工作单位,系统会将这些操作一起提交或者撤销,即同时成功或者同时失败
ACID:
原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
事务并发问题:
| 问题 | 描述 |
|---|---|
| 脏读 | 一个事务读取到另一个事务尚未提交的数据。 |
| 不可重复读 | 一个事务先后读取同一条记录,但两次读取的数据不同。 |
| 幻读 | 一个事务按照条件查询数据时未找到对应行,但在插入数据时又发现该行已存在,仿佛出现“幻影”。 |
事务隔离级别:
| 隔离级别 | 中文说明 | 可能存在的问题 |
|---|---|---|
| READ UNCOMMITTED | 未提交读 | 脏读、不可重复读、幻读 |
| READ COMMITTED | 读已提交 | 不可重复读、幻读 |
| REPEATABLE READ | 可重复读 | 幻读 |
| SERIALIZABLE | 串行化 | 无 |
实际上可重复读的隔离级别可以解决部分幻读问题
在 MySQL InnoDB 引擎的“可重复读(REPEATABLE READ)”隔离级别下,幻读问题在实际应用中被有效解决了,但这属于数据库对标准隔离级别的增强实现,而非 SQL 标准的强制要求
- 从 SQL 标准来看:可重复读级别本身允许幻读。
- 从 MySQL InnoDB 实现来看:它通过 MVCC(多版本并发控制) 和 间隙锁(Next-Key Lock) 两种机制的协同作用,分别解决了“快照读”和“当前读”场景下的幻读问题:
- MVCC:确保事务内普通的
SELECT查询(快照读)基于一致性视图,看不到其他事务插入的数据。 - 间隙锁:对涉及范围查询的加锁操作(如
SELECT ... FOR UPDATE),会锁住记录之间的“间隙”,阻止其他事务插入新记录。
- MVCC:确保事务内普通的
因此,虽然从严格意义上讲,标准并未强制要求可重复读解决幻读,但 InnoDB 的实际实现已经将幻读风险降至极低,可以认为在该级别下幻读被“有效防御”了。这也是 MySQL 选择 REPEATABLE READ 作为默认隔离级别的重要原因。
redolog和undolog:
redolog:在数据库存储层面,实际上存储分为两个层次,缓冲层和磁盘层,缓存层面是基于内存的,会将磁盘层中的数据页加载到缓冲层中便于数据的查询和修改,而当进行增删改查的操作时,首先会进入到缓冲层中查询数据页,然后进行修改,缓存层再将修改后的数据同步到磁盘层,在这中间,就有redolog bufffer来记录数据页的变化,然后同步到磁盘层,但是如果还未同步服务器就宕机了,那么就会造成数据的丢失,所以在事务提交之后,会将缓冲中的所有修改信息存到redolog file日志文件中,用于刷新脏页到磁盘时发生错误,进行数据恢复使用,这就是redolog
undolog:回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚 和 MVCC(多版本并发控制)。undo log和redo log记录物理日志不一样,它是逻辑日志。
- 可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然。
- 当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。
undo log和redo log的区别
- redo log:记录的是数据页的物理变化,服务宕机可用来同步数据
- undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
- redo log保证了事务的持久性,undo log保证了事务的原子性和一致性
保证事务的隔离性:
锁:排他锁(一个事务获取了数据行的排他锁,那么别的事务就不能获取数据行的其他锁)
MVCC:多版本并发控制,维护一个数据的多个版本,使得读写没有冲突
在数据库数据行中,实际上还隐藏了几个字段
| 隐藏字段 | 含义 |
|---|---|
| DB_TRX_ID | 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。 |
| DB_ROLL_PTR | 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。 |
| DB_ROW_ID | 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。 |
在事务进行的时候,会在trx_id生成一个id,记录每一次操作的事务id,是自增的,而回滚指针roll_pointer指向的是上一个版本的事务版本地址,undolog有两个作用,回滚日志,存储老版本的数据,同时,它也会存储一个版本链,一个链表,将历史的事务版本串联起来,在事务同时进行并发的时候,如果读取数据,那么会从readview中读取数据
- readview
ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。
- 当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select … lock in share mode(共享锁),select … for update、update、insert、delete(排他锁)都是一种当前读。
- 快照读
简单的select(不加锁)就是快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
- Read Committed:每次select,都生成一个快照读。
- Repeatable Read:开启事务后第一个select语句才是快照读的地方。
readview中有这么几个字段
| 字段 | 含义 |
|---|---|
| m_ids | 当前活跃的事务ID集合 |
| min_trx_id | 最小活跃事务ID |
| max_trx_id | 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的) |
| creator_trx_id | ReadView创建者的事务ID |
会根据规则判断,当前事务能读取到哪个版本的数据

这样就能避免读写冲突的问题
readView解决的是一个事务查询选择版本的问题,根据readView的匹配规则和当前的一些事务id判断该访问那个版本的数据
不同的隔离级别快照读是不一样的,最终的访问的结果不一样
RC:每一次执行快照读时生成ReadView RR:仅在事务中第一次执行快照读时生成ReadView,后续复用
主从同步原理
MySQL主从复制的核心就是二进制日志binlog(DDL语句和DML语句)
① 主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中
② 从库通过IOthread线程读取主库的二进制日志文件Binlog,写入到从库的中继日志Relay Log。
③ 从库通过sqlthread线程重做中继日志中的事件,将改变反映它自己的数据
这就是mysql的主从同步原理
分库分表
以下是关于数据库分库分表策略的整理表格,结合了您的文本内容和相关知识:
| 策略类型 | 核心概念 | 主要目的 | 适用场景 | 优缺点 | 常见工具 |
|---|---|---|---|---|---|
| 垂直分库 | 按业务模块拆分数据库,不同业务使用不同数据库 | 1. 解耦业务 2. 提高并发处理能力 3. 分散磁盘IO和网络连接压力 |
1. 业务模块清晰、耦合度低 2. 并发量高 3. 不同业务数据量差异大 |
优点: - 业务解耦,便于维护 - 针对不同业务优化 - 降低单库连接数压力 缺点: - 无法解决单表数据量过大问题 - 跨库查询复杂(需业务层或中间件支持) |
ShardingSphere MyCat |
| 垂直分表 | 将一张表按字段使用频率拆分(冷热分离) | 1. 减少单表字段数 2. 提高查询效率 3. 避免大字段影响性能 |
1. 表字段多,且有明显冷热区分 2. 某些字段占用空间大(如BLOB/TEXT) 3. 频繁查询的字段较少 |
优点: - 提升热点数据查询速度 - 减少磁盘IO - 表结构更清晰 缺点: - 查询需关联多表 - 事务处理复杂 |
通常由ORM框架或业务代码实现 |
| 水平分库 | 将同一业务数据按规则拆分到多个数据库 | 1. 解决海量数据存储问题 2. 分散写压力,提高并发 |
1. 单库数据量接近物理极限 2. 高并发写入场景 3. 数据持续快速增长 |
优点: - 根本性解决数据量瓶颈 - 大幅提升并发处理能力 缺点: - 跨库查询、事务、排序非常复杂 - 数据迁移和扩容难度大 |
ShardingSphere MyCat |
| 水平分表 | 将同一张表的数据按规则拆分到多个结构相同的表中 | 1. 解决单表数据量过大问题 2. 提升查询和写入性能 |
1. 单表数据量过大(如千万级) 2. 索引效率下降 3. 维护困难(备份、DDL操作慢) |
优点: - 提升单表操作性能 - 维护相对简单(同库) 缺点: - 仍在同一库,受限于单库资源 - 跨表查询复杂 |
ShardingSphere MyCat |
何时需要考虑分库分表?
- 单表数据量 > 千万级 且性能明显下降
- 数据库服务器CPU/IO使用率持续高位
- 业务增长迅速,预计数据量将持续暴涨
框架面试经典案例
spring-单例bean是线程安全的吗?
不是线程安全的 Spring框架中有一个@Scope注解,默认的值就是singleton,单例的。
因为一般在spring的bean的中都是注入无状态的对象,没有线程安全问题,如果在bean中定义了可修改的成员变量,是要考虑线程安全问题的,可以使用多例或者加锁来解决
当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中针对该单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。
比如:我们通常在项目中使用的Spring bean都是不可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。
如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用域由“singleton”变更为“prototype”。