一份热乎乎的字节面试真题(一)
前言
大家好,我是捡田螺的小男孩,有位伙伴面试了字节(四年半工作经验),分享下面试真题,大家一起加油哈。
- 说说Redis为什么快
- Redis有几种数据结构,底层分别是怎么存储的
- Redis有几种持久化方式
- 多线程情况下,如何保证线程安全?
- 用过volatile吗?底层原理是?
- MySQL的索引结构,聚簇索引和非聚簇索引的区别
- MySQL有几种高可用方案,你们用的是哪一种
- 说说你做过最有挑战性的项目
- 秒杀采用什么方案
- 聊聊分库分表,需要停服嘛
- redis挂了怎么办?
- 你怎么防止优惠券有人重复刷?
- 抖音评论系统怎么设计
- 怎么设计一个短链地址
- 有一个数组,里面元素非重复,先升序再降序,找出里面最大的值
1.说说Redis为什么快
1.1 基于内存存储实现
内存读写是比在磁盘快很多的,Redis基于内存存储实现的数据库,相对于数据存在磁盘的MySQL数据库,省去磁盘I/O的消耗。
1.2 高效的数据结构
Mysql索引为了提高效率,选择了B+树的数据结构。其实合理的数据结构,就是可以让你的应用/程序更快。先看下Redis的数据结构&内部编码图:
1.2.1 SDS简单动态字符串
- 字符串长度处理:Redis获取字符串长度,时间复杂度为O(1),而C语言中,需要从头开始遍历,复杂度为O(n);
- 空间预分配:字符串修改越频繁的话,内存分配越频繁,就会消耗性能,而SDS修改和空间扩充,会额外分配未使用的空间,减少性能损耗。
- 惰性空间释放:SDS 缩短时,不是回收多余的内存空间,而是free记录下多余的空间,后续有变更,直接使用free中记录的空间,减少分配。
1.2.2 字典
Redis 作为 K-V 型内存数据库,所有的键值就是用字典来存储。字典就是哈希表,比如HashMap,通过key就可以直接获取到对应的value。而哈希表的特性,在O(1)时间复杂度就可以获得对应的值。
1.2.3 跳跃表
- 跳跃表是Redis特有的数据结构,就是在链表的基础上,增加多级索引提升查找效率。
- 跳跃表支持平均 O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。
1.3 合理的数据编码
Redis 支持多种数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是redis设计者总结优化的结果。 - String:如果存储数字的话,是用int类型的编码;如果存储非数字,小于等于39字节的字符串,是embstr;大于39个字节,则是raw编码。
- List:如果列表的元素个数小于512个,列表每个元素的值都小于64字节(默认),使用ziplist编码,否则使用linkedlist编码
- Hash:哈希类型元素个数小于512个,所有值小于64字节的话,使用ziplist编码,否则使用hashtable编码。
- Set:如果集合中的元素都是整数且元素个数小于512个,使用intset编码,否则使用hashtable编码。
- Zset:当有序集合的元素个数小于128个,每个元素的值小于64字节时,使用ziplist编码,否则使用skiplist(跳跃表)编码
1.4 合理的线程模型
I/O 多路复用
多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用用epoll作为I/O多路复用技术的实现。并且,Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
2. Redis有几种数据结构,底层分别是怎么存储的
常用的,Redis有以下这五种基本类型:
- String(字符串)
- Hash(哈希)
- List(列表)
- Set(集合)
- zset(有序集合) 它还有三种特殊的数据结构类型
- Geospatial
- Hyperloglog
- Bitmap
2.1 Redis 的五种基本数据类型
String(字符串) - 简介:String是Redis最基础的数据结构类型,它是二进制安全的,可以存储图片或者序列化的对象,值最大存储为512M
- 简单使用举例: set key value、get key等
- 应用场景:共享session、分布式锁,计数器、限流。
- 内部编码有3种,int(8字节长整型)/embstr(小于等于39字节字符串)/raw(大于39个字节字符串)
Hash(哈希) - 简介:在Redis中,哈希类型是指v(值)本身又是一个键值对(k-v)结构
- 简单使用举例:hset key field value 、hget key field
- 内部编码:ziplist(压缩列表) 、hashtable(哈希表)
- 应用场景:缓存用户信息等。
List(列表) - 简介:列表(list)类型是用来存储多个有序的字符串,一个列表最多可以存储2^32-1个元素。
- 简单实用举例:lpush key value [value ...] 、lrange key start end
- 内部编码:ziplist(压缩列表)、linkedlist(链表)
- 应用场景:消息队列,文章列表
Set(集合) - 简介:集合(set)类型也是用来保存多个的字符串元素,但是不允许重复元素
- 简单使用举例:sadd key element [element ...]、smembers key
- 内部编码:intset(整数集合)、hashtable(哈希表)
- 应用场景:用户标签,生成随机数抽奖、社交需求。
有序集合(zset) - 简介:已排序的字符串集合,同时元素不能重复
- 简单格式举例:zadd key score member [score member ...],zrank key member
- 底层内部编码:ziplist(压缩列表)、skiplist(跳跃表)
- 应用场景:排行榜,社交需求(如用户点赞)。
2.2 Redis 的三种特殊数据类型 - Geo:Redis3.2推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作。
- HyperLogLog:用来做基数统计算法的数据结构,如统计网站的UV。
- Bitmaps :用一个比特位来映射某个元素的状态,在Redis中,它的底层是基于字符串类型实现的,可以把bitmaps成作一个以比特位为单位的数组
3. Redis有几种持久化方式
Redis是基于内存的非关系型K-V数据库,既然它是基于内存的,如果Redis服务器挂了,数据就会丢失。为了避免数据丢失了,Redis提供了持久化,即把数据保存到磁盘。
Redis提供了RDB和AOF两种持久化机制,它持久化文件加载流程如下:
3.1 RDB
RDB,就是把内存数据以快照的形式保存到磁盘上。
什么是快照?可以这样理解,给当前时刻的数据,拍一张照片,然后保存下来。
RDB持久化,是指在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis 重启的时候,通过加载dump.rdb文件来恢复数据。RDB触发机制主要有以下几种:
RDB 的优点
适合大规模的数据恢复场景,如备份,全量复制等
RDB缺点
没办法做到实时持久化/秒级持久化。
新老版本存在RDB格式兼容问题
3.2 AOF
AOF(append only file) 持久化,采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的。
AOF的工作流程如下:
AOF的优点
数据的一致性和完整性更高
AOF的缺点
AOF记录的内容越多,文件越大,数据恢复变慢。
4. 多线程情况下,如何保证线程安全?
加锁,比如悲观锁select for update,sychronized等,如,乐观锁,乐观锁如CAS等,还有redis分布式锁等等。
5. 用过volatile吗?它是如何保证可见性的,原理是什么
volatile关键字是Java虚拟机提供的的最轻量级的同步机制,它作为一个修饰符, 用来修饰变量。它保证变量对所有线程可见性,禁止指令重排,但是不保证原子性。
我们先来看下java内存模型(jmm):
- Java虚拟机规范试图定义一种Java内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台上都能达到一致的内存访问效果。
- Java内存模型规定所有的变量都是存在主内存当中,每个线程都有自己的工作内存。这里的变量包括实例变量和静态变量,但是不包括局部变量,因为局部变量是线程私有的。
- 线程的工作内存保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接操作操作主内存。并且每个线程不能访问其他线程的工作内存。
volatile变量,保证新值能立即同步回主内存,以及每次使用前立即从主内存刷新,所以我们说volatile保证了多线程操作变量的可见性。
volatile保证可见性跟内存屏障有关。我们来看一段volatile使用的demo代码:
public class Singleton {
private volatile static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
编译后,对比有volatile关键字和没有volatile关键字时所生成的汇编代码,发现有volatile关键字修饰时,会多出一个lock addl $0x0,(%esp),即多出一个lock前缀指令,lock指令相当于一个内存屏障
lock指令相当于一个内存屏障,它保证以下这几点:
- 重排序时不能把后面的指令重排序到内存屏障之前的位置
- 将本处理器的缓存写入内存
- 如果是写入动作,会导致其他处理器中对应的缓存无效。
第2点和第3点就是保证volatile保证可见性的体现嘛
6. MySQL的索引结构,聚簇索引和非聚簇索引的区别
- 一个表中只能拥有一个聚集索引,而非聚集索引一个表可以存在多个。
- 索引是通过二叉树的数据结构来描述的,我们可以这么理解聚簇索引:索引的叶节点就是数据节点。而非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。
- 聚集索引:物理存储按照索引排序;非聚集索引:物理存储不按照索引排序
文章转自公众号:小白debug