一.Redis
1.redis 大key删除?
在Redis中,由于大key(即存储大量数据的键)可能会对性能产生负面影响,通常需要对它们进行删除或拆分。以下是一些处理大key的方法:
-
使用DEL命令:如果你知道大key的名称,可以直接使用DEL命令来删除它。例如:
DEL large_key
-
使用SCAN命令删除:如果你不知道大key的名称,可以使用SCAN命令进行模式匹配,找到所有符合条件的键,并依次删除它们。例如:
SCAN 0 MATCH large_key_pattern COUNT 1000
上述命令将以每次最多返回1000个匹配键的方式进行扫描。你可以根据需要多次执行该命令,直到找到并删除所有的大key。
-
使用DUMP和RESTORE命令拆分:如果大key中包含较少的独立数据单元,你可以使用DUMP和RESTORE命令将大key拆分为多个小key。首先使用DUMP命令将大key导出为字节码,然后再使用RESTORE命令将字节码导入为多个小key。这种方式可以将大key拆分为更易于管理和查询的小key集合。
以下是一个使用DUMP和RESTORE命令拆分大key的示例:
DUMP large_key | RESTORE new_key
上述命令将大key的数据转储为字节码,并使用RESTORE命令将字节码恢复到一个新的键new_key。
2.Redis 为什么快?
一方面是Redis的大部分操作是在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表。
另一方面,就是Redis采用了多路复用机制。
Redis 网络框架调用 epoll 机制,让内核监听这些套接字。
3.redis hash和string 的使用场景
Redis是一个高性能的内存数据库,它提供了多种数据类型来满足不同的使用场景。在Redis中,Hash和String是常用的数据类型之一。
Redis Hash适用于以下场景:
-
存储对象:Hash可以用来存储对象类型的数据,类似于关联数组。你可以将一个对象存储为一个Hash结构,使用字段(field)和值(value)来表示对象的属性和属性值。这样可以方便地存储和获取对象的各个属性。
-
缓存数据:Hash可以用来缓存大量的数据,比如用户信息、配置文件等。由于Hash的读写操作都是O(1)复杂度的,它非常适合用来作为缓存工具。
-
计数器:Hash可以用来实现计数器功能,通过Hash的原子操作如HINCRBY命令可以对某个字段的值进行原子增加或减少。
Redis String适用于以下场景:
-
缓存数据:String可以用来缓存短期内需要频繁访问的数据,比如简单的键值对、JSON等。
-
分布式锁:String可以用来实现分布式锁功能。通过SETNX命令可以在分布式环境中实现互斥访问,保证关键资源的原子性操作。
-
延迟队列:String可以用来实现延迟队列,通过设置过期时间来实现消息的延迟投递。
总的来说,Redis Hash适合存储结构化数据对象,Redis String适合存储简单的键值对或需要进行原子操作的数据。具体使用场景还需要根据实际的业务需求进行选择。
4.redis的基础数据类型
Redis提供了多种基础数据类型,每种数据类型都有不同的功能和使用场景。下面是Redis的基础数据类型:
-
String(字符串): Redis中最基本的数据类型,可以存储任意类型的数据,比如数字、文本、二进制数据等。String类型支持非常丰富的操作,例如获取、设置、追加等。
-
Hash(哈希): Hash是一个键值对集合,类似于关联数组。在Redis中,Hash可以存储一个对象的多个字段和字段值,非常适合存储对象形式的数据。
-
List(列表): List是一个有序的字符串集合,可以在列表的两端进行插入和删除操作。它支持快速的列表推入(push)和弹出(pop)操作,可以作为栈或队列使用。
-
Set(集合): Set是一个无序的字符串集合,它的成员是唯一的,不允许重复。Set提供了多种集合操作,如交集、并集、差集等,非常适合存储无需重复的数据。
-
Sorted Set(有序集合): Sorted Set是一个有序的字符串集合,它的成员是唯一的,但每个成员都关联着一个分数(score)。成员按照分数排序,可以按照分数范围进行区间查询。
-
Bitmap(位图): Bitmap存储二进制数据,支持位操作和计数。它常用于记录用户的活跃状态、日志记录和精确的计数等。
5.redis 实现按分数排序
使用zset
6.Redis Zset底层实现
前提:保存元素数量小于128,并且每个元素长度小于64字节 (这两个参数可以通过zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改)
ziplist原理: 压缩列表(ziplist)是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
使用字典和跳表。
字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的值,跳跃表节点的 score 属性保存元素的分值。
7.Redis 还有什么应用?
消息队列、广播、分布式锁
二.Mysql
1. char和varchar的区别
在MySQL数据库中,char和varchar是用于存储字符数据的两种数据类型。它们的区别主要体现在以下几个方面:
-
存储方式:char类型用固定长度来存储数据,而varchar类型使用可变长度。例如,如果定义一个char(10)列,不管实际存储的数据长度是1还是10,都将占用10个字符的存储空间;而如果定义一个varchar(10)列,并存储了长度为1的数据,只会占用1个字符的存储空间。
-
存储效率:由于char类型使用固定长度的存储,所以在存储大量短字符串时,char类型可能会更高效,因为不需要额外的存储字节来记录实际长度。
-
数据查询:由于char类型使用固定长度,所以在执行查询操作时可能有些开销,尤其是对长字符串进行查询。而在varchar类型中,需要额外存储字节来记录实际长度,这样在查询时可能会稍微慢一些。
-
存储限制:char类型的存储长度是固定的,所以存储的数据长度必须等于定义的长度或小于其长度;而varchar类型的存储长度是可变的,可以存储任意长度的数据,但受到数据库的最大行大小限制。
综上所述,如果数据长度相对固定且较短,使用char类型可能更合适,因为它在存储和查询效率上可能更高;如果数据长度不定或较长,使用varchar类型可能更合适,因为它可以节省存储空间。
2.int的使用场景
在MySQL中,INT(整数)类型是一种常用的数据类型,适用于表示整数值。以下是一些常见的使用场景:
-
主键和唯一标识符:INT类型可以用来定义主键或唯一标识符列,用于保证每行数据的唯一性和快速检索。
-
计数器和计量器:INT类型可以用来表示计数器或计量器,例如记录网页访问次数、用户点击次数、商品销售数量等。
-
索引列:INT类型可以作为索引列,用于加快查询速度。通过创建索引,可以更快地检索和过滤数据。
-
外键关联:INT类型可以用于定义外键关联列,用于与其他表建立关联关系和维护数据之间的一致性。
-
枚举值映射:INT类型可以用于映射枚举值,将不同的整数值与特定的含义、状态或选项相关联。例如,0表示禁用,1表示启用等。
-
存储时间戳:INT类型可以用来存储时间戳,以记录某个事件的发生时间。通常可以使用UNIX时间戳表示时间,它表示从1970年1月1日午夜起的秒数。
需要根据具体的业务需求和数据特点来选择适当的数据类型。当数据范围超出了INT类型的表示范围时,可以考虑使用BIGINT类型等更大的整数类型。
3.分表
在MySQL中,分表是一种将数据分割存储到不同物理表中的技术,它可以提高数据库性能和扩展性。以下是常见的MySQL分表方法:
水平分表:
-
范围分表:根据数据的某个范围条件(如时间、地理位置等)将数据分散到不同的表中。例如,可以创建以年份为后缀的表名,将每年的数据存储在不同的表中。
-
散列分表:根据数据的一定规则(如哈希函数)将数据分散到不同的表中。例如,可以使用用户ID的哈希值作为散列函数,将用户数据分散存储到不同的表中。
垂直分表:
-
列分表:将表的列进行拆分,在不同的物理表中存储不同的列。这种方式可以将大表拆分为多个小表,提高查询性能。
-
垂直分库:将相关的数据表分散到不同的数据库中。这种方式可以将数据库负载分散到多个服务器上,提高数据库的扩展性和性能。
在进行分表时,需要考虑以下事项:
-
数据表之间的关联关系:分表后,原来的关联关系需要重新考虑和处理。可以使用外键或应用层逻辑来维护关联关系。
-
数据迁移和同步:分表操作可能需要将现有数据迁移到新的分表结构中,并确保新旧表之间的数据同步和一致性。
-
查询优化:分表后,查询语句需要修改以适应新的表结构,可能需要重新优化查询和索引以提高查询性能。
-
数据访问层的改动:分表操作可能需要对应用程序的数据访问层进行修改,以适应新的表结构。
需要根据具体的业务需求和数据特点来选择适当的分表策略,并进行维护和优化。分表可以提高数据库的性能、扩展性和维护性,但同时也增加了系统复杂性和管理难度。
4.分表过程中如何不影响线上业务
在进行MySQL分表过程中,可以采取一些措施来尽量减少对线上业务的影响:
-
预切分:在进行正式的分表之前,可以先进行预分表。将原始数据按照一定规则划分到不同的表中,但仍保留原来的表结构和逻辑。这样可以在不影响线上业务的情况下,逐步积累新表中的数据。
-
数据迁移工具:选择一个合适的数据迁移工具,如pt-online-schema-change等。这些工具可以在分表过程中实现零停机迁移,将原表数据迁移到新分表中,同时保持线上业务可用。
-
分批操作:将分表过程分为多个阶段,逐步进行。每次迁移一小部分数据到新分表中,并确保这个过程不会对线上业务产生较大的负载。可以使用合理的时间窗口或低峰期进行分批操作。
-
数据同步:在分表过程中确保数据的一致性,可以进行实时或定期的数据同步。在每次数据迁移完成后,将新表中的数据与旧表中的数据保持同步,可以通过主从复制、binlog解析等方式来实现。
-
监控和回滚:在执行分表的过程中,需要实时监控关键指标和性能情况,确保对线上业务的影响可控。若出现问题,及时回滚操作,恢复到分表前的状态。
-
定期维护和优化:在分表完成后,进行后续的维护和优化工作,包括删除旧表、调整分表结构、重新分配分表规则等,以提升数据库性能和管理效率。
以上措施可以根据具体情况和业务需求进行调整和组合使用,确保分表过程不会对线上业务产生较大影响,保证业务的连续性和稳定性。同时,建议备份数据和进行彻底的测试,以保证数据的安全性和完整性。
5.Mysql查询没有使用索引的情况
1)where中的字段涉及隐式数据类型转换 users表的a字段建有索引,数据类型为varchar
SELECT * FROM users WHERE a = 1 1 上述语句不会使用到a字段的索引,因为a字段的数据类型是varchar,而SQL中的WHERE条件是a=1,涉及到数字和字符串的隐式转换,应修改为:
SELECT * FROM users WHERE a = '1' 1 应养成WHERE条件都加单引号的习惯。
2)建有联合索引,但查询条件不符合最左匹配原则 users表的a、b字段建有联合索引(ab)
SELECT * FROM users WHERE b = '1' 上述语句不会使用到ab索引,不能跳过a字段,只根据b字段来查询。
3)字段使用了表达式、函数
SELECT * FROM users WHERE b + 1 = 2
SELECT * FROM users WHERE FROM_UNIXTIME(b) = 2 4)模糊查询时,查询条件第一个字符使用了通配符
SELECT * FROM users WHERE b LIKE '%tony' 1 5)使用了OR查询
SELECT * FROM users WHERE a = 1 OR b = 1 1 6)优化器使用错了索引 有时候MySQL的优化器会认为有其它的索引更合适,转而使用其它索引来查询的情况,甚至放弃索引使用全表扫描来查询,如果出现这种情况,可以尝试:
使用FORCE INDEX关键字来强制MySQL使用正确的索引 有可能是索引的统计信息不够准确,导致优化器判断出错,使用ANALYZE TABLE 表名命令来重新统计索引信息
6.Mysql索引底层数据结构
MySQL 是会将数据持久化在硬盘,而存储功能是由 MySQL 存储引擎实现的,所以讨论 MySQL 使用哪种数据结构作为索引,实际上是在讨论存储引使用哪种数据结构作为索引,InnoDB 是 MySQL 默认的存储引擎,它就是采用了 B+ 树作为索引的数据结构。
要设计一个 MySQL 的索引数据结构,不仅仅考虑数据结构增删改的时间复杂度,更重要的是要考虑磁盘 I/0 的操作次数。因为索引和记录都是存放在硬盘,硬盘是一个非常慢的存储设备,我们在查询数据的时候,最好能在尽可能少的磁盘 I/0 的操作次数内完成。
二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn)降低为 O(n)。
为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高。
而树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。
B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。
但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:
-
B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
-
B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;
-
B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。
三.基础
1.CDN为什么快?
CDN(Content Delivery Network)之所以快,是因为它具有以下几个特点:
-
靠近用户的边缘节点:CDN在全球范围内建立了大量的边缘节点,这些节点分布在不同的地理位置,接近用户。当用户请求访问内容时,CDN会将内容缓存到最近的边缘节点上,使得用户可以从离自己更近的节点获取内容,减少了网络传输的距离,从而提高了访问速度。
-
负载均衡和智能路由:CDN会根据实时的网络状况和用户的地理位置等信息,智能地选择最佳的边缘节点来服务用户的请求。通过负载均衡和智能路由算法,CDN能够将用户的请求分发到最优的节点,避免了单一服务器的过载和性能瓶颈,提高了响应速度和吞吐量。
-
静态内容缓存:CDN主要用于分发静态内容,如图片、视频、CSS和JavaScript等。这些静态内容往往具有高重复性和高访问频率,CDN会将这些内容缓存在边缘节点上,当用户请求访问时,可以直接从边缘节点获取内容,无需访问源服务器,提高了访问速度。
-
动态内容加速:除了静态内容,一些CDN还提供动态内容加速的功能。CDN可以缓存动态生成的内容,并使用缓存服务器和边缘计算等技术来提供快速的动态内容传输和处理,减轻了源服务器的负载,加快了内容的传输和渲染速度。
-
减少网络拥塞:CDN通过分散流量到不同的边缘节点来减少网络拥塞。当一个地区的边缘节点过载时,CDN可以将请求导向其他正常的边缘节点,避免了网络瓶颈和延迟问题,确保用户可以快速地获取所需的内容。
综上所述,CDN之所以快速,是通过在全球范围内部署边缘节点、靠近用户,利用缓存、负载均衡和智能路由等技术手段来加速内容传输,减少网络拥塞和传输距离,提高用户的访问速度和体验。
2.WebSocket
WebSocket 是一种协议,用于在 Web 应用程序中创建实时、双向的通信通道。
传统的 HTTP 请求通常是一次请求一次响应的,而 WebSocket 则可以建立一个持久连接,允许服务器即时向客户端推送数据,同时也可以接收客户端发送的数据。WebSocket 相比于传统的轮询或长轮询方式,能够显著减少网络流量和延迟,提高数据传输的效率和速度。它对实时 Web 应用程序和在线游戏的开发非常有用。
WebSocket 可以在浏览器和服务器之间建立一条双向通信的通道,实现服务器主动向浏览器推送消息,而无需浏览器向服务器不断发送请求。其原理是在浏览器和服务器之间建立一个“套接字”,通过“握手”的方式进行数据传输。由于该协议需要浏览器和服务器都支持,因此需要在应用程序中对其进行判断和处理。
四.Go
1.Go切片扩容机制
Go1.18之前切片的扩容是以容量1024为临界点,当旧容量 < 1024个元素,扩容变成2倍;当旧容量 > 1024个元素,那么会进入一个循环,每次增加25%直到大于期望容量。
Go1.18不再以1024为临界点,而是设定了一个值为256的threshold
,以256为临界点;超过256,不再是每次扩容1/4,而是每次增加(旧容量+3*256)/4;
2.Go拼接字符串
1.直接使用运算符
func BenchmarkAddStringWithOperator(b *testing.B) { hello := "hello" world := "world" for i := 0; i < b.N; i++ { _ = hello + "," + world } }1234567
golang 里面的字符串都是不可变的,每次运算都会产生一个新的字符串,所以会产生很多临时的无用的字符串,不仅没有用,还会给 gc 带来额外的负担,所以性能比较差
2.fmt.Sprintf()
func BenchmarkAddStringWithSprintf(b *testing.B) { hello := "hello" world := "world" for i := 0; i < b.N; i++ { _ = fmt.Sprintf("%s,%s", hello, world) } }1234567
内部使用 []byte 实现,不像直接运算符这种会产生很多临时的字符串,但是内部的逻辑比较复杂,有很多额外的判断,还用到了 interface,所以性能也不是很好
3.strings.Join()
func BenchmarkAddStringWithJoin(b *testing.B) { hello := "hello" world := "world" for i := 0; i < b.N; i++ { _ = strings.Join([]string{hello, world}, ",") } }1234567
join会先根据字符串数组的内容,计算出一个拼接之后的长度,然后申请对应大小的内存,一个一个字符串填入,在已有一个数组的情况下,这种效率会很高,但是本来没有,去构造这个数据的代价也不小
4.buffer.WriteString()
func BenchmarkAddStringWithBuffer(b *testing.B) { hello := "hello" world := "world" for i := 0; i < 1000; i++ { var buffer bytes.Buffer buffer.WriteString(hello) buffer.WriteString(",") buffer.WriteString(world) _ = buffer.String() } }
这个比较理想,可以当成可变字符使用,对内存的增长也有优化,如果能预估字符串的长度,还可以用 buffer.Grow() 接口来设置 capacity
3.go defer的执行顺序
在Go语言中,defer语句用于在函数执行结束前进行一些清理操作,它的执行顺序是"后进先出",也就是最后定义的defer语句最先执行,最先定义的defer语句最后执行。下面是一个示例:
goCopyfunc main() { defer fmt.Println("Third") defer fmt.Println("Second") defer fmt.Println("First") }
输出结果为:
First Second Third
在示例中,三个defer语句按定义的顺序入栈,但是在函数执行到结束时才会依次出栈执行。因此,"Third"会最先被打印,然后是"Second",最后是"First"。
需要注意的是,defer语句中的函数参数是在定义时被计算的,而不是在执行时。这意味着如果defer语句中使用的是一个函数调用,它的参数会被立即计算。例如:
goCopyfunc main() { i := 0 defer fmt.Println(i) i++ fmt.Println("Value of i:", i) }
输出结果为:
Value of i: 1 0
在这个示例中,defer语句中的fmt.Println(i)
输出的是defer定义时的i
的值,即0,而不是在函数执行时的最终值1。这是因为defer语句在定义时会对参数进行计算并保存起来。
4.go在什么情况下使用指针?
-
推荐在方法上使用指针(前提是这个类型不是 map、slice 等引用类型)
-
当结构体较大的时候使用指针会更高效,可以避免内存拷贝,“结构较大” 到底多大才算大可能需要自己或团队衡量,如超过 5 个字段或者根据结构体内存占用来计算
-
如果要修改结构体内部的数据或状态必须使用指针
-
如果方法的receiver是map、slice 、channel等引用类型不要使用指针
-
小数据类型如 bool、int 等没必要使用指针传递
-
如果该函数会修改receiver或变量等,使用指针
5.两个协程交替输出1-100的奇偶数
func main() { ch0 := make(chan int) ch1 := make(chan int) go func() { for i := 1; i < 100; i += 2 { <-ch0 fmt.Println(i) ch1 <- 0 } }() go func() { for i := 2; i < 100; i += 2 { <-ch1 fmt.Println(i) ch0 <- 0 } }() ch0 <- 0 select {} }
6.GMP模型
-
全局队列(Global Queue):存放等待运行的
G
-
P的本地队列:同全局队列类似,存放的也是等待运行的
G
,存的数量有限,不超过256
个。新建G'
时,G'
优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G
移动到全局队列 -
P列表:所有的
P
都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS
(可配置)个 -
M:线程想运行任务就得获取
P
,从P的本地队列获取G
,P
队列为空时,M
也会尝试从全局队列
拿一批G
放到P
的本地队列,或从其他P
的本地队列一半放到自己P
的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去
五.算法
1.四十亿个数中取最大的十个数
可以先分治,分成不同的块去处理
-
创建一个大小为10的最小堆(Min Heap)来存储当前最大的十个数。初始时,堆是空的。
-
遍历四十亿个数,并逐个将它们与当前堆中的最小值进行比较。
-
如果当前数比最小堆的最小值大,则用当前数替换最小堆的根节点,并重新调整最小堆,以保持最小堆的性质。
-
如果当前数比最小堆的最小值小,则不进行替换操作,继续处理下一个数。
-
-
最终,堆中存储的就是最大的十个数。
使用堆排序算法的时间复杂度为O(NlogK),其中N为总数的个数,K为需要取的最大数的个数(这里为10)。该算法的优点是只需要维护一个大小为K的最小堆,不需要对整个数据集进行排序,因此可以在较短的时间内求得最大的十个数。
请注意,由于数据量为四十亿个数,需要确保计算机具有足够的内存来处理这么大规模的数据。如果内存不够,可以考虑将数据分块处理,然后针对每个分块运行堆排序算法,最后再将结果进行合并。
2.取两个列表的交集 不能用set和自带的方法
双指针遍历