2019 面试记录
# FunPlus (小视频业务)
# 数据库的事务隔离机制
# 隔离级别
- READ UNCOMMITTED(未提交读)
这个级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,被称为脏读(Dirty Read),这个级别性能不会比其他级别好太多,但缺乏其他级别的很多好处,一般很少使用。
- READ COMMITTED(提交读)
这个级别是大多数数据库系统的默认隔离级别(但 MySQL 不是)。提交读是数据库隔离级别中的一种,它指的是一个事务在读取数据时,不会读取到其他并发事务对同一数据所做的任何修改,即只能读取到已经提交的数据。
在提交读隔离级别下,事务可以读取到其他事务已经提交的数据,但是不能读取到其他事务尚未提交的数据。这意味着一个事务在读取数据时,不会受到其他事务的影响,可以保证读取到的数据是一致的。
提交读隔离级别可以提供较高的并发性能,因为事务可以并发地读取已经提交的数据,而不需要等待其他事务的提交。然而,由于事务在读取数据时不会考虑其他事务的修改,可能会导致不可重复读(nonrepeatable read)(因为两次执行同样的查询,可能会得到不一样的结果。)的问题。
因此,在使用提交读隔离级别时,需要注意处理并发事务可能导致的数据一致性问题,例如通过锁机制或者乐观并发控制来解决。
- REPEATABLE READ(可重复读)
可重复读是数据库隔离级别中的一种,它指的是一个事务在执行期间,多次读取同一数据时,能够保证读取到的数据是一致的,即不会受到其他并发事务对同一数据所做的修改的影响。
在可重复读隔离级别下,事务在读取数据时会获取一个快照(snapshot)来保留事务开始时的数据状态,之后的读取操作都是基于这个快照进行的,而不会受到其他事务的修改的影响。这意味着一个事务在执行期间,无论其他事务如何修改数据,它读取到的数据都是一致的。
可重复读隔离级别可以提供较高的数据一致性,因为事务执行期间的读取操作都是基于一个快照,不会受到其他事务的干扰。但是,由于事务在执行期间可能会有其他事务对同一数据进行修改,可能会导致幻读(Phantom Read)(读取到其他事务插入的数据)的问题。
幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Row)。
因此,在使用可重复读隔离级别时,需要注意处理并发事务可能导致的幻读问题,例如通过锁机制或者多版本并发控制(MVCC)来解决。可重复读是 MySQL 的默认事务隔离级别。
- SERIALIZABLE(可串行化)
串行化是数据库隔离级别中最严格的一种级别。在串行化隔离级别下,事务串行执行,即每个事务在执行时都会对数据进行排他性锁定,确保同时只有一个事务能够对同一数据进行读取或写入操作。
在串行化隔离级别下,事务之间完全隔离,不会出现任何并发冲突。每个事务在执行期间,会对读取和写入的数据加上锁,其他事务无法同时对同一数据进行读取或写入操作,直到当前事务完成。
串行化隔离级别能够提供最高的数据一致性和完整性,因为事务之间完全隔离,不会出现任何并发冲突或数据不一致的情况。但是,由于事务串行执行,可能会导致较低的并发性能,因为每个事务需要等待其他事务释放锁才能执行。
因此,在使用串行化隔离级别时,需要权衡数据一致性和并发性能之间的关系,根据具体的业务需求来选择合适的隔离级别。
所有隔离级别
read uncommitted : 读取尚未提交的数据 :哪个问题都不能解决
read committed:读取已经提交的数据 :可以解决脏读 —- oracle 默认的
repeatable read:可重复读:可以解决脏读 和 不可重复读 —- mysql 默认的
serializable:串行化:可以解决 脏读 不可重复读 和 虚读—-相当于锁表
事务隔离级别 | 脏读 | 可重复读 | 幻读 |
---|---|---|---|
未提交读(read uncommited) | × | × | × |
提交读(read commited) | √ | × | × |
可重复读(repeatable read) | √ | √ | × |
串行化(serialziable) | √ | √ | √ |
注:×表示有该问题,√表示解决该问题。
脏读:事务 A 读取了事务 B 更新的数据,然后 B 进行回滚操作,那么 A 读取到的数据是脏数据;
不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果不一致;
幻读:“当事务 A 要对数据表中某个字段的所有值进修改操作,此时有一个事务是插入一条记录并提交给数据库,当提交事务 A 的用户再次查看时就会发现有一行数据未被修改,其实是事务 B 刚刚添加进去的”,这就是幻读;
隔离级别越高,越能保证数据的完整性和统一性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为 Read Committed。它能够避免脏读,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
- MySQL 事务隔离机制&锁 (opens new window)
- MySQL 隔离级别 (opens new window)
- 数据库事务和四种隔离级别 (opens new window)
- RR(REPEATABLE-READ) 与 RC(READ-COMMITED) 隔离级别的异同 (opens new window)
# flask 组件及源码剖析
- Flask 自带的常用组件介绍 (opens new window)
- session
- flash,消息闪现
- jsonify,返回 json 化数据
- blueprint,构建大型应用条理化
- g,Flask 中的全局变量 g ,可以为特定请求临时存储任何需要的数据并且是线程安全的,当请求结束时,这个对象会被销毁,下一个新的请求到来时又会产生一个新的 g。
- abort,自定义错误
- current_app,应用上下文
- 一个 Flask 应用运行过程剖析 (opens new window)
- Flask 的请求处理流程和上下文 (opens new window)
- flask 源码解析 (opens new window)
- Flask 源码解析:Flask 应用执行流程及原理 (opens new window)
- Flask 面试题 (opens new window)
- Flask 源码解读 | 浅谈 Flask 基本工作流程 (opens new window)
# redis 中的数据类型,其中列表和有序集合有什么区别
Redis 中的列表和有序集合是两种不同的数据结构,具有以下区别:
有序性:列表(List)是一个有序的字符串列表,按照插入顺序进行排序。而有序集合(Sorted Set)是一个有序的字符串集合,每个成员都关联着一个分数,通过分数来进行排序。
元素的唯一性:列表中的元素可以重复,而有序集合中的元素必须是唯一的。
操作的复杂度:列表的插入、删除和查找操作的复杂度是 O(n),其中 n 是列表的长度。而有序集合的插入、删除和查找操作的复杂度是 O(log(n)),其中 n 是有序集合中的元素数量。当数据量特别大的时候,插入操作会特别耗时。
使用场景:列表适合用于实现队列、栈等数据结构,可以通过左端或右端进行插入和删除操作。有序集合适合用于实现排行榜、计数器等场景,可以根据分数进行排序和检索。
总的来说,列表适用于需要保持元素插入顺序的场景,而有序集合适用于需要根据分数进行排序和检索的场景。
# list 列表
List 内部数据结构是双向链表,可以在链表左、右两边分别操作,所以插入数据的速度很快。
也可以把 list 看成一种队列,所以在很多时候可以用 redis 用作消息队列,这个时候它的作用类似于 activeMq;
但是缺点就是在数据量比较大的时候,访问某个数据的时间可能会很长,但针对这种情况,可以使用 zset。
应用案例有时间轴数据,评论列表,消息传递等等,它可以提供简便的分页,读写操作。
# Set 集合
Set 就是一个集合,内部数据结构是整数集合(intset)、HASH 表,集合的概念就是一堆不重复值的组合。利用 Redis 提供的 Set 数据结构,可以存储一些集合性的数据。
比如在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。
因为 Redis 非常人性化的为集合提供了求交集、并集、差集等操作,那么就可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。
- 共同好友、二度好友
- 利用唯一性,可以统计访问网站的所有独立 IP
- 好友推荐的时候,根据 tag 求交集,大于某个阈值(threshold)就可以推荐
# Zset 集合(Sorted Sets)
Sorted Set 有点像 Set 和 Hash 的结合体。
和 Set 一样,它里面的元素是唯一的,但是 Set 里面的元素是无序的,而 Sorted Set 里面的元素都带有一个浮点值,叫做分数(score),内部数据结构跳跃表,所以这一点和 Hash 有点像,因为每个元素都映射到了一个值。 使它在 set 的基础上增加了一个顺序属性,这一属性在添加修改元素的时候可以指定,每次指定后,zset 会自动重新按新的值调整顺序。可以对指定键的值进行排序权重的设定,它应用排名模块比较多。
比如一个存储全班同学成绩的 Sorted Sets,其集合 value 可以是同学的学号,而 score 就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。另外还可以用 Sorted Sets 来做带权重的队列,比如普通消息的 score 为 1,重要消息的 score 为 2,然后工作线程可以选择按 score 的倒序来获取工作任务,让重要的任务优先执行。
zset 集合可以完成有序执行、按照优先级执行
的情况;
- redis 五种数据结构详解(string,list,set,zset,hash) (opens new window)
- Redis 实战 - list、set 和 Sorted Set (opens new window)
# 独到科技
# 进程线程以及协程
进程 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的基本单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信*(IPC,即:Inter-Process Communication)来通信。由于进程比较重,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程 线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。可以利用到并发优势,又可以避免反复系统调用和进程切换造成的开销。
# 区别
- 进程与线程比较
线程是指进程内的一个执行单元,也是进程内的可调度实体。
- 线程与进程的区别:
- 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间;
- 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源;
- 线程是处理器调度的基本单位,但进程不是;
- 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制;
- 协程与线程进行比较
- 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样 Python 中则能使用多核 CPU;
- 线程进程都是同步机制,而协程则是异步;
- 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态; 内核态的线程是由操作系统来进行调度的,在切换线程上下文时,要先保存上一个线程的上下文,然后执行下一个线程,当条件满足时,切换回上一个线程,并恢复上下文。协程也是如此(指需要切换 → 保存 → 恢复上下文)。只不过,用户态的线程不是由操作系统来调度的,而是由程序员来调度的,是在用户态的。
进程和线程、协程的区别 (opens new window)
# 二叉树的深度优先算法和广度优先算法
二叉树的深度优先遍历的非递归的通用做法是采用栈,广度优先遍历的非递归的通用做法是采用队列。 1:树的深度优先遍历主要分为:前序遍历、中序遍历以及后序遍历
- 前序遍历:若二叉树为空则结束,否则依次先访问根节点,然后访问左子树,最后访问右子树。
- 中序遍历:若二叉树为空则结束,否则先访问根节点的左子树,然后访问根节点,最后访问右子数。
- 后序遍历:若二叉树为空则结束,否则先访问根节点的左子树,然后访问右子数,最后访问根节点。 深度优先一般采用递归的方式实现,递归的深度为树的高度。
2:树的广度优先算法:广度优先是按照层次来遍历树的节点,先是根节点,然后依次遍历第二层子节点,当第二层子节点遍历完后,在依次遍历第三层子节点。广度优先采用队列来记录当前可遍历的节点,当遍历某个节点时,将其左孩子和右孩子结点依次入队,待该层遍历完了以后,再依次遍历下一层儿子结点。
3:非递归实现特点: 深度优先一般采用递归实现,如改用非递归,则可需要来模拟栈,当需要先遍历当前节点的儿子结点时(例如中序遍历)需要将其压入栈中,先遍历其儿子结点,然后再将其弹出栈,遍历当前节点。广度优先一般采用非递归来实现,用一个队列来保存依次需要遍历的节点。
- 二叉树深度优先遍历(DFS)和广度优先遍历(BFS) (opens new window)
- 简述树的深度优先算法、广度优先算法,及非递归实现的特点 (opens new window)
- 广度优先搜索(BFS)和深度优先搜索(DFS) (opens new window)
https://nullcc.github.io/2018/06/07/广度优先搜索(BFS)和深度优先搜索(DFS)/ (opens new window)
# Python 垃圾回收机制
引用计数
标记-清除机制
分代技术
# Python 传值还是传引用
python 参数传递采用的是“传对象引用”的方式。这种方式相当于传值和传引用的一种综合。如果函数收到的是一个不可变对象(数字、字符或元组)的引用,就不能直接修改原始对象——相当于通过‘值传递’来传递对象。如果函数收到的是一个可变对象(字典、列表)的引用,就能修改对象的原始值——相当于‘传引用’来传递对象。
Python 传值还是传引用?| 通过对象引用传递 (opens new window)
# 中天联科
# 类属性和实例属性的区别,如何判断类 A 是否有属性 x
class A:
X = 'Hello' # 类属性
def __init__(self,name='World'):
self.name = name # 实例属性
a = A('foo')
print(A.X) # Hello
print(a.name) #foo
print(hasattr(a,'X')) # True
print(hasattr(A,'X')) # True
print(hasattr(A,'name')) # False
print(hasattr(a,'name')) # True
setattr(a,'name','bar')
setattr(A,'X','Bye')
print(a.name) # bar
print(A.X) # Bye
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 什么是列表推导式?如何用一行代码判断一个文件夹下面文件数大于 10 个的子目录
import os
# 不包括嵌套层
subdirs = [subdir for subdir in os.listdir('path_to_folder') if os.path.isdir(os.path.join('path_to_folder', subdir)) and len(os.listdir(os.path.join('path_to_folder', subdir))) > 10]
# 包括嵌套层
subdirs = [dirpath for dirpath, _, filenames in os.walk('D:\codes') if
len(filenames) > 10]
2
3
4
5
6
7
import os
def find_file_more_than_10(root_dir_fp=None, limit=10):
if root_dir_fp is None:
root_dir_fp = os.path.split((os.path.abspath(__file__)))[0]
return [dirpath for dirpath, _, filenames in os.walk(root_dir_fp) if
len(filenames) > limit]
if __name__ == '__main__':
print(find_file_more_than_10())
2
3
4
5
6
7
8
9
10
11
12
# 什么是协程?什么是生成器
- 生成器
yield
和生成器表达式(i for i in range(10))
。
# 金山云
# 单例模式,如何实现?以及如何判断只有这一个实例
class Singleton:
_instance = {}
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance
# 装饰器实现法
def deco_singleton(cls):
_instance = {}
def wrapper(*args, **kwargs):
if not cls in _instance:
_instance[cls] = cls(*args, **kwargs)
return _instance[cls]
return wrapper
@deco_singleton
class A:
pass
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 死锁产生的原因
# ping 域名的过程
ping 某域名的过程详解 (opens new window)
# select 和 epoll 区别
- Python 全栈之路系列之 IO 多路复用 (opens new window)
- select、poll、epoll 之间的区别总结[整理] (opens new window)
- Python 异步非阻塞 IO 多路复用 Select/Poll/Epoll 使用 (opens new window)
- Python 使用 select 和 epoll 实现 IO 多路复用实现并发服务器 (opens new window)
- How To Use Linux epoll with Python (opens new window)
# 艾普艾
# Redis的数据类型都有哪些,如果要实现计数器功能,应该选用哪种数据类型?使用 Redis,如果内存满了会怎么样
- 数据类型 string,list,set,zset,hash
- 计数器/限流器功能
- 可以选用
string
类型,调用incr()
方法,参见INCR (opens new window) 每次自增加 1
redis> SET mykey "10"
"OK"
redis> INCR mykey
(integer) 11
redis> GET mykey
"11"
2
3
4
5
6
- 可以选用
hash
类型,调用hincrby()
方法,参见HINCRBY (opens new window) 对关联的统计项进行统一管理;
redis> HSET myhash field 5
(integer) 1
redis> HINCRBY myhash field 1
(integer) 6
redis> HINCRBY myhash field -1
(integer) 5
redis> HINCRBY myhash field -10
(integer) -5
2
3
4
5
6
7
8
- 可以选用
set
类型,调用sadd()
方法,参见SADD (opens new window) 多次调用只加一,防作弊刷数据等;
redis> SADD myset "Hello"
(integer) 1
redis> SADD myset "World"
(integer) 1
redis> SADD myset "World"
(integer) 0
redis> SMEMBERS myset
1) "Hello"
2) "World"
2
3
4
5
6
7
8
9
更多 Python 实例应用:Redis 多方式实现计数器功能(附代码) (opens new window)
# 内存满了
此时不能继续写入数据,而且系统的其他操作任务也会受到影响。为防止这种现象发生,应该启用内存淘汰策略。
# 更多
10 个常见的 Redis 面试"刁难"问题 (opens new window)
# 常见状态码错误?301、302 错误及区别?502 错误出现时应该怎么解决
- 301/302 跳转,301 redirect: 301 代表永久性转移(Permanently Moved);302 redirect: 302 代表暂时性转移(Temporarily Moved )
参见http 状态码 301 和 302 详解及区别——辛酸的探索之路 (opens new window)
- 502 Bad Gateway Error
对用户访问请求的响应超时错误
- DNS 测试,ping 测试
- 检查防火墙端口,检查防火墙日志
- 数据库调用延迟
- 网络服务进程是否正常
# 参考
# 华胜天成
# 类属性的继承
class Parent:
x = 10
class Child1(Parent):
pass
class Child2(Parent):
pass
a = Parent()
b = Child1()
c = Child2()
print(a.x,b.x,c.x) # (10, 10, 10)
a.x = 20
print(a.x,b.x,c.x) # (20, 10, 10)
b.x = 30
print(a.x,b.x,c.x) # (20, 30, 10)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A:
x = 'a'
class B:
x = 'b'
class C(A,B):
pass
class D(B,A):
pass
print(A.x,B.x,C.x,D.x) # ('a', 'b', 'a', 'b')
A.x = 'a1'
print(A.x,B.x,C.x,D.x) # ('a1', 'b', 'a1', 'b')
B.x = 'b1'
print(A.x,B.x,C.x,D.x) # ('a1', 'b1', 'a1', 'b1')
C.x = 'c'
print(A.x,B.x,C.x,D.x) # ('a1', 'b1', 'c', 'b1')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在多继承中,当一个类有多个父类时,它会按照从左到右的顺序搜索属性和方法。优先找到谁就继承谁的属性。