面试题


递归遍历所有的 文件夹和文件


from pathlib import Path

def print_directory_contents(s_path):
    path = Path(s_path)
    for child in path.iterdir():
        if child.is_file():
            print(child.resolve())
        elif child.is_dir():
            print_directory_contents(child.resolve())

# 具体实现步骤如下:

    # 输入文件夹路径。
    # 将路径转换成Path对象。
    # 使用Path.iterdir()函数获取该文件夹中所有文件和文件夹的名称。
    # 遍历列表中的每一个子项。
    # 如果该路径是一个文件,打印该文件的绝对路径。
    # 如果该路径是一个文件夹,递归调用自身,传入该路径。
    
# 需要注意的是,Path库是从Python 3.4版本开始引入的,如果是旧版本的Python,需要使用os模块来操作文件和文件夹。

import os

def print_directory_contents(s_path):
    for s_child in os.listdir(s_path):
        s_child_path = os.path.join(s_path, s_child)
        if os.path.isdir(s_child_path):
            print_directory_contents(s_child_path)
        else:
            print(s_child_path)

输入日期, 判断这一天是这一年的第几天

import datetime

def day_of_year(date):
    year_beginning = datetime.datetime(date.year, 1, 1)
    delta = date - year_beginning
    return delta.days + 1

打乱一个排好序的list对象

import random

my_list = [1, 2, 3, 4, 5]

#打乱列表
random.shuffle(my_list)

print(my_list)

# 该函数会直接修改原列表,因此请确保您已经备份好了需要打乱顺序的原始列表

字典按value值进行排序

# 方案一:使用sorted()函数和lambda表达式

my_dict = {'apple': 5, 'banana': 3, 'orange': 2, 'pear': 1}

# 使用sorted()函数和lambda表达式按照value排序
sorted_dict = sorted(my_dict, key=lambda x: x[1])

print(sorted_dict)
# 输出结果:
# 
# {'pear': 1, 'orange': 2, 'banana': 3, 'apple': 5}



# 方案二:使用operator模块的itemgetter()函数
import operator

my_dict = {'apple': 5, 'banana': 3, 'orange': 2, 'pear': 1}

# 使用itemgetter()函数按照value排序
sorted_dict = dict(sorted(my_dict.items(), key=operator.itemgetter(1)))

print(sorted_dict)
# 输出结果:
# 
# {'pear': 1, 'orange': 2, 'banana': 3, 'apple': 5}

字典设置默认值 defaultdict

defaultdict 是 Python 内置字典类(dict)的一个子类,它重载了一个方法并添加了一个可写的实例变量。

`defaultdict` 类允许给定的默认类型作为工厂函数,这样不存在的键就返回这个默认类型对应的初始值

dict_by_sale_email = defaultdict(list)

下面是 defaultdict 的几种应用场景:

  • 字典初始化:使用 defaultdict 可以对一个字典在初始化时就设定所有不存在的键的默认值,可以让代码更简洁明了,例如:
from collections import defaultdict

person = defaultdict(int)  # 所有未定义的键将获得 int 对应的空值
person['age'] += 1
person['age'] += 1
person['age'] += 1
print(person['age'])  # 这将输出 3,因为 age 变量初值为 0

  • 分组累加:在从其它数据源聚合数据时,可以使用 defaultdict 对数据进行分组并累加得到最终结果,例如统计字符串中单词出现的次数:
from collections import defaultdict

s = "the quick brown fox jumps over the lazy dog"
words = s.split()
word_count = defaultdict(int)

for word in words:
    word_count[word] += 1

print(word_count)  # 会输出一个字典,其中单词和对应的次数都被打印出来
  • 处理缺失值:在数据处理过程中,有些数据可能会出现缺失的情况,可以使用 defaultdict 在初始值为 None 或 0 的情况下进行处理,例如:
from collections import defaultdict

data = [
    {"name": "张三", "age": 18},
    {"name": "李四", "age": 20},
    {"name": "王五"}
]
people = defaultdict(lambda: {"name": None, "age": 0})

for person in data:
    people[person["name"]]["name"] = person["name"]
    if "age" in person:
        people[person["name"]]["age"] = person["age"]

print(dict(people))  # 会输出一个字典,其中缺失 age 的数据将被填充为 0

总之,defaultdict 主要用于创建默认值非空的字典统计频数以及分组累加等场景,可以简化代码实现和提高代码可读性。

列表切片操作不会抛出IndexError异常

因为列表切片操作不会抛出IndexError异常。如果切片结束位置超出了列表的长度,则返回一个空列表。此时,切片索引不合法,但不会导致程序异常。

l = ['a','b','c','d','e']
print(l[10:]) 

# []
列表切片的实现机制是在底层使用了C语言实现,具体实现原理如下:

    对列表进行切片操作时,会从列表的头部或尾部偏移切片索引,并以切片步长为间隔,提取需要的元素,组成一个新的列表。
    
    如果切片的起始位置和结束位置都没有超出列表长度,则直接从内存中按照索引提取元素,返回一个新的列表。
    
    如果切片的结束位置超出了列表长度,则会在新的列表中只提取列表中存在的元素,如果列表中不存在对应的元素,则返回一个空列表。
    
    如果切片的起始位置超出了列表长度,则返回一个空列表。

在所有情况下,切片操作都不会修改原有列表,而是返回一个新的列表。

给定两个列表,怎么找出他们相同的元素和不同的元素

list1 = [1, 2, 3, 4, 5]
list2 = [3, 4, 5, 6, 7]

set1 = set(list1)
set2 = set(list2)

print(set1 & set2)
# {3, 4, 5}
print(set1 ^ set2)
# {1, 2, 6, 7}
print(set1 | set2)
# {1, 2, 3, 4, 5, 6, 7}

python新式类和经典类的区别?

Python2 中,有新式类经典类的区别

Python3 中默认只有一种类型的类,即都是新式类

下面是它们的区别:

  • 1 经典类不继承 object新式类继承 object

在 Python3 中所有类默认继承 object,而在 Python2 中,如果一个类没有显式地继承自 object 或其子类,那么这个类就是一个经典类。在 Python2 中,如果你需要使用新式类的特性,必须显式地指定该类继承自 object 或它的子类,例如:

class MyClass(object):
    pass
  • 2 方法解析顺序(Method Resolution Order,MRO)不同

经典类的属性查找方式是按照从子类到父类的顺序进行搜索

新式类在搜索属性时,会按照一个特定的算法(C3算法)在类的继承层次结构中进行搜索,该算法确保所有基类的属性仅被访问一次,且按照正确的顺序进行搜索。

  • 3 多继承的 MRO 不同

在多继承情况下

新式类MRO 算法确保每个基类仅被访问一次,且按照正确的线性顺序进行访问

经典类则是深度优先的搜索方式。

这就导致,当继承树中存在钻石继承时,新式类不会出现属性查找顺序的歧义,而经典类则可能导致与预期不符的结果。

  • 4 内置函数 type 的行为不同

Python2 中,当使用 type 函数创建一个类时,如果它继承自经典类(没有显式地继承自 object),那么它的一些特性与新式类是不同的。

Python3 中,所有类都经过优化,使得 type创建的类与通过 class 关键字定义的类没有区别。

总结来说,Python2 中的新式类比经典类功能更强大,具有更多的特性,同时也修复了一些经典类存在的问题; 而 Python3 的默认统一采用新式类,避免了这些问题。

Python 3 中有以下几种内置数据结构类型:

列表(List):可变序列,元素可以是任何类型。
元组(Tuple):不可变序列,元素可以是任何类型。
集合(Set):无序,不重复的集合,支持交、并、差等基本操作。
字典(Dictionary):键-值对映射的散列表,键和值都可以是任何类型。
字符串(String):不可变序列,字符串是字符的集合。
此外,Python 3 中还有一些其他内置类型,例如布尔型(bool)、整型(int)、浮点型(float)、复数型(complex)等。这些类型是基础类型,用于表示程序的基本数据。


Python 3 中内置的数字类型有整型(int)、浮点型(float)和复数型(complex)。
    
    整型(int):用于表示整数,Python 3 中的整型是任意精度的整数,可以表示任意大小的整数,不受位数限制。
    
    浮点型(float):用于表示浮点数,即小数。Python 3 中的浮点型采用 IEEE754 标准,支持无限大、NaN、正负零等特殊值,可表示的精度是 10^-16 级别。
    
    复数型(complex):用于表示复数,即有实部和虚部的数。Python 3 中的复数型支持常规的加、减、乘、除等算术操作,也支持求模、求共轭等操作。

这些数字类型之间的区别主要在于所能表示的数据范围、精度和支持的操作等方面。在使用时需要根据实际需要选择合适的数据类型。
如果数据量很大或需要高精度的计算,可以选择整型;如果需要使用小数或浮点数,可以选择浮点型;如果需要表示复数,可以选择复数型。

python如何实现单例模式?请写出两种实现方式?

from functools import wraps

# 1 装饰器
def singleton(mcs):
    instance = {}

    @wraps(mcs)
    def get_instance(*args, **kw):
        if mcs not in instance:
            instance[mcs] = mcs(*args, **kw)
        return instance[mcs]
    return get_instance


# 元类创建
class SingletonClass(type):
    _instance = {}

    def __call__(mcs, *args, **kw):
        if mcs not in mcs._instance:
            mcs._instance[mcs] = super(SingletonClass, mcs).__call__(*args, **kw)
        return mcs._instance[mcs]


# 3 类初始化
class SingleParentClass:
    _instance = {}

    def __new__(mcs, *args, **kwargs):
        if mcs not in mcs._instance:
            mcs._instance[mcs] = super(SingleParentClass, mcs).__new__(mcs, *args, **kwargs)
        return mcs._instance[mcs]


@singleton
class A:
    ...


class B(metaclass=SingletonClass):
    ...


class C(SingleParentClass):
    ...


a = A()

b = A()

print(a == b)

# True


c = B()
d = B()

print(c == d)
# True


f = C()

m = C()

print(f == m)
# True

元类的执行过程

class MetaF(type):
    def __init__(cls, *args, **kw):
        print(f"MetaF __init__")
        super(MetaF, cls).__init__(*args, **kw)

    def __call__(cls, *args, **kwargs):
        print(f"MetaF __call__")
        return super(MetaF, cls).__call__(*args, **kwargs)

    def __new__(mcs, *args, **kwargs):
        print(f"MetaF __new__")
        return super(MetaF, mcs).__new__(mcs, *args, **kwargs)


# 也就是  class = MetaF("F", (), {})
class F(metaclass=MetaF):

    def __init__(self):
        print("F __init__")

    def __new__(cls, *args, **kwargs):
        print("F __new__")
        return super(F, cls).__new__(cls, *args, **kwargs)

    def __call__(self, *args, **kwargs):
        print("F __call__")


        
# MetaF __new__
# MetaF __init__
# MetaF __call__
# F __new__
# F __init__
# F __call__

class F(metaclass=MetaF):
    ...

    MetaF __new__       执行MetaClas(type)的 __new__ 方法(类是type的对象)
    MetaF __init__      执行MetaClas(type)的 __init__ 方法(类是type的对象)


f = F()
    MetaF __call__      执行MetaClas(type)的 __call__ 方法(类是type的对象)
    F __new__
    F __init__

f()
    F __call__          执行F的 __call__ 方法(f 是 F对象)

反转数字


# 字符串切片
def reverse_number(number: int):
    tmp_str = str(number)
    if tmp_str[0] == "-":
        return int(f"-{tmp_str[:0:-1]}")
    return int(f"{tmp_str[::-1]}")

s = reverse_number(2312345)
print(s)

遍历修改列表 (删除元素)


# 1 使用倒序(索引递减), 避免索引超出范围


a = list(range(0, 9))

for i in range(len(a) - 1, -1, -1):
    if a[i] % 2 == 0:
        a.remove(a[i])

print(a)



# 2 利用 for ... else ..., 

# 每次删除元素后跳出遍历,直达删除全都

a = list(range(0, 9))


while 1:
    for i in a:
        if i % 2 == 0:
            a.remove(i)
            break
    else:
        break
        
print(a)

is== 有什么区别?

is:比较的是两个对象的id值是否相等,也就是比较俩对象是否为同一个实例对象。是否指向同一个内存地址

== : 比较的两个对象的内容/值是否相等,默认会调用对象的eq()方法

Python中变量的作用域

Python中变量的作用域分为全局作用域局部作用域

全局作用域:

在程序的整个范围内都可以访问的变量,即全局变量。

全局变量可以在程序的任何位置被访问和修改,包括函数内部和函数之外。在函数内部如果要修改全局变量的值,需要在函数内部使用 global 关键字声明。

示例代码:

global_var = "Global"

def test_func():
    print(global_var)

test_func() # 输出 Global

def modify_func():
    global global_var
    global_var = "Modified Global"

modify_func()
print(global_var) # 输出 Modified Global

局部作用域:

在函数内部定义的变量,只能在该函数内部被访问的变量,即局部变量。

在函数外部无法访问局部变量,也不能在函数外部修改局部变量的值。

示例代码:

def test_func():
    local_var = "Local"
    print(local_var)

test_func() # 输出 Local

print(local_var)  # 会报错,因为局部变量只能在函数内部访问

如果需要在函数中访问全局变量,可以使用 global 关键字将其声明为全局变量。

python 中 变量的查找顺序

Python 中变量的查找顺序由 LEGB 规则确定,即

从内到外依次查找局部变量(Local)

封闭函数中的变量(Enclosing)

全局变量(Global)

内置变量(Built-in)

如果在当前作用域内找不到变量,就会往外层作用域查找,直到找到为止。

也就是说,在嵌套的函数中,内部函数可以访问外部函数的变量,而外部函数不能访问内部函数的变量

示例代码如下:

global_var = "Global"

def outer_func():
    outer_var = "Outer"
    
    def inner_func():
        inner_var = "Inner"
        print(inner_var) # Inner
        print(outer_var) # Outer
        print(global_var) # Global
        
    inner_func()
    print(outer_var) # Outer

outer_func()

在上述代码中,

global_var 是全局变量,可以在任何位置访问;

outer_var 是外部变量,只能在外部函数和内部函数中访问;

inner_var 是内部变量,只能在内部函数中访问。

其中,内部函数 inner_func 可以访问全局变量、外部变量和内部变量,而外部函数 outer_func 只能访问全局变量和外部变量。

字符串数字 转换成 数字 (不使用内部API)

根据 ASCII 码将其转换为数字

0 到 9 这几个数字字符的 ASCII 码是连续的(48 到 57)

根据位数进行乘法运算得到最终结果
s = "123"
ans = 0
for c in s:
    ans = ans * 10 + ord(c) - ord('0')
    
print(ans) # 123

两个有序列表,l1,l2,对这两个列表进行合并 不使用extend

可以使用循环来进行列表合并,具体步骤如下:
    
    建立一个新的空列表result来存储合并后的数据。
    
    通过两个指针i,j来分别指向l1和l2的第一个元素。
    
    循环遍历,比较i和j所指向的元素,将较小值加入result,同时指针向后移动一位。
    
    当其中一个列表被遍历完时,将另一个列表的剩余元素直接加入result。
    
    返回合并后的结果列表result。
l1 = [1, 2, 3, 4, 5, 6]
l2 = [3, 4, 5, 6, 7, 8, 9, 10]


def merge_order_list(l_1, l_2):
    result = []

    i, j = 0, 0

    while i < len(l_1) and j < len(l_2):
        if l_1[i] < l_2[j]:
            result.append(l_1[i])
            i += 1
        else:
            result.append(l_2[j])
            j += 1
    
    # 超出边界
    if i == len(l_1):
        result += l_2[j:]
    else:
        result += l_1[i:]

####

def multi():
    return [lambda x: i*x for i in range(4)]


for m in multi():
    print(m(3))
    
# 9
# 9
# 9
# 9
# 因为,最后函数被调用的时候,for循环已经完成, i 的值最后是3,因此每一个返回值的i都是3,所以最后的结果是[9,9,9,9]



# 生成器 遍历时候才声明
def multi_gen():
    return (lambda x: i*x for i in range(4))


for m in multi_gen():
    print(m(3))

# 0
# 3
# 6
# 9

Cython,Jython, Pypy Cpython Numba各有什么缺点

Cython:

    需要编写Cython代码,具有一定的学习和编写成本。
    移植性相对较差,需要适配不同的操作系统和编译器。

Jython:

    对于某些Python库或应用程序支持不完整或不稳定。
    性能相对较慢,尤其是在处理大量数据时。

Pypy:
    
    优化的是Python的解释器,而不是源代码。
    不支持部分Python库的加速。
    在处理多线程和内存占用方面存在一些问题。

CPython:

    解释器运行效率相对较慢,尤其在处理大量数据时。
    由于GIL的存在,多线程性能相对较差

Numba:

    只适用于一些数值计算和科学计算领域,而并不支持所有Python库的加速。
    需要与NumPy或其他Cython或C库配合使用。
    可读性相对较差,代码可维护性相对较弱。

总的来说,这些库和解释器都有各自的优点和缺点。

CythonNumba适用于科学计算和数值计算领域,但需要较高的编写和学习成本。 PypyJython具有一定的跨平台性和代码可读性,但在性能和Python库的支持上存在一些问题。

CPython是Python语言的默认解释器,但在处理大量数据和多线程方面存在缺陷。

抽象类接口类的区别和联系

在Python中,抽象类和接口类都是通过继承abc.ABC类来实现的

区别

1) 接口类中只能定义抽象方法,而 抽象类中可以定义抽象方法和具体方法。

2) 抽象类中可以定义抽象属性和具体属性,而接口类不能定义属性。

3) 子类继承抽象类时可以选择实现抽象方法,也可以不实现,但继承接口类时必须实现接口中的所有抽象方法。

联系

抽象类和接口类都是用于约束类的行为。

抽象类和接口类都是不能被直接实例化的。

下面是一个抽象类和接口类的例子:

import abc

# 抽象类
class Animal(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    # 抽象方法
    def talk(self):
        pass
    # 具体方法
    def run(self):
        print("Animal is running...")

class Dog(Animal):
    def talk(self):
        print("Dog is barking...")

d = Dog()
d.talk()  # Dog is barking...
d.run()   # Animal is running...

# 在这个例子中,Animal是一个抽象类,其中talk是一个抽象方法,run是一个具体方法。
# Dog通过继承Animal类,实现了talk方法,并继承了run方法。


# 接口类
class Shape(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def area(self):
        pass
    
    @abc.abstractmethod
    def perimeter(self):
        pass

class Circle(Shape):
    def __init__(self, r):
        self.r = r
        
    def area(self):
        return 3.14 * self.r * self.r
    
    def perimeter(self):
        return 2 * 3.14 * self.r

c = Circle(5)
print(c.area())       # 78.5
print(c.perimeter())  # 31.4


# Shape是一个接口类,其中area和perimeter都是抽象方法。
# Circle通过继承Shape类,实现了area和perimeter方法。

哪些操作会导致Python内存溢出,怎么处理

Python内存溢出通常是由于程序分配了过多的内存,而Python没有及时释放内存导致的。

以下是一些可能导致Python内存溢出的操作:

创建大量对象并保留对它们的引用,尤其是大对象。

对已经创建的大型数据结构进行不必要的复制。

递归调用过多层次或者发生死循环等。

读取大量数据到内存中并在内存中保留数据。

使用一些内存占用高的库或者模块,如一些科学计算库numpy、pandas等。

以下是一些处理Python内存溢出的方法:

通过减少保留大型对象的引用,及时清理无用的对象。

尽量避免不必要的复制,通过对大型数据进行迭代处理等方式,避免一次性将整个数据复制到内存中。

对于递归调用层数过多的函数,可以考虑改写非递归方式实现。

在读取大量数据时,可以适当增加硬件资源,如增加内存、优化硬盘寻址等策略。

使用内存占用较低的库或模块,优化代码结构,避免内存泄漏等。

利用Python的垃圾回收机制,在程序运行过程中自动回收无用的内存,可以避免一些内存泄漏问题。

分析内存使用情况,借助一些工具如Python的 memprofile(第三方库)、heapy、pympler等对内存泄漏点进行分析和优化。

总之,避免Python内存溢出的关键在于优化程序设计,合理使用内存资源和规避一些可能导致内存泄漏的操作。

Python的内存管理机制:

引用计数:Python使用引用计数来追踪每个对象的引用次数。

    当一个对象被赋值给一个变量时,它的引用次数会增加;
    当这个变量被删除时,它的引用次数会减少。
    当一个对象的引用次数为0时,它将被自动删除。
        
引用追踪:当一个对象的引用次数为0时,Python会将其从内存中删除。

    为了保证对象之间的正确性,Python在内部维护了一个链表,用于追踪每个对象的引用情况。

循环引用处理:当两个对象互相引用时,如果仅使用引用计数,这两个对象的引用计数将永远不为0,导致内存泄漏。

    为了解决这个问题,Python使用了标记-清除算法和分代垃圾回收。


    
    Python的标记-清除算法和分代垃圾回收的实现如下:
    
    标记-清除算法
    标记-清除算法是一种基本的垃圾回收算法,它通过标记所有活动对象,在清除所有未被标记的非活动对象来回收内存空间。Python的垃圾回收机制会定期触发标记-清除算法,并使用两个标记,即可达标记和不可达标记,来确定哪些对象可以被回收。
    
    分代垃圾回收
    分代垃圾回收算法是一种优化的垃圾回收算法,它将对象分为不同的代,并使用不同的垃圾回收策略。Python的分代垃圾回收机制会将所有对象分为三代,分别是0代、1代和2代,每代对象的回收策略也不同。其中,0代对象的回收策略是基于频度,1代对象的回收策略是基于时间,而2代对象的回收策略则是基于空间。当对象在分代垃圾回收过程中经过多次回收仍未被回收时,会被转移至更高一代的对象中进行进一步回收。

调优手段:

尽量缩小变量作用域,及时删除不需要的对象。

使用生成器而不是列表来存储大量数据。

使用标准库中的内置函数,如map、filter、reduce等,减少循环次数和内存占用。

使用numpy代替纯Python代码处理大量数据。

使用Cython或其他编写Python扩展的工具,将性能敏感的代码实现为C语言扩展。

使用内存映像文件而不是将数据加载到内存中。
            
    以下是一个简单的例子,演示如何使用Python标准库中的mmap模块来实现内存映像文件:
    
    import mmap
    
    # 打开文件并将它映射到内存中
    with open("data.txt", "r+b") as f:
        # 读取文件的大小并将其映射到内存中
        file_size = os.stat("data.txt").st_size
        mmapped_file = mmap.mmap(f.fileno(), file_size)
    
        # 从内存映像文件中读取数据,这里假设文件中存储的是一些字符串
        print(mmapped_file.readline())
        print(mmapped_file.readline())
        print(mmapped_file.readline())
    
        # 在内存映像文件中查找某个字符串并替换它
        mmapped_file.seek(0)
        search_str = b"hello"
        replace_str = b"hi"
        replace_count = 0
        while True:
            found_pos = mmapped_file.find(search_str)
            if found_pos == -1:
                break
            mmapped_file[found_pos:found_pos+len(search_str)] = replace_str
            replace_count += 1
    
        # 将修改的内容同步写回到磁盘上
        mmapped_file.flush()
    
        # 解除映射关系
        mmapped_file.close()

    在这个例子中,我们首先打开一个名为data.txt的文件,并将它映射到内存中。然后我们从内存映像文件中读取了一些数据,并在内存映像文件中查找并替换了某个字符串。最后,我们将修改后的内容同步写回到磁盘上,并解除内存映射关系。这种方式可以大大减少文件读写所需的时间和资源,提高程序的效率。

对于长生命周期的对象,尽量使用较小的数据类型,如使用int代替float等。

使用局部变量代替全局变量,减少内存占用。

使用第三方工具和库,例如memory_profiler、objgraph和pympler,帮助识别和解决内存泄漏和性能瓶颈问题。

Mysql怎么限制IP访问

可以通过MySQL访问控制列表(ACL Access Control List)限制IP访问

ACL包括允许访问的用户服务器IP地址范围。 在MySQL中,有两个系统表存储着这些信息,分别是mysql.usermysql.db。`

如果要限制某个用户特定IP地址下的访问权限,可以使用以下命令:

GRANT ALL PRIVILEGES ON database_name.* TO 'username'@'ip_address' IDENTIFIED BY 'password';

    database_name是数据库名
    username是用户名
    ip_address是允许访问的IP地址,password是密码。

    这样就限制了该用户只能从指定的IP地址访问该数据库。

如果要限制所有用户特定IP地址下的访问权限,可以在mysql.user表中添加一条记录(如果没有的话):

INSERT INTO mysql.user (Host, User, Password) VALUES ('ip_address', '%', PASSWORD('password'));

    ip_address是允许访问的IP地址,’%’表示所有用户,password是密码。

    这样就限制了所有用户只能从指定的IP地址访问MySQL服务器。

添加完记录后,需要使用以下命令使其生效:

FLUSH PRIVILEGES;

这样就可以限制IP访问了。

当然,在生产环境下,我们通常还需要使用防火墙等其他方法加强安全性。

设计模式

设计模式是一种解决软件设计问题的通用方法,它可以在特定情况下提供优雅、高效、可维护的解决方案。

设计模式是由经验丰富的开发人员创建的,它们已经反复证明,在不同的应用环境中都是可行的。

以下是几个比较常见的设计模式:

工厂模式(Factory Pattern)
    工厂模式是一种创建型设计模式,它允许开发人员通过抽象工厂类来创建一组相关或相互依赖的对象,而不必指定它们的具体类。这样的方式可以使代码更加灵活和可扩展。

单例模式(Singleton Pattern)
    单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供全局访问点来访问该实例。它通常用于管理共享资源,如一个数据库连接池。

观察者模式(Observer Pattern)
    观察者模式是一种行为型模式,它定义了对象之间的一对多依赖关系,使得当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。

策略模式(Strategy Pattern)
    策略模式是一种行为型模式,它定义了一系列算法,将每个算法都封装起来,并使它们之间可以互换。该模式使算法的变化独立于使用它们的客户端。

装饰器模式(Decorator Pattern)
    装饰器模式是一种结构型模式,它允许开发人员在运行时给一个对象动态地添加额外的职责,而不会影响到其他对象。
    这种方式可以避免使用子类创建复杂的对象,同时也在一定程度上实现了开闭原则。

以上是我了解的几个设计模式,它们都可以帮助开发人员设计出优秀的软件,在实际项目中有广泛应用。

python 面向对象有三大特性:封装、继承、多态

封装

封装可以通过将数据和方法封装在一个类中,防止外部代码直接访问对象的内部数据和方法。

可以通过类的访问控制实现封装,例如:

# 定义一个Person类,将数据和方法封装在类中
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def say_hello(self):
        print("Hi, my name is", self.name, "and I am", self.age, "years old.")

# 创建一个Person对象并调用对象的公开方法
p = Person("John", 30)
p.say_hello()

# 尝试直接访问对象的数据,将会引发AttributeError异常
print(p.name)

# 在这个示例中,Person类将数据和方法封装起来,外部代码无法直接访问对象的内部数据self.name。
# 而使用公开的方法say_hello()来间接访问数据。

继承

继承允许一个类继承另一个类的属性和方法,并可以在子类中添加、重写或者替换父类的属性和方法。例如:
# 定义一个Animal类,它有一个公共方法make_sound()
class Animal:
    def make_sound(self):
        pass

# 定义一个Dog类,它继承自Animal类,并重写父类的方法make_sound()
class Dog(Animal):
    def make_sound(self):
        print("Woof")

# 定义一个Cat类,它继承自Animal类,并替换父类的属性和方法
class Cat(Animal):
    def __init__(self, name):
        self.name = name

    def make_sound(self):
        print("Meow")

# 创建一只狗和一只猫,并调用它们的公共方法make_sound()
d = Dog()
c = Cat("Tom")

d.make_sound()
c.make_sound()
# 在这个示例中,Dog和Cat类都继承自Animal类,它们分别重写或替换了父类的方法,实现了自己的make_sound()方法。

多态

多态指的是同一种对象在不同的情况下表现出不同的行为。在Python中,多态可以通过重载运算符、接口和类方法等方式实现。例如:
# 定义一个Animal类,它有一个公共方法make_sound()
class Animal:
    def make_sound(self):
        pass

# 定义一个Dog和Cat类,它们都重载了父类的make_sound()方法
class Dog(Animal):
    def make_sound(self):
        print("Woof")

class Cat(Animal):
    def make_sound(self):
        print("Meow")

# 定义一个函数,它接受一个Animal对象并调用make_sound()方法
def make_some_noise(animal):
    animal.make_sound()

# 创建一只狗和一只猫,并将它们传递给函数make_some_noise()
d = Dog()
c = Cat()

make_some_noise(d)
make_some_noise(c)

# 在这个示例中,make_some_noise()函数接受一个Animal对象,并调用它的make_sound()方法。
# 在函数的实际调用中,我们传递了一个狗对象和一只猫对象,它们都重载了make_sound()方法,并实现了不同的行为。
# 这就体现了Python的多态特性。

Python提供包括管道、队列、共享内存 进程间通信的方式

管道(Pipe):

管道是一种基于文件句柄的进程间通信方式,在父进程和子进程之间创建一个输入输出的管道,可以通过这个管道进行双向通信。

import multiprocessing

def worker(conn):
    conn.send([42, None, 'hello'])
    conn.close()

if __name__ == '__main__':
    # 创建管道
    parent_conn, child_conn = multiprocessing.Pipe()
    # 创建子进程并启动
    p = multiprocessing.Process(target=worker, args=(child_conn,))
    p.start()
    # 父进程从管道中接收数据
    print(parent_conn.recv()) # prints "[42, None, 'hello']"
    p.join()
    
# 在上述代码中,我们通过multiprocessing.Pipe()方法创建了一个双向管道,这个管道将由父子进程共享。
# 在子进程中,我们向管道中写入了一个列表,之后关闭管道。
# 在父进程中,我们通过recv()方法从管道中接收数据。

队列(Queue):

队列是Python中线程和进程安全的通信方式,它提供了多种队列类型,包括FIFO队列、优先级队列等。 在多个进程间共享的队列中,进入数据会被多个进程消费,多个进程添加数据都不会冲突。

import multiprocessing

def worker(q):
    for i in range(10):
        q.put(i)
    q.put(None)

if __name__ == '__main__':
    # 创建队列
    q = multiprocessing.Queue()
    # 创建子进程并启动
    p = multiprocessing.Process(target=worker, args=(q,))
    p.start()
    # 父进程从队列中取数据
    while True:
        data = q.get()
        if data is None:
            break
        print(data)
    p.join()
    
# 在上述代码中,我们通过multiprocessing.Queue()方法创建了一个队列,这个队列将由父子进程共享。
# 在子进程中,我们将一定数量的数据放入队列中,并在最后放入None对象用于表示队列的结束。
# 在父进程中,我们会从队列中持续取出数据,并进行处理,当取出数据为None时,结束循环。

共享内存(Value和Array):

共享内存是一种不同于其他通信方式的高效方式,可以在多个进程中共享内存,不需要在进程间传递大量数据。 Python中提供了Value和Array等两个用于共享内存的数据结构。

import multiprocessing

def worker(num, arr):
    num.value += 1
    for i in range(len(arr)):
        arr[i] = arr[i] * 2


if __name__ == '__main__':
    # 创建共享内存数据
    num = multiprocessing.Value('i', 0)
    arr = multiprocessing.Array('i', range(10))
    # 创建子进程并启动
    p = multiprocessing.Process(target=worker, args=(num, arr))
    p.start()
    # 等待子进程结束
    p.join()
    # 打印共享内存数据
    print(num.value)
    print(arr[:])
    
# 在上述代码中,我们通过multiprocessing.Value()方法创建了一个内存中的值和一个multiprocessing.Array()方法创建了一个数组,
# 这两个数据结构将由父子进程共享。在子进程中,我们修改了num的值并对数组元素进行了一定的运算。
# 在父进程中,我们等待子进程结束并打印出共享内存数据。

什么是僵尸进程孤儿进程?怎么避免僵尸进程?

僵尸进程是指子进程结束后,其在内核中的进程描述符依然存在,但其父进程没有及时处理孩子退出的消息。

孤儿进程是指父进程异常退出,而子进程没有及时退出或被其他进程所接管的进程。

避免僵尸进程的方法:

父进程使用wait或waitpid函数等待子进程结束,及时回收子进程资源。

在创建子进程后,通过设置信号的处理函数来回收子进程资源。当子进程结束时,会向其父进程发送SIGCHLD信号,父进程可以通过信号处理函数处理该信号,并调用wait或waitpid函数等待子进程结束,及时回收子进程资源。

使用守护进程的方式避免僵尸进程的产生。守护进程是一种长期运行的后台进程,可以通过调用setsid函数、关闭文件描述符等操作创建,它不依赖于终端,不与任何控制终端产生关联,也不会受到用户登录或退出的影响。

需要注意的是,在使用wait或waitpid等函数等待子进程结束时,要防止使用错误的选项或参数,导致进程阻塞或无法回收子进程资源。

简述浏览器通过WSGI请求动态资源的过程

浏览器通过WSGI请求动态资源的过程:

用户在浏览器中输入网址,浏览器向服务器发送HTTP请求。

服务器接收到请求后,调用相应的WSGI应用程序处理请求。

WSGI应用程序根据请求的路径、请求方法等信息,调用相应的Python函数处理请求。

Python函数根据请求的逻辑,生成响应数据。

响应数据返回给WSGI应用程序,通过WSGI服务器返回给浏览器。

浏览器接收响应数据,进行渲染展示。

需要注意的是

WSGI应用程序通常由Web框架负责实现,如Django、Flask等;

WSGI服务器实现有很多,如Gunicorn、uWSGI、mod_wsgi等,可以根据实际需要选择合适的服务器。

用浏览器访问www.baidu.com的过程如下:

用户在浏览器地址栏中输入www.baidu.com并按下回车键。

浏览器向DNS服务器发送一个请求,询问www.baidu.com的IP地址。

DNS服务器返回www.baidu.com的IP地址(通常是多个)

浏览器通过IP地址连接到百度的服务器,并发送HTTP请求。

百度服务器接收到HTTP请求后,根据请求的URL路径和参数,响应相应的HTML文档。

浏览器接收到HTML文档后,开始渲染文档,并向服务器请求HTML文档中的其他资源,如图片等。

服务器提供所需的其他资源,并响应浏览器的请求,浏览器渲染并显示整个页面。

需要注意的是,此过程可能涉及到缓存、HTTPS协议等细节问题。

HTTP中最常用的请求方式有两种:GET和POST

GETPOST是HTTP协议中最常用的两个请求方式,它们在发送数据接收数据的方式上有不同。

GET请求:获取数据,常用于读取数据和信息展示的场景。

POST请求:提交数据,常用于提交表单、上传文件等场景。

Post和Get请求的区别:

参数位置不同

GET请求方法是将请求信息包含在URL中,而不是在包含请求数据的HTTP消息主体中。例如:http://www.example.com/path/file.html?arg1=123&arg2=abc

POST请求方法是将请求数据在请求消息主体中发送。而不是在URL中。例如:http://www.example.com/path/file.html

对数据大小的限制不同

GET请求访问的数据大小有限制,一般在2KB之内,因为数据需要在URL中传递。

POST请求则没有大小限制,因为数据在消息主体中传递。

请求安全性不同

GET请求的数据是以明文形式出现在URL中,所以不安全,可能会暴露数据。

POST请求将数据封装在请求消息主体中,不会暴露在URL中,相对比较安全。

缓存机制不同

GET请求可以被浏览器缓存

POST请求不能被浏览器缓存。

总之,GET请求一般用于传输少量数据,不包含敏感信息;而POST请求用于传输数据量较大,包含敏感信息的情况。

CookieSessionJWT都是用于用户身份验证的技术,它们有着不同的工作原理和应用场景。

Cookie

Cookie是服务器发送到浏览器并保存在本地的一小段数据,以便在用户访问网站时存储和检索相关信息。它可以在不同的页面中传递信息,也可以在用户关闭浏览器后依然存在。

Cookie通常用于 维护用户的登录状态跟踪用户的行为, 可以通过设置Cookie的属性来实现过期时间、安全性、域和路径等。

应用场景:

    `Cookie`最常见的应用场景是用于`用户认证`和`用户行为跟踪`,例如`网站的登录状态`、`购物车中的商品`、`用户主题首选项`等。

Session

Session是在服务器端存储的一段数据,用于跟踪用户的会话状态。需要借助 cookie 实现

每个Session都有一个唯一的Session ID,该ID在每个用户与应用程序交互时都会传递,以便在服务器端进行数据的读取和修改。

Session可以在应用程序中存储各种类型的数据,如用户信息、购物车中的商品、访问权限等。

应用场景:
    `Session`通常用于`用户认证`和 `管理状态信息`,例如在 服务器端记录`用户登录状态`,`检查权限`和`访问控制模型`等。

JWT

JWT(JSON Web Token)是一种安全的身份验证方式,通过在网络应用之间传递信息来验证用户的身份。

它使用JSON格式来封装信息,并使用加密算法将信息签名,以确保信息的完整性和安全性。

JWT通常包含用户信息、有效期和签名等。JWT可以很容易地在不同的应用程序之间共享,因为它是轻量级的,不需要在服务器端进行存储和验证。

应用场景:

    `JWT`最常见的应用场景是在 `分布式系统中`进行身份验证和授权,如`单点登录系统`、`API鉴权`等。

总之,Cookie、Session和JWT都是用于用户认证和维护状态的技术,根据不同的应用场景和安全要求选择不同的技术实现可以提高系统的可靠性和安全性。

HTTP协议的状态码

HTTP协议的状态码分为五类,分别是1xx、2xx、3xx、4xx和5xx,下面是状态码的具体含义:

1xx(提供信息)

100:继续请求,客户端应该继续发出请求。

101:协议切换,服务器正在切换到一个不同的协议(例如HTTP/2)。

2xx(成功)

200:请求成功,服务器已经成功处理了请求。

201:创建成功,请求已经成功创建一个新的资源。

204:无内容,服务器成功处理了请求,但返回的响应报文不包含实体的主体部分。

206:部分内容,服务器成功处理了范围请求。

3xx(重定向)

301:永久重定向,在请求的资源被永久性转移时使用。

302:临时重定向,在请求的资源临时性转移时使用。

304:未修改,客户端发送了一个带条件的GET请求,服务器告诉客户端资源未被修改,可以使用缓存的响应。

307:临时重定向,与302状态码含义相似,但不允许将POST变成GET。

4xx(客户端错误)

400:请求无效,由于客户端发送的请求有误而无法被服务器处理。

401:未授权,需要客户端先进行身份验证后,才能访问请求的资源。

403:禁止访问,客户端没有访问资源的权限。

404:未找到,客户端请求的资源不存在。

405:方法不被允许,服务器禁止使用该方法访问资源。

`4xx`表示客户端请求有误或者请求无法被服务器响应。

5xx(服务器错误)

500:服务器错误,服务器遇到了一个未知的错误,无法完成该请求。

502:错误网关,服务器作为网关或代理时,从上游服务器接收到无效的响应。

503:服务不可用,服务器当前无法处理请求,一段时间后可能会恢复正常。

`5xx`表示服务器遇到错误或无法处理请求。

什么是tcp2MSL

2MSLTCP协议中的一个计时器,全称为”2 Maximum Segment Lifetime”,中文翻译为“两倍的最大报文段寿命”

它是为了确保TCP连接被彻底释放而设立的一个定时器。

2MSL发生在TCP连接的最后一步,即在四次挥手的最后一个阶段

即客户端发送最后一个ACK报文段后,会进入TIME-WAIT状态,并等待2MSL时间,以确保双方都没有数据包需要发送,从而安全地关闭连接。
在2MSL时间结束后,客户端才会彻底关闭连接,释放相关资源。
因此,2MSL时间是四次挥手的最后一个阶段,也是TCP连接关闭的最后一步。

2MSL的时间通常为2倍的MSL,也就是两个通信节点之间最长的存活时间,一般设置为30秒,MSL的值根据不同的操作系统和TCP协议的版本而定。

经过2MSL的时间,说明可能滞留在网络中的连接请求已经彻底消失,从而保证没有任何数据报文段存在于网络中。

  • 客户端TIME-WAIT状态必须等待2MSL的时间主要出于以下几个原因:

    1 TCP连接的两个端点都可能在 关闭连接之前 发送其余的数据包,2MSL的时间长度保证了这些数据包在网络中完全消失, 以充分清除整个连接,确保不会影响到其他连接。

    2 确保服务端可以收到客户端最后一个ACK报文段,因为服务端在发送FIN报文段后,一定要收到客户端对该报文段的确认包ACK, 才能够准确地确认TCP连接已经关闭,释放相关资源。

    3 防止已失效的连接请求重新出现并导致错误,在时间等待期间,发送重复的FIN报文段也无法导致重新打开连接。

    因此,2MSL时间的作用是确保TCP连接被完全关闭,从而保证网络上的稳定性,防止新旧连接之间产生冲突,确保数据传输的正确性和可靠性。

HTTP和HTTPS区别

HTTP (HyperText Transfer Protocol)是一种基于TCP/IP协议的传输协议,

用于传输Web上的数据,常用于从Web服务器传输超文本到本地浏览器。

HTTP是明文传输,安全性不高。

HTTPS (HTTP Secure)是基于HTTP的安全协议,通过使用SSL/TLS协议来加密HTTP的内容。

使用HTTPS可以提供加密、身份验证和完整性保护等安全特性,保护用户隐私和Web应用安全。

这是通过在传输层(TCP)和网络层(IP)之间添加TLS/SSL层实现的。

因此,HTTP与HTTPS的主要区别在于:

HTTPS是加密传输协议,HTTP是明文传输协议。

HTTPS会通过TLS/SSL协议验证服务器的身份,从而提高通信的安全性。

HTTPS可以防止中间人攻击和窃听、修改等安全隐患,从而保护用户的私密信息。

总之,HTTP与HTTPS之间的主要区别就是安全性。HTTPS用于加密Web通信而HTTP不是。 HTTPS更适用于传输帐号和密码、信用卡信息等敏感数据。

HTTP 通过以下几种方法来保证安全传输

HTTPS 协议:

    HTTPS 是 HTTP 的安全版本,可以通过使用 SSL 或 TLS 加密技术来确保网络通讯的安全性。
    例如,https://www.google.com 这个 URL 就使用了 HTTPS 协议来加密网络通讯,提供更加安全的搜索服务。

SSL/TLS 加密技术:

    通过 SSL 或 TLS 加密技术,可以对 HTTP 的数据进行加密和解密,以保障网络通讯的安全。
    例如,当用户在网上购物时,其输入的支付信息将通过 SSL/TLS 加密技术进行保护,以避免支付信息被黑客窃取或篡改。

密码学技术:

    HTTP 可以使用密码学技术对数据进行加密和解密,以确保数据传输的安全性。
    例如,当用户在进行在线银行转账时,其输入的密码将通过密码学技术进行加密,使其难以被黑客破解。

数字证书及数字签名:
    
    HTTP 可以使用数字证书和数字签名来验证数据的真实性和完整性。
    例如,访问一个网站时,浏览器会检查该网站的数字证书,以确保该网站是真实的,并且数据在传输过程中没有被篡改。

通过以上方法,HTTP 可以保证信息传输的安全性和可靠性,使用户可以安心地进行网络通讯和在线交易。

SSL、TLS、mTLS 通信安全协议

SSL(Secure Sockets Layer)TLS(Transport Layer Security)是常用的通信安全协议,用于网络通信的加密、认证和数据完整性保护。

SSL 是一种早期的协议,后来被 TLS 所取代,TLS 的版本从 1.0 至 1.3 不断更新完善。

传统的 SSL/TLS 是单向认证,只需要服务端提供证书,客户端验证服务器证书的有效性即可。


SSL/TLS 使用公钥和私钥进行加密通信,其中,公钥用于加密发送方的数据,而私钥用于解密接收方的数据。
协议还包括数字证书和证书信任链,用于验证通信双方的身份和建立可信连接。


SSL/TLS 通信协议广泛应用于 Web 等 Internet 应用中,可以使用 HTTP 协议(HTTPS)或其他应用层协议进行通信。

mTLS(Mutual TLS)是一种基于证书的双向认证机制,也称为双向 SSL

mTLS 在服务端和客户端之间建立双向信任关系,客户端需要提供自己的证书,服务端验证客户端证书的有效性。
这种机制可以提高通讯的安全性,进一步防止中间人攻击和伪装攻击等。


mTLS 将双向认证机制应用于 SSL/TLS 通信中,客户端与服务端之间需要提供证书进行身份认证。

mTLS 广泛应用于微服务、容器化应用、API 网关等场景,可以提高应用之间的安全通信能力。

HTTP协议头部中表示数据类型的字段是Content-Type

HTTP协议是HyperText Transfer Protocol的简写,是一种用于Web应用程序和Web服务器之间交互数据的协议。

HTTP使用一个客户端-服务端模型,通过在客户端发起请求并由服务器进行响应的方式,来实现数据传输和通信。

HTTP协议头部中表示数据类型的字段是Content-Type。它指定了HTTP消息体中的媒体类型。

常见的媒体类型有text/html、text/plain、application/json、image/jpeg等。

Content-Type的常见取值如下:

text/html——HTML格式

text/plain——纯文本格式

application/json——JSON数据格式

application/xml——XML数据格式

Content-Type的格式如下:

Content-Type: type/subtype; parameter=value

其中,type表示大类别,subtype表示小类别,parameter表示参数,value为参数值,多个参数以分号隔开。

例如,Content-Type: text/html; charset=UTF-8,表示该文本是采用UTF-8编码的HTML格式。

在响应头中,Content-Type是必须要包含的字段,而在请求头中,则可选传输。

HTTP请求方法

HTTP请求方法是指 客户端对于服务器的行为请求方式,HTTP定义了多种请求方法,常用的有以下几种:

GET:用于获取指定资源的信息。请求参数通过URL传递,不安全。

POST:用于向指定资源提交数据,请求参数在请求体中传递,适合传递大量数据。

PUT:用于将请求的数据存储到指定位置,不区分新建或者修改。安全性较低。

DELETE:用于请求服务器删除指定的资源,但是不保证被删除。

HEAD:和GET类似,但是服务器只返回请求头信息,不返回实际数据。
     可以用于检测资源是否可用,判断文件是否修改等。

OPTIONS:用于获取资源的通信选项,常被用于 预检请求 等。

CONNECT:用于开启服务器与客户端之间的隧道(通常用于SSL加密的TCP通信)。

TRACE:执行一个假的请求来追踪请求-响应链。

如果需要定义新的方法,需要遵循HTTP协议的规范并在请求头中定义该方法。

不同的请求方法有不同的特点和应用场景,开发者需要根据具体需求选择。

Socket套接字需要传入以下参数:

Address family(地址族):指定网络通信的协议族,常见的有IPv4和IPv6。

Socket type(套接字类型):指定通信协议的类型,常见的有 面向连接的流式套接字 和 面向消息的数据报式套接字。

Protocol(协议):指定网络协议类型,如TCP、UDP、RAW等。

这些参数通常在创建Socket时传入,例如在Python中创建TCP套接字可以用如下代码:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
这里传入了
Address family 为IPv4,
Socket type 为面向连接的流式套接字
Protocol 为TCP。

其中socket.AF_INET和socket.SOCK_STREAM都是Python内置的常量,分别对应IPv4的地址族和面向连接的流式套接字。

HTTP 常见请求头

Accept:指定客户端能够接收的内容类型。

Accept-Charset:指定客户端能够接收的字符集。

Accept-Encoding:指定客户端能够接收的内容压缩格式。

Accept-Language:指定客户端能够接收的自然语言。

Authorization:指定客户端的身份认证信息。

Cache-Control:指定请求和响应的缓存机制。

Connection:指定客户端和服务器之间连接的状态。

Content-Length:指定请求或响应的内容长度。

Content-Type:指定请求或响应的内容类型。

Cookie:指定客户端要发送的Cookie。

Host:指定请求的目标主机。

If-Modified-Since:指定客户端发送请求的内容的修改时间,用于缓存控制。

If-None-Match:指定客户端要检查的实体标签,用于缓存控制。

Pragma:指定客户端和服务器的缓存机制。

Referer:指定请求来源的URL。

User-Agent:指定客户端的浏览器类型和操作系统信息

URL(Uniform Resource Locator)的形式通常为:

[协议]://[域名或IP地址][:端口号]/[路径]?[查询参数]#[锚点]

例如:

https://www.example.com:8080/index.html?param1=value1&param2=value2#section1

协议:例如HTTP、HTTPS、FTP。

域名或IP地址:指定服务器的地址。

端口号:HTTP协议默认端口号为80,HTTPS协议默认端口号为443,如果需要访问其他端口,则需要在URL中指定。

路径:指定服务器上的文件路径。

查询参数:用于向服务器传递额外的数据或指令。

锚点(或称片段):用于指定网页中的特定位置,浏览器会自动跳转至该位置。

对Flask蓝图(Blueprint)的理解

Flask蓝图(Blueprint)是一种将应用程序拆分为可复用组件的机制。它是在Flask应用程序中组织视图和其他代码的最佳实践方法之一。

在Flask中,蓝图是一种独立于应用程序的可重复使用组件,可以用于组织一个应用程序的视图、模板、静态文件和其他代码。 蓝图和应用程序一样,可以有自己的路由和视图函数,并且还可以有自己的模板目录和静态文件目录。在蓝图中定义的路由和视图函数可以包含相对路径,并且可以与应用程序的路由和视图函数组合在一起工作。

使用蓝图可以将应用程序划分为多个独立的模块,使代码更加清晰易懂,并可以简化应用程序的测试和维护工作。
蓝图还可以在单个应用程序中组织不同的API,方便不同的开发人员使用不同的API。

总的来说,蓝图是Flask中将应用程序分解为可重用组件的最佳实践方法之一,可以大大提高应用程序的可维护性和可伸缩性。

# 1.创建一个蓝图对象

blue = Blueprint("blue",__name__)

# 2.在这个蓝图对象上进行操作,例如注册路由、指定静态文件夹、注册模板过滤器...

@blue.route('/')
def blue_index():
    return "Welcome to my blueprint"

# 3.在应用对象上注册这个蓝图对象

app.register_blueprint(blue,url_prefix="/blue")

FlaskDjango 路由映射的区别

Flask和Django都是Web框架,但它们的路由映射机制略有不同。

Flask中,路由映射是通过装饰器来实现的。

通常会使用视图函数对不同的URL进行处理,并将函数使用@app.route()装饰器绑定到对应的URL上。例如:
from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "Hello World!"

# 这个代码片段定义了一个名为index的视图函数,它将响应在根URL(”/“)上的请求。

# 在Flask中,路由映射可以使用其他的HTTP方法绑定到相同的URL上。例如:

@app.route("/user/<username>")
def show_user_profile(username):
    return "User {}".format(username)

@app.route("/user/<username>/edit")
def edit_user_profile(username):
    return "Editing User {}".format(username)

# 这个代码片段展示了在Flask中如何绑定带变量的URL(即动态路由)。

# 在这个例子中,路由映射了/user/john和/user/peter这样的URL,并从其中的变量进行提取。

Django中,路由映射是通过URLconf(URL配置)文件或包中的模块来实现的。

每个模块定义一个或多个视图函数,并在URLconf中将这些视图函数绑定到对应的URL。例如:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('user/<username>/', views.show_user_profile, name='show_user_profile'),
    path('user/<username>/edit', views.edit_user_profile, name='edit_user_profile'),
]

# 在这个例子中,URLconf文件定义了三个路由,分别对应根URL(”/“)和两个带变量的URL(即动态路由)。


总的来说,Flask和Django都提供了路由映射来实现请求和视图函数之间的关系,

Flask使用 装饰器 将视图函数与路由关联,

Django使用 URLconf 将视图函数与路由关联。

无论使用哪种方式,都可以实现适当的路由映射,并为Web应用程序提供有用的API。

wsgi, uwsgi, uWSGI 的关系

1 WSGIWeb Server Gateway Interface的缩写,定义了一种规范,为Python Web应用程序和Web服务器之间的协作提供了一个标准化接口。 它允许Web应用程序Web服务器进行通信,尤其是通过调用应用程序代码中的可调用对象(通常是函数或方法)来处理请求和生成响应。

WSGI本身只是一种协议规范,并不提供Web服务器或应用程序服务器的实现。

2 uWSGI是一种Web服务器,既可用作Web应用程序服务器,也可以与其他Web服务器(如NGINX或Apache)进行协作。 它实现了WSGI协议,与Python的WSGI应用程序接口很好地集成,并可以通过多个协议(如HTTP、FastCGI等)进行通信。 uWSGI还支持异步、多线程、多进程和协程模型等特性,用于提高Web应用程序的性能以及灵活性。

3 uwsgi是一种使用uWSGI实现WSGI协议的特定格式的协议,它是uWSGI服务器的独占协议

因此,可以这样总结它们之间的关系:

WSGI是定义了Python Web应用程序和Web服务器之间通信的协议规范;
uWSGI是一种实现了WSGI协议的Web服务器和应用程序服务器;
uwsgi则是与uWSGI服务器通信的一个特定格式的协议。

Django、Flask、Tornado, fastapi 的对比

Django:Django是一种完全的Web应用程序框架,它提供了许多默认配置和工具,可以快速开发复杂的Web应用程序。
        Django具有强大的ORM功能,拥有大量的插件和社区支持。
        但是,Django的复杂性和约束性较高,需要遵守其固定的开发方式。

Flask:Flask是一种轻量级的Web框架,它提供了最基本的Web开发工具,通过良好的扩展性,可以根据需要添加更多的功能。
        Flask最大的优势在于其简单而灵活的结构,可以根据自己的需要自由发挥。
        但是,相对而言,Flask的性能和规模较小。

Tornado:Tornado是一种异步Web框架,它的主要特点是处理Web请求数量大时的稳定性和可伸缩性。
        Tornado适用于管理大量WebSockets连接和处理高并发请求的场景。
        但是,Tornado的使用门槛较高,需要去理解异步编程方式,而且其面向的应用场景也较为特殊。

FastAPI:FastAPI是一种基于Python3.6+的Web框架,它使用了Python的Type Hints和AsyncIO实现快速API开发,具有强大的性能和可扩展性。
        FastAPI的开发方式类似于Flask,但是其基于ABNF(Abstract Syntax Notation One Form)模型实现,可以自动生成OpenAPI文档,大大提高了开发效率。

综上,这些框架各具特色,根据具体的应用需求,选择适合的框架应该是基于开发权衡的一个评估过程。

Django请求生命周期的大致过程如下:

1 客户端向Django的Web服务器发起请求。

2 Web服务器将请求传递给Django框架,Django框架根据路由规则查找到对应的视图函数。

3 Django框架将请求封装成HttpRequest对象,发送给对应的视图函数进行处理。

4 视图函数对HttpRequest对象进行处理,生成HttpResponse对象,将响应发送回Web服务器。

5 Web服务器将HttpResponse对象发送回客户端,并结束本次请求响应过程。

在这个过程中,Django框架实现了许多功能,如URL路由、请求上下文、请求中间件、模板引擎等,为开发人员提供了强大的功能和便捷的开发方式。

Python中三大框架各自的应用场景

django:主要是用来搞快速开发的,他的亮点就是快速开发,节约成本,如果要实现高并发的话,就要对django进行二次开发

比如把整个笨重的框架给拆掉自己写socket实现http的通信,底层用纯c,c++写提升效率,ORM框架给干掉,自己编写封装与数据库交互的框架
ORM虽然面向对象来操作数据库,但是它的效率很低,使用外键来联系表与表之间的查询; 

flask: 轻量级,主要是用来写接口的一个框架,实现前后端分离,提考开发效率,Flask本身相当于一个内核,其他几乎所有的功能都要用到扩展

(邮件扩展Flask-Mail,用户认证Flask-Login),都需要用第三方的扩展来实现。比如可以用Flask-extension加入ORM、文件上传、身份验证等。
Flask没有默认使用的数据库,你可以选择MySQL,也可以用NoSQL。

其WSGI工具箱用Werkzeug(路由模块),模板引擎则使用Jinja2,这两个也是Flask框架的核心。

Tornado: Tornado是一种Web服务器软件的开源版本。

Tornado和现在的主流Web服务器框架(包括大多数Python的框架)有着明显的区别:

它是非阻塞式服务器,而且速度相当快。
得利于其非阻塞的方式和对epoll的运用,Tornado每秒可以处理数以千计的连接因此Tornado是实时Web服务的一个理想框架

找出 1G 的文件中高频词

对于较大的文件,可以考虑将其分块读取分布式计算,以提高处理效率。以下是一些实现方式的简述:

  • 分块读取

方式一:按行读取

在上述示例代码中,我们逐行读取了整个文件,对于较小的文件这种方式可以处理,但对于大型文件来说,逐行读取相当于每次只处理一个文本行,处理效率较低。 一种更好的方式是将文件分成固定大小的块,逐块读取并处理。

示例代码:

import re
from collections import Counter

def get_high_freq_words(file_path, block_size=100000000, top_k=10):
    # 定义正则表达式,用于分割单词
    regex = re.compile(r'\b[a-zA-Z]+\b')

    # 定义计数器,用于统计每个单词的出现次数
    word_counter = Counter()

    # 以块为单位读取文件,并统计每个单词的出现次数
    with open(file_path, 'r', encoding='utf-8') as f:
        while True:
            block = f.read(block_size)
            if not block:
                break
            words = regex.findall(block)
            word_counter.update(words)

    # 获取出现频率最高的前 top-k 个单词
    high_freq_words = word_counter.most_common(top_k)

    return high_freq_words

# 测试代码
file_path = 'path/to/your/file.txt'
high_freq_words = get_high_freq_words(file_path, block_size=100000000, top_k=10)
print(high_freq_words)

方式二:按大小分块

根据文件大小均分成若干块,然后逐块读取并处理。该方法可以避免读取过长的行导致内存溢出的问题。

示例代码:

import re
from collections import Counter
import os

def get_file_size(file_path):
    return os.stat(file_path).st_size

def get_high_freq_words(file_path, block_num=10, top_k=10):
    # 计算每块大小
    file_size = get_file_size(file_path)
    block_size = file_size // block_num

    # 定义正则表达式,用于分割单词
    regex = re.compile(r'\b[a-zA-Z]+\b')

    # 定义计数器,用于统计每个单词的出现次数
    word_counter = Counter()

    # 按块读取文件,并统计每个单词的出现次数
    with open(file_path, 'r', encoding='utf-8') as f:
        for i in range(block_num):
            # 读取一块的内容
            block = f.read(block_size)

            # 处理最后一块时,可能文件不是整块大小,需要特殊处理
            if i == block_num - 1:
                block += f.read()

            words = regex.findall(block)
            word_counter.update(words)

    # 获取出现频率最高的前 top-k 个单词
    high_freq_words = word_counter.most_common(top_k)

    return high_freq_words

# 测试代码
file_path = 'path/to/your/file.txt'
high_freq_words = get_high_freq_words(file_path, block_num=10, top_k=10)
print(high_freq_words)
  • 分布式计算

当处理大型文件时,可能需要借助分布式计算框架,将数据分布到多台机器上进行处理,可以显著提高处理速度。

以下是一些常用的分布式计算框架:

Apache Hadoop
Apache Spark
Apache Flink
Dask
Ray

这些框架都提供了简单易用的API,可帮助用户实现分布式计算。虽然这些框架使用起来有些不同,但在处理大型文件时基本思路相同,即将文件分成多个块,将块分配给多台机器进行处理,最后将结果合并。 以下是一个用Apache Spark处理大型文件的示例代码:

from pyspark import SparkConf, SparkContext
import re
from collections import Counter

def get_high_freq_words(file_path, top_k=10):
    # 定义Spark配置和上下文
    conf = SparkConf().setAppName("HighFreqWords")
    sc = SparkContext(conf=conf)

    # 定义正则表达式,用于分割单词
    regex = re.compile(r'\b[a-zA-Z]+\b')

    # 读取文件,并划分成多个块
    file_data = sc.textFile(file_path)
    blocks = file_data.repartition(10)

    # 逐块处理,统计每个单词的出现次数
    word_counter = blocks.flatMap(lambda line: regex.findall(line)) \
                      .map(lambda word: (word, 1)) \
                      .reduceByKey(lambda a, b: a + b) \
                      .map(lambda item: item[::-1]) \
                      .sortByKey(ascending=False) \
                      .take(top_k)

    # 停止Spark上下文
    sc.stop()

    return word_counter

# 测试代码
file_path = 'path/to/your/file.txt'
high_freq_words = get_high_freq_words(file_path, top_k=10)
print(high_freq_words)
# 该代码使用Apache Spark,将文件分成10个块,逐块处理并统计每个单词的出现次数,最后获取出现频率最高的前10个单词。需要注意的是,使用分布式计算框架时需要在多台机器上部署和运行,需要一定的分布式计算经验。

怎么在海量数据中找出重复次数最多的一个

有多种算法可以在海量数据中找出重复次数最多的一个,以下是两种常用的方法:

MapReduce

    MapReduce是一个分布式计算框架,可以用于处理大规模数据集。在MapReduce中,可以使用 Map 和 Reduce 两个操作来处理数据。具体地,在找出重复次数最多的一个时,可以进行以下步骤:
    
    Map 阶段:将海量数据分成多个块,并将每个块中的每个元素都映射成一个键值对。键表示元素的值,值为1。
    Shuffle 阶段:将 Map 阶段得到的键值对按键进行分组。
    Reduce 阶段:对每个键的值进行累加,得到每个元素的出现次数。
    最后,遍历 Reduce 阶段得到的键值对,可以找出重复次数最多的一个元素及其出现次数。

布谷鸟哈希

    布谷鸟哈希是一种哈希表的实现方式,具有高效的查找和插入操作,并且可以动态调整哈希表的大小。具体地,在找出重复次数最多的一个时,可以进行以下步骤:
    
    将海量数据进行哈希,并将哈希值存储到布谷鸟哈希表中。
    对于哈希冲突的情况,可以使用链表等方法进行处理。
    遍历布谷鸟哈希表,找出出现次数最多的一个哈希值。
    可以采用以上两种方法中的任意一种,具体选择根据数据集的特点以及实现难度和效率来决定。

TCP and UDP

OSI 和 TCP/UDP 都是网络通信的协议范式,但它们的角色和定位不同。

OSI 模型是一种概念性的框架,用来描述网络通信的各个层次,从而使不同的协议能够在特定层次上相互协作。 而 TCP/UDP 则是 OSI 模型中两个很重要的协议,它们分别负责 OSI 模型的传输层

  • TCP (Transmission Control Protocol) 是 OSI 模型中第四层(传输层)的协议之一。

TCP 提供了可靠的数据传输和流量控制服务,在数据传输过程中保证数据的完整性、可靠性和有序性。

TCP 协议的特点是面向连接、有序、可靠。它适用于对网络延迟要求较高,对数据准确性要求较高的应用。

  • UDP (User Datagram Protocol) 是 OSI 模型中第四层(传输层)的协议之一。

UDP 与 TCP 不同之处在于它是一种无连接协议,通信双方在传输数据之前不需要建立连接。

UDP 在数据传输过程中不保证数据的完整性、可靠性和有序性。UDP 协议的特点是无连接、不可靠、简单、高效。 它适用于对网络延迟要求较低,对数据准确性要求不高的应用。

在实际网络通信中,TCP 和 UDP 都有各自的应用场景。

例如,在文件传输、电子邮件等应用场景中,TCP 协议的可靠性和保证数据完整性是非常重要的。
而在视频流传输、音频流传输、在线游戏等应用场景中,UDP 由于传输效率高,可以更好地满足实时性要求。
  • TCP 的三次握手四次挥手

TCP 的三次握手四次挥手 是 TCP 协议用于建立和关闭连接时的具体过程。 简单来说,三次握手是客户端和服务器建立连接,四次挥手是关闭连接。

三次握手是指:

客户端向服务器发送 SYN (同步请求)包。
服务器收到 SYN 包后,向客户端发送 SYN/ACK(同步应答)包。
客户端收到 SYN/ACK 包后,向服务器发送 ACK(确认)包。

这时,TCP 连接建立成功。

四次挥手是指:

客户端发送 FIN (结束)包。
服务器收到 FIN 包后,向客户端发送 ACK 包,表明已收到关闭请求。
服务器处理完客户端的数据后,发送 FIN 包给客户端,表示数据处理完成,可以关闭连接。
客户端收到 FIN 包后,向服务器发送 ACK 包,表明自己也准备关闭连接。 这时,TCP 连接关闭完成。

需要注意的是,TCP 连接的建立和关闭都是需要经过三次握手和四次挥手的过程,这样可以保证连接的可靠性和完整性。 同时,在实际应用中还需要考虑网络延迟、丢包等因素,以确保 TCP 连接的稳定性和高效性。

常见的使用UDP协议的应用

UDP(User Datagram Protocol)是一种面向无连接的传输层协议,它主要用于传输不需要可靠性保证的短消息或实时数据,如音频和视频。

以下是常见使用UDP协议的应用:

DNS (Domain Name System):

    DNS 是一种域名系统,其主要作用是通过域名将网站的 IP 地址解析出来,使用户能够通过友好的域名访问网站。
    DNS 请求和响应都使用UDP协议,因为其具有低延迟和快速传输的特点,对于DNS解析来说特别适合。

实时游戏:

    UDP 可以提供低延迟和高速的网络传输,使得实时游戏的玩家可以在网络游戏中及时地接收和传输数据。
    例如,一些大型多人在线游戏(MMO)就使用UDP协议来快速同步玩家的数据和位置。

VoIP(Voice over Internet Protocol):

    VoIP 技术使用 UDP 协议来传输语音数据包,使得用户可以通过网络进行语音和视频通信。
    由于 UDP 协议不需要保证数据包的可靠性,因此在使用 VoIP 时可能会出现少量的数据包丢失或抖动,但是这种情况不会对用户造成太大影响。

IoT(Internet of Things):

    IoT 设备通常需要进行快速的消息传输,因此 UDP 协议非常适合用于传输 IoT 设备的状态和数据包。
    使用UDP协议可以加快速度,降低延迟,提高设备响应速度。

实时视频流:UDP协议广泛应用于视频流的传输,例如实时直播和视频会议等。

    UDP 的快速传输和低延迟使得实时视频流的传输更加顺畅和实时。
    但UDP协议也存在数据包丢失和抖动的问题,需要应用程序进行处理和优化。

SQL 关系型数据库

Mysql: 免费的开源关系型数据库,被广泛应用于Web应用开发,特别是配合PHP使用。MySQL在性能、稳定性和易用性上都得到了广大用户的认可。 适合应用于各类型中小型网站以及小型/中型数据存储。 根据需求选择其商业版本还是社区版本。

IBM DB2: 商业数据库,支持关系型和非关系型两种模式,适用于存储大型数据,并且能提供高效的数据处理。适合用于企业级、大型项目中,尤其是在银行、保险、零售等数据密集型行业。

DuckDB: 是一个分析型的嵌入式SQL数据库,优势在于其能对大量数据进行高效的分析处理。适合用于数据分析、数据科学等领域。

MariaDB: 是MySQL的一个分支,完全兼容MySQL,包括API和命令行,使之能够轻松成为MySQL的替代品。适合用于Web开发,小型和中型数据存储。

Oracle: Oracle 数据库是一个强大的关系数据库管理系统,具有较高的安全性、灵活的数据处理能力,目前在全球范围内有着广泛的应用。其数据库主要应用于大中型的企业级应用。

PostgreSQL: 开源的对象关系型数据库管理系统,支持SQL和JSON等多种数据类型。并且在安全性、并发性、性能优化上有着很好的处理。适合用于Web开发和中大型项目中。

SQL Server: Microsoft SQL Server是由微软公司所推出的关系型数据库管理系统。在功能、性能、易用性等方面也十分强大。适合用于需要处理大量数据的企业级应用。

SQLite: 简洁轻便,是嵌入式SQL数据库引擎。无需单独的服务器进程、允许将数据库存为单一文件等特性,使得 SQLite 适合于中小型应用,移动应用。

ClickHouse: 是一个面向在线分析(OLAP)的列式数据库管理系统。处理查询能力极强,适合大数据分析。

CockroachDB: 是一个分布式SQL数据库,设计用于大规模云环境,它的分布式架构使得数据复制和容灾性非常容易。适应于对数据的高可用性和分布式存储要求很高的大规模云环境。

Azure SQL server: Azure SQL 数据库是一种完全托管的云数据库服务,由微软的数据中心管理。它提供了方便的扩展性、高可用性以及安全性。特别适合于需要在云环境中运行的应用。

NO SQL 数据库

Redis: 开源的,基于内存的键值对数据库,支持不同类型的数据结构如字符串,哈希,列表,集合。通常使用做缓存和消息队列。

MongoDB: 是一种基于分布式文件存储的开源文档数据库。MongoDB具有较高的写入负载容量,并且支持丰富的查询和临时处理。

BigTable: Google的大规模、结构化的非关系型数据库。主要被用于谷歌搜索, Gmail, Google Map等众多服务。

Cassandra: Apache的分布式NoSQL数据库系统,主要设计目标是处理大量的数据,分布在很多服务器中,具有高度的准备可扩展性和高可用性。

Couchbase: 是一个为互联网或者移动互联网应用设计的NoSQL数据库。提供简易的可扩展性,一致的高性能,360度的高可用性,以及一种构架。

DocumentDB: AWS出品的MongoDB兼容服务,完全托管,高性能,高度可扩展的NoSQL数据库,用于处理各种规模的应用程序。

InfluxDB: 是一个开源的时间序列数据库,用于处理和分析来自IoT设备,实时应用的时间序列数据。

KeySpace: 是一种高度分布式、备份和读取优化的数据存储系统。它是为云原生应用程序设计的,可以并行执行ACID事务。

TimeStream: Amazon的产品,全托管的时序数据库,用于处理IoT和操作应用的时间序列数据,具有高速查询和低成本。

上述数据库具体应用场景:

Redis: 缓存系统,消息传递系统,实时分析,游戏领域
MongoDB: CMS系统,商业智能,实时数据整合,移动应用
BigTable: 大数据分析,物联网,时间序列数据
Cassandra: 网络音乐服务,社交产品,实时监控系统
Couchbase: 个性化应用,互联网广告,移动应用和游戏
DocumentDB: 移动应用,网站内容管理,实时大数据应用
InfluxDB: 物联网,实时分析,监控和警告
KeySpace: 分布式ACID事务,云原生应用程序
TimeStream: IoT设备监测,运维应用,实时仪表板。

MVCC 机制

悲观并发控制:
    MySQL中悲观并发控制通常通过使用行级锁或表级锁来实现。当某个事务需要对一个数据行进行修改时,它会尝试加锁该行,防止其他事务对该行进行修改。
    在MySQL中,可以使用FOR UPDATE或FOR SHARE语句来进行行级锁操作,使用LOCK TABLES语句来进行表级锁操作。

    场景应用实例:
        在银行系统中,当一个用户进行转账操作时,需要加锁该用户的账户,防止其他用户同时对该账户进行操作。


乐观并发控制:
    MySQL中乐观并发控制通常通过使用版本号或时间戳来实现。当某个事务需要进行写操作时,它会先读取数据行的版本号或时间戳,
    然后在写入数据时将版本号或时间戳加1,如果读取和写入数据时版本号或时间戳不一致,说明该数据行已被其他事务修改,需要进行回滚操作。

    场景应用实例:
        在电商网站中,某个用户操作某个商品的时候,可以使用版本号或时间戳进行乐观并发控制,确保商品信息的一致性。


多版本并发控制:
    MySQL中多版本并发控制通常通过使用多版本并发控制(MVCC)来实现。MVCC会为每个事务建立一个快照,
    存储该事务在每个时间点看到的数据,当事务需要提交时,会检查提交的数据是否与快照一致,如果一致,则提交成功,否则需要进行回滚操作。

    场景应用实例:
        在大型网站中,多个用户频繁访问、修改和删除数据,需要使用多版本并发控制来确保数据的一致性。

MVCC(多版本并发控制 是一种高效并发控制协议,通过为每个事务的读操作提供一个独立的版本号,实现了多个事务之间的隔离性,避免了传统的锁机制可能带来的死锁和阻塞。

  • 导致RCRR这两种隔离级别的快照读 不同原因:

      RC级别下,每一次的快照读都会重新生产 Read View 
        
      RR级别下,则会沿用 第一次快照读生成的 Read View。
    
  • RR 模式update delete 为什么触发当前读

      在 MySQL 中,RR (Repeatable Read)隔离级别下,update delete 操作会触发当前读,
    
          这是因为 RR 隔离级别下,事务在进行查询时会对读取的数据加上 共享锁,以保证查询结果的一致性。
          但是,当执行 update delete 操作时,需要对数据加上  排他锁(exclusive lock),以防止其他事务同时对同一行数据进行修改或删除操作,而排他锁不能与共享锁同时存在,因此当前读会被触发。
        
      具体来说,当一个事务在 RR 隔离级别下执行 SELECT 操作时,MySQL 会为结果集中的所有行加上共享锁。
      如果这个事务在执行 SELECT 操作时发现需要修改或删除某个行,那么它会尝试将该行的共享锁升级为排他锁,这一过程就会触发当前读。
        
      需要注意的是,在 RR 隔离级别下进行 update delete 操作触发当前读的问题并不意味着 RR 隔离级别是不合适的,事实上,RR 隔离级别在保障数据的一致性方面仍然是非常有价值的。
      只要在使用 RR 隔离级别时合理使用锁机制,就可以避免出现当前读引起的问题。
    
  • MVCC 机制

MVCC 基本思想是:

每行数据保存多个版本,而不仅仅是当前版本。

当某一事务需要对某行记录进行操作时,系统会取出该行的最新版本,然后进行操作,同时生成一个新版本,更新该行记录。

每个版本都有一个唯一的时间戳,可以通过时间戳判断该数据是否可见。 如果当前事务的时间戳小于该行数据版本的时间戳,那么该行数据对于当前事务来说是不可见的。

在 MySQL 中,RR (Repeatable Read)隔离级别下,update delete 操作会触发当前读,这是因为 RR 隔离级别下,事务在进行查询时会对读取的数据加上共享锁,以保证查询结果的一致性。但是,当执行 update delete 操作时,需要对数据加上排他锁(exclusive lock),以防止其他事务同时对同一行数据进行修改或删除操作,而排他锁不能与共享锁同时存在,因此当前读会被触发。

具体来说,当一个事务在 RR 隔离级别下执行 SELECT 操作时,MySQL 会为结果集中的所有行加上共享锁。如果这个事务在执行 SELECT 操作时发现需要修改或删除某个行,那么它会尝试将该行的共享锁升级为排他锁,这一过程就会触发当前读。

需要注意的是,在 RR 隔离级别下进行 update delete 操作触发当前读的问题并不意味着 RR 隔离级别是不合适的,事实上,RR 隔离级别在保障数据的一致性方面仍然是非常有价值的。只要在使用 RR 隔离级别时合理使用锁机制,就可以避免出现当前读引起的问题。

对于已经提交的事务,其生成的版本可以一直保存;对于未提交的事务,则可以在事务回滚时撤销其生成的版本。

MVCC 实现增删查改操作

    Insert:

        当执行 Insert 操作时,会为该新记录生成一个版本号,同时该版本也成为该行数据的最新版本。如果数据需要更新后再次插入,会生成另一个版本号。
    
    Delete:

        当执行 Delete 操作时,InnoDB 不会立即删除该行记录,而是将其当前版本的删除标志位设置为 true,并插入一条新版本,版本号比原始版本号大 1,同时修改该行记录的删除标志位为 false。
        这么做是为了避免在并发环境中的读操作读到错误数据。
    
    Update:
        
        InnoDB 也不会立即更新该行记录,而是将其当前版本的删除标志位设置为 true,并插入一条新版本,版本号比原始版本号大 1,然后将该新版本的记录值修改为更新后的值。
        这么做是为了避免同样的原因,避免在并发环境中的读操作读到错误数据。
    
    Select:
        
        当执行 Select 操作时,InnoDB 会先计算出当前事务的版本信息,然后读取该版本的记录值,如果发现该行的版本号比当前事务的版本号小,则视为该行数据已经过期,无法读取,返回一个 null。

以上就是 MVCC 的机制以及增删查改操作的实现方式。

MVCC 可以在高并发环境下提高数据库的并发性能,避免了传统锁机制可能带来的死锁和阻塞。 但同时,MVCC 也带来了额外的存储和计算开销,需要根据具体业务需求对其进行调整。

I/O多路复用(IO multiplexing)

I/O多路复用(IO multiplexing 是一种用于管理多个 socket 连接的机制, 它允许单线程在没有阻塞的情况下处理多个连接,提高了程序的并发性和效率。

在 Linux 中,使用 select 函数来实现 I/O 多路复用。

在使用 select 函数时,需要将需要监控的 文件描述符(即套接字)放入一个 fd_set 集合中,然后传入 select 函数中进行监控。
fd_set 集合中的大小是固定的,默认是 1024,这意味着一次最多可以监控 1024 个文件描述符。

那么为什么 select 描述符的限制是 1024

这是因为在早期的系统中,fd_set 采用的是一个 32 位的数组来表示文件描述符是否可读。
32 位表示的最大十进制数是 2^32-1,即 4294967295,也就是说一个 fd_set 最多可以表示 4294967296 个描述符,
这远远超过了实际需要的数量。为了节约空间,选择了一个比较小的数值 1024。

此外,更大的 fd_set 数组会占用更多的资源,而且 select 函数的效率也会受到影响,因此采用 1024 这一相对较小的数值是一种平衡的做法。

随着计算机的发展,现在的系统一般采用 pollepoll 等新的机制来实现 I/O 多路复用,可以支持更多的文件描述符

TCP滑动窗口

TCP滑动窗口 是一种流量控制的机制,用于控制发送方发送数据的速率,避免网络拥塞和丢包。 它采用了一个滑动窗口的概念,同时限制了发送方和接收方的数据量。

具体原理如下:

发送方和接收方在建立连接时,会协商一个窗口大小(即滑动窗口的大小)。

发送方将待发送的数据分成若干个数据段,每个数据段的大小不能超过窗口大小,发送到接收方。

接收方收到数据后,会将其放置在接收缓冲区中,并发送一个确认消息(ACK)给发送方,通知它已经成功接收了数据。

发送方收到接收方的ACK后,会将窗口向右滑动一个位置,即将窗口中已经确认的数据删除,并发送新的数据。

如果发送方没有接收到接收方的ACK,说明接收方并没有成功接收到数据,发送方会不断重发该数据,直到接收到ACK为止。

通过这种方式,利用滑动窗口的机制,TCP可以自适应地控制数据的传输速率,避免网络拥塞和丢包,从而保证数据传输的可靠性和稳定性。

metaclass 在 django 中的应用

除了django中的ORM系统外,metaclass在Django REST framework(DRF)中也有广泛的应用。

在DRF中,metaclass主要用于实现序列化器(Serializer)类的自动化行为。

    序列化器是DRF中很重要的一个概念,用来帮助我们将模型转化为JSON或其他格式的数据,或将JSON数据转化为模型实例。

    通过定义metaclass,DRF的Serializer类可以自动生成序列化和反序列化的功能,例如自动根据模型的字段生成序列化器的字段、自动根据模型的关系生成关联字段等。

此外,在DRF中,metaclass还用于实现视图集(Viewset)类的自动化行为。

    视图集是DRF中用来处理HTTP请求的概念,与Django的视图函数类似。通过metaclass自动生成视图中的各个方法,包括get、post、put等HTTP方法的处理方式,
    从而使得实现RESTful API更加简洁方便。

总的来说,metaclass在Django和DRF中都是非常重要的概念,用于实现自动化行为,减少重复的编码工作,提升代码复用程度和开发效率。

list、tuple、dict底层,set去重原理

list底层是基于 数组 实现的,可以通过索引访问元素,可变类型,支持操作包括增删改查等。

tuple底层同样是基于 数组 实现的,不同于list的是,tuple元素不可变,因此在当作不可变的常量表达式使用时,会比list更加高效。

dict底层是基于 哈希表 实现的,支持按照键值对进行增删改查等操作,其中哈希表的实现保证了查找、插入等操作的平均时间复杂度为O(1)。

set底层同样是基于 哈希表 实现的,其实现方式和dict类似,但是set中只保存了键值而没有对应的值,因此使用set可以很方便地实现去重操作。

    当使用set进行去重时,具体的原理是将需要去重的元素作为set的key来进行存储,重复的元素会被自动去除,而不需要像使用list一样进行手动去重操作。

需要注意的是,在使用哈希表实现的容器中,哈希函数的设计和实现会影响到容器的性能和使用效果。 哈希函数需要将不同的键值映射到尽量不同的散列桶中,避免哈希冲突。 如果哈希函数设计得不好,会导致哈希冲突过多,从而降低容器的效率。

gRPC、Thrift、SOAP、GraphQL和REST传输方式

gRPC    可以使用Protocol Buffer格式进行数据传输,底层协议可以使用基于socket的传输机制或HTTP/2协议。

Thrift  也使用二进制协议进行传输,并支持多种传输协议,如TCP、HTTP和WebSocket等

SOAP    是基于XML格式进行数据传输,底层协议一般使用HTTP或HTTPS。

GraphQL 使用自定义的查询语言进行数据传输,底层协议可以使用HTTP或HTTPS。

REST    是一种架构设计原则,使用HTTP协议进行数据传输,可以使用JSON或XML等格式进行数据编码。


对于效率的比较,这些技术的效率取决于很多因素,包括网络延迟、带宽、服务器硬件等等。
通常来说,gRPC和Thrift 都是基于二进制协议进行高效传输的 使用binary格式,可以比基于文本格式的SOAP、GraphQL和REST更高效地传输数据。
但是在实际应用中,需要综合考虑多个因素来选择适合的技术。

__dict__dir 的区别

__dict__ 仅仅是那个实例的实例属性的集合,并不包含该实例的所有有效属性

dir() 获取一个对象所有有效属性

class MyClass:
    x = 1

    def my_method(self):
        return "hello"

obj = MyClass()
print(obj.__dict__)         
# 输出 {}
print(MyClass.__dict__)     # 输出类的属性和方法,并排除了从 object 继承的属性和方法
# {'__module__': '__main__', 'x': 1, 'my_method': <function MyClass.my_method at 0x104d13670>, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None}
obj.y = 2                   # 动态添加一个属性
print(obj.__dict__)         # 输出 {'y': 2}
# {'y': 2}

print(dir(obj))        # 输出对象的属性和方法
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'my_method', 'x', 'y']
print(dir(MyClass))    # 输出类的属性和方法,并排除了从 object 继承的属性和方法
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'my_method', 'x']


闭包, 使用闭包要注意什么

闭包指的是在一个函数内部定义另外一个函数,并且在内部函数中引用了外部函数的变量或参数

这样的函数定义可以使用外部函数的变量或参数,即使外部函数已经执行结束了。

闭包常常用来在一个函数内部创建可供外部访问的变量或类似对象的功能。

闭包的作用:

可以用于封装模块,可以避免污染全局变量空间;
可以保护函数内部的变量不被修改;
可以实现装饰器

以下是一个简单的闭包示例:

def outer(x):
    def inner(y):
        return x + y
    return inner

f = outer(10)  # f是一个闭包,x的值为10

print(f(20))  # 输出30,相当于调用inner(20)

使用闭包时需要注意:

由于闭包函数可以访问外部函数的变量,所以如果这些变量是可变的,可能会发生意外的改变。例如:

def outer():
    x = [1]
    def inner():
        x.append(2)
        return x
    return inner

my_closure = outer()
print(my_closure())    # 输出 [1, 2]
print(my_closure())    # 输出 [1, 2, 2],因为 x 是一个可变列表,在第二次调用 inner 时,x 中已经包含了之前添加的 2
# 为了避免这种情况,可以使用不可变的类型,例如元组

观察者模式

观察者模式是一种设计模式,它用于创建事件机制

一个被观察的对象发生改变时,它的所有依赖项都会收到通知并自动更新。
class Observable:
    """
        Observable 类是一个被观察的对象。它维护一个观察者列表,它可以注册和删除观察者,当它的状态发生变化时,它会通知它的所有观察者。
    """
    def __init__(self):
        self._observers = []

    def register_observer(self, observer):
        self._observers.append(observer)

    def remove_observer(self, observer):
        self._observers.remove(observer)

    def notify_observers(self, *args, **kwargs):
        for observer in self._observers:
            observer.update(*args, **kwargs)

    def add_observer(self, observer):
        self._observers.append(observer)

    def add_item(self, item):
        self.notify_observers(item=item)


class Observer:
    """
        Observer 类是观察者的基类, update() 方法是用于接收状态变化的通知的方法。
    """
    def update(self, *args, **kwargs):
        pass



class SMSSender(Observer):
    def update(self, item):
        print(f'Sending SMS notification about {item}.')


class EmailSender(Observer):
    def update(self, item):
        print(f'Sending Email notification about {item}.')


observable = Observable()

sms_sender = SMSSender()
email_sender = EmailSender()

observable.register_observer(sms_sender)
observable.register_observer(email_sender)

observable.add_item('apple')

observable.add_item('banana')

rediszset 使用场景

Rediszset 是一种有序集合,其中每个元素都有一个得分(score)与之对应,通过分数可以对集合中的元素进行排序,同时支持范围查询。

因此,zset 适用于以下场景:

排行榜:    

    将每个用户的分数作为得分,可以快速地通过分数对用户进行排名。

        假如我们需要实现一个简单的在线游戏排行榜,其中每个用户有一个分数,根据分数高低进行排名。可以使用 zset 存储用户分数,成员为用户ID,分数为分数值,例如:

        ZADD player_score 98 "user1"
        ZADD player_score 87 "user2"
        ZADD player_score 90 "user3"
        可以使用 ZRANGE 命令从高到低获取排名前三的用户:
        
        ZRANGE player_score 0 2 WITHSCORES
        输出结果为:
        
        1) "user1"
           2) "98"
        2) "user3"
           3) "90"
        4) "user2"
           5) "87"

针对有限集合的索引:

    例如,可以用 zset 存储文章的创建时间,并将文章ID作为每个元素的成员,在查询时,可以通过时间区间查询(例如,查找某个时间段内所有的文章ID)。
    
        假如我们需要存储一些文章,需要按照发布时间进行排序,并且支持根据时间范围检索。可以使用 zset 存储文章的发布时间戳,成员为文章ID,例如:

        ZADD article_published_time 1539832580 "article1"
        ZADD article_published_time 1539864580 "article2"
        ZADD article_published_time 1539918780 "article3"

        可以使用 ZRANGEBYSCORE 命令获取指定时间范围内的文章ID:
        
        ZRANGEBYSCORE article_published_time 1539864580 1539918780
    
        输出结果为:
        
        1) "article2"
        2) "article3"

实时数据排名:

    使用 zset 存储每个页面的浏览数,并在用户请求页面时进行实时排名,可以提高对热门页面的访问速度和系统的响应速度。

        假如我们需要统计一些页面的访问次数,并根据访问次数进行排名,来提高对热门页面的访问速度和系统的响应速度。

        可以使用 zset 存储页面的访问次数,成员为页面ID,分数为访问次数值,例如:
    
        ZADD page_views 100 "page1"
        ZADD page_views 67 "page2"
        ZADD page_views 50 "page3"

        可以使用 ZREVRANGE 命令从多到少获取排名前三的页面:
        
        ZREVRANGE page_views 0 2 WITHSCORES

        输出结果为:
        
        1) "page1"
           2) "100"
        2) "page2"
           3) "67"
        3) "page3"
           4) "50"

消息队列:

    将每个消息的时间戳作为元素的 score,可以快速地根据时间戳获取消息,实现简单的延迟队列。

        假如我们需要实现一个简单的延迟队列,即在指定的延迟时间内将消息发送给消费者。

        可以使用 zset 存储消息的延迟时间戳,成员为消息ID,例如:

        ZADD delay_queue 1543262392 "msg1"
        ZADD delay_queue 1543262592 "msg2"
        ZADD delay_queue 1543262792 "msg3"

        可以使用 ZRANGEBYSCORE 命令获取指定延迟时间范围内的消息ID,并将消息发送给消费者:
        
        ZRANGEBYSCORE delay_queue -inf 1543262592 | xargs -L 1 echo "send message: "

        上述命令将发送所有延迟时间戳 小于等于 1543262592 的消息给消费者。

总之,zset 适用于需要按照一定规则对元素进行排序、统计、筛选等场景,具有较好的性能表现

yieldsend 都是与生成器相关的关键字,但它们的用途不同

在生成器函数中

yield 关键字可以暂停函数并返回一个结果,当再次执行函数时,会从暂停的位置继续执行。

send 方法可以在暂停的位置重新启动生成器,并在向生成器发送一个值作为生成器的 yield 的结果

def my_generator():
    while True:
        result = yield
        print('Generator received:', result)

g = my_generator()
next(g)  # 先执行一次,使生成器准备好接收send的值
g.send('Hello')

# 在这个例子中,my_generator() 是一个无限循环的生成器函数,它会一直等待外部向它发送数据。
# 
# 1 在调用 send() 方法之前,必须先调用 next() 函数来启动生成器,并让它暂停在 yield 处准备接收数据。
# 2 send() 方法会向生成器发送一个值,这个值会作为 yield 的结果。在 my_generator() 函数中通过 print() 函数输出了发送的值。

在 Python 中,yield 关键字常常用于定义生成器函数,使函数返回一个迭代器对象,通过迭代器可以逐个生成一系列值。

使用 yield 定义生成器函数有如下特点:

调用生成器函数时,不会立即执行函数体的代码,而是返回一个迭代器对象。

当迭代器的 __next__() 方法被调用时,函数体代码开始执行,直到函数遇到 yield 关键字暂停执行,并将 yield 后面的值返回给迭代器。

被生成器暂停的状态会被保存下来,下一次调用 __next__() 方法时,函数从上一次暂停的位置继续执行。

当函数体的代码执行完毕或遇到 return 语句时,迭代器会自动抛出 StopIteration 异常,迭代结束。

下面是一个简单的使用 yield 定义生成器函数的例子:

def my_generator():
    range_ = yield
    for i in range(range_):
        yield i


g = my_generator()
# next(g)  # 先执行一次,使生成器准备好接收send的值
g.send(None)
g.send(3)

for i in g:
    print(i)
# 1
# 2

保证服务健壮性的几个关键点:

错误处理:应该对各种可能出现的错误进行清晰的处理,如网络连接问题、数据库连接失败等,使用异常处理机制捕获并记录错误日志。

容灾备份:进行定期备份,并制定恢复方案,以应对不可控因素,如系统崩溃、黑客攻击等。

负载均衡:当服务请求量很大时,可通过负载均衡器将服务请求均衡地分配到多台服务器上,确保系统的高可用性和稳定性。

增量部署:将新的功能或更新内容进行分批发布,逐渐测试和验证,以降低系统维护的风险。

系统监控:将系统的运行情况实时监控,并及时发现并处理故障问题,防止故障扩大化。

性能优化:系统在长时间运行过程中可能出现各种问题,如内存泄漏、性能降低等,因此要进行性能优化,保证系统高效稳定运行,如定时清理不必要的缓存等。

通过以上关键点可以降低系统故障风险,提高系统的健壮性。另外,还需要加强人员培训和管理,提高团队技术水平和协作能力,为系统运行提供保障。

多台服务器共享session问题

1.通过数据库mysql共享session
    
     a.采用一台专门的mysql服务器来存储所有的session信息。
    
      用户访问随机的web服务器时,会去这个专门的数据库服务器check一下session的情况,以达到session同步的目的。 
    
      缺点就是:依懒性太强,mysql服务器无法工作,影响整个系统;
    
    b.将存放session的数据表与业务的数据表放在同一个库。如果mysql做了主从,需要每一个库都需要存在这个表,并且需要数据实时同步。
    
      缺点:用数据库来同步session,会加大数据库的负担,数据库本来就是容易产生瓶颈的地方,如果把session还放到数据库里面,无疑是雪上加霜。上面的二种方法,第一点方法较好,把放session的表独立开来,减轻了真正数据库的负担 。但是session一般的查询频率较高,放在数据库中查询性能也不是很好,不推荐使用这种方式。
    
2.通过cookie共享session

    把用户访问页面产生的session放到cookie里面,就是以cookie为中转站。
    
    当访问服务器A时,登录成功之后将产生的session信息存放在cookie中;当访问请求分配到服务器B时,服务器B先判断服务器有没有这个session,如果没有,在去看看客户端的cookie里面有没有这个session,如果cookie里面有,就把cookie里面的sessoin同步到web服务器B,这样就可以实现session的同步了。 
    
    缺点:cookie的安全性不高,容易伪造、客户端禁止使用cookie等都可能造成无法共享session。

3.通过服务器之间的数据同步session

  使用一台作为用户的登录服务器,当用户登录成功之后,会将session写到当前服务器上,我们通过脚本或者守护进程将session同步到其他服务器上,这时当用户跳转到其他服务器,session一致,也就不用再次登录。

  缺陷:速度慢,同步session有延迟性,可能导致跳转服务器之后,session未同步。而且单向同步时,登录服务器宕机,整个系统都不能正常运行。

4.通过NFS共享Session

  选择一台公共的NFS服务器(Network File Server)做共享服务器,所有的Web服务器登陆的时候把session数据写到这台服务器上,那么所有的session数据其实都是保存在这台NFS服务器上的,
   不论用户访问那太Web服务器,都要来这台服务器获取session数据,那么就能够实现共享session数据了。

  缺点:依赖性太强,如果NFS服务器down掉了,那么大家都无法工作了,当然,可以考虑多台NFS服务器同步的形式。

5.通过memcache同步session

  memcache可以做分布式

     他可以把web服务器中的内存组合起来,成为一个"内存池",不管是哪个服务器产生的sessoin都可以放到这个"内存池"中,其他的都可以使用。 

  优点:以这种方式来同步session,不会加大数据库的负担,并且安全性比用cookie大大的提高,把session放到内存里面,比从文件中读取要快很多。 

  缺点:memcache把内存分成很多种规格的存储块,有块就有大小,这种方式也就决定了,memcache不能完全利用内存,会产生内存碎片,如果存储块不足,还会产生内存溢出。 

6.通过redis共享session

  redis与memcache一样,都是将数据放在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

根据实际开发应用,一般选择使用memcache或redis方式来共享session.

更换机房, 服务整体迁移的方案

更换机房并进行服务迁移需要做好以下步骤:

确认目标机房:根据公司业务需求、机房资源情况以及更换机房的目的等综合考虑,选择合适的目标机房。

规划迁移方案:制定服务迁移策略、时间表、资源分配以及检验标准等,要保证有条不紊地进行服务迁移。

测试环境搭建:在目标机房搭建好测试环境,使用与生产环境一致的硬件配置和软件环境来验证系统的稳定性和性能效果。

数据备份与迁移:对所有重要数据进行备份,确保数据完整性和安全性。然后将备份数据迁移到目标机房,并对数据进行检查验证。

系统迁移与配置:将系统软件、应用程序以及相关配置文件迁移到目标机房。根据目标机房的实际情况,修改系统参数、配置文件等,确保系统能在目标机房正常运行。

测试与验收:在测试环境中对迁移后的系统进行全面测试,并与生产环境进行比较验证。测试成功后,进行上线前的最后检查。

上线运行:在上线前,做好备份和回滚准备,确保系统无误后进行上线。

监控和运营:在迁移后要及时监控系统运行状况,发现问题及时解决,保持系统的稳定和可用性。

示例:

某公司的应用服务需要从一个机房迁移到另一个机房,并保证在迁移过程中对业务的影响要小到不可见。公司采用以下方案:

计划时间:周末两天

在目标机房搭建测试环境。

数据备份:在生产环境开始迁移前,运维人员对重要数据进行备份,并进行验证验证其完整性。

系统迁移:运维人员将应用程序和系统软件包迁移至目标机房,并在测试环境中检查验证。

性能测试:通过性能测试验证迁移后的应用程序性能。

上线:在确认测试无问题或问题已经解决后,在周末早上进行应用程序的正式迁移,并解决上线过程中的任何问题。

监控和运营:跟踪系统运行,解决任何可能的问题,确保服务稳定。

消息队列 多消费者如何保证幂等性

在消息队列中,多个消费者可能会同时消费同一个消息,可能会导致重复消费的问题,因此需要保证消费的幂等性。

为了保证消息消费的幂等性,可以考虑以下几个方法:

使用唯一标识符:每个消息可以有一个唯一标识符,消费者在处理消息的时候可以根据这个唯一标识符判断是否处理过该消息,如果处理过,则直接忽略该消息。

去重缓存:可以使用一个缓存来存储已经处理过的消息,判断消息是否已经处理过可以通过缓存中是否存在该消息的唯一标识符来实现。

事务:对于消息处理过程中需要修改数据的操作,可以使用数据库的事务来保证幂等性。

状态机:可以使用状态机来表示消息处理的状态,保证每个消息只会被处理一次。

综上所述,保证消息消费的幂等性需要采取多种措施,包括使用唯一标识符、去重缓存、事务以及状态机等。具体可以根据实际情况采取不同的方法


# 1 使用唯一标识符

import uuid

def process_message(msg):
    msg_id = msg['id']
    if msg_id in processed_msgs:
        # 如果消息已经被处理过,则直接忽略
        return
    # 处理消息
    # ...
    # 将消息的唯一标识符存入已处理消息的集合中
    processed_msgs.add(msg_id)

def consume():
    while True:
        msg = queue.get()
        process_message(msg)
        
# 在上面的示例代码中,processed_msgs 是一个集合,用于存储已处理过的消息的唯一标识符。在处理消息的时候,如果消息已经被处理过,则直接忽略。
# 如果消息没有被处理过,则处理消息,同时将消息的唯一标识符存入已处理消息的集合中。




# 2 去重缓存

import redis

cache = redis.Redis(host='localhost', port=6379, db=0)

def process_message(msg):
    msg_id = msg['id']
    if cache.get(msg_id):
        # 如果消息已经被处理过,则直接忽略
        return
    # 处理消息
    # ...
    # 将消息的唯一标识符存入缓存中,并设置过期时间
    cache.setex(msg_id, 3600, 'processed')

def consume():
    while True:
        msg = queue.get()
        process_message(msg)
        
# 在上面的示例代码中,cache 是一个使用 Redis 实现的缓存,用于存储已处理过的消息的唯一标识符。
# 在处理消息的时候,如果消息已经被处理过,则直接忽略。如果消息没有被处理过,则处理消息,同时将消息的唯一标识符存入缓存中,并设置过期时间。




# 3 事务

def process_message(msg):
    msg_id = msg['id']
    with db.transaction():
        if db.select('select count(*) from msg_processed where id=%s', msg_id) > 0:
            # 如果消息已经被处理过,则直接回滚事务
            db.rollback()
            return
        # 处理消息
        # ...
        # 将消息的唯一标识符插入 msg_processed 表中
        db.insert('insert into msg_processed (id) values (%s)', msg_id)
        db.commit()

def consume():
    while True:
        msg = queue.get()
        process_message(msg)
        
# 在上面的示例代码中,db 是一个数据库连接对象,用于操作数据库。在处理消息的时候,先判断消息是否已经被处理过,如果已经被处理过,则回滚事务,
# 否则处理消息,并将消息的唯一标识符插入 msg_processed 表中。




# 4 状态机

import enum

class MessageState(enum.Enum):
    NEW = 'new'
    PROCESSING = 'processing'
    DONE = 'done'

def process_message(msg):
    msg_id = msg['id']
    state = cache.get(msg_id)
    if state == MessageState.DONE:
        # 如果消息已经被处理过,则直接忽略
        return
    if state == MessageState.PROCESSING:
        # 如果消息正在被处理,则等待下一次消费再进行处理
        return
    # 将消息的状态设置为“正在处理”
    cache.set(msg_id, MessageState.PROCESSING)
    try:
        # 处理消息
        # ...
        # 将消息的状态设置为“已完成”
        cache.set(msg_id, MessageState.DONE)
    except Exception:
        # 如果处理消息出现异常,则将消息的状态设置为“新消息”
        cache.set(msg_id, MessageState.NEW)
        raise

def consume():
    while True:
        msg = queue.get()
        process_message(msg)
        
# 在上面的示例代码中,cache 是一个使用 Redis 实现的缓存,用于存储消息的状态。在处理消息的时候,先从缓存中获取消息的状态,如果消息已经被处理过,则直接忽略;
# 如果消息正在被处理,则等待下一次消费再进行处理;否则将消息的状态设置为“正在处理”,处理消息,将消息的状态设置为“已完成”。如果处理消息出现异常,则将消息的状态设置为“新消息”

位图布隆过滤器都是用于快速判断某个元素是否存在的数据结构,但是它们之间存在一定的区别。

相似点:

    两者都是用位运算进行操作,并且采用的都是空间换时间的策略;
    只能用于元素的存在性检查,不能进行元素查找和删除操作;

不同点:

    位图适用于元素数目可预测的情况,基本上都是对已有数据进行查找,因此只需要支持一种操作——检查是否存在。
    布隆过滤器适用于元素数目无法预测的情况,并且在元素数量非常巨大的情况下能够有效降低内存消耗;

    位图只有两种状态:0和1,且每个元素只有一种映射方法。
    布隆过滤器有多种hash映射方法,每个元素通过多个hash函数映射到多个位上,因此误判的概率更高;

    布隆过滤器在随着元素添加的数量增加时,误判的概率会逐渐增加
    位图不会。因此布隆过滤器适用于对误判概率有一定容忍度的场景;

    布隆过滤器在删除元素时可能会导致误判率上升,
    位图不受影响。

综上所述,

位图适合于元素数量较少且确定的场景,误判率为0;

布隆过滤器适合于元素数量巨大不确定的场景,误判率可控。

系统设计

系统设计是软件开发过程中的重要环节,是将系统需求转化为软件系统结构或模式的过程。以下是一个基于软件工程标准的示例:

需求分析:
通过与业务方进行交流,明确系统需求和用户需求,并对其进行实际可行性分析和相关技术风险评估。根据分析结果,进一步细化需求,获得系统的功能、性能和非功能要求等关键性质。

概要设计:
在需求分析的基础上,建立起系统的整体结构和主要组成部分的关系,绘制出类图、时序图等设计图,调整并分配功能模块和组件,明确系统的性能和可扩展性等方案。

详细设计:
在概要设计的基础上,进一步细化设计,为每个模块或组件绘制出详细设计文档。其中包括类、方法的接口设计、协议的定义、异常处理、数据库设计等内容,确保系统的可维护性和高内聚低耦合性。

编码与单元测试:
根据详细设计文档进行编码,实现设计方案,并在实现过程中进行单元测试,以保证代码的功能正确性。

集成测试:
在完成单元测试后,进行功能性和集成性测试,确保各个模块的协同运行和系统的稳定性。

系统测试:
在集成测试通过后,进行系统测试,测试整个系统的功能、性能和稳定性等方面,以确保系统能够满足用户需求。

发布与维护:
在经过测试的系统正式发布后,进行系统维护,包括对相关问题的修复、功能的改进和安全问题等的优化,确保系统能够持续稳定地运行。

以上是一个简单的基于软件工程标准的系统设计示例,实际应用中需要根据具体情况进行实际操作,并综合考虑其实际可行性和相关风险。

Redis的集群模式

Redis Cluster

Redis Cluster是Redis官方提供的分布式集群方案,它采用哈希分区的方式将数据分散到多个节点上,在保证高可用和数据一致性的前提下,实现数据的快速存储和查询。
Redis Cluster最多支持16384个槽位,每个节点负责一部分槽位,当节点宕机时,槽位会自动转移至其他可用节点。

Redis Cluster通过Gossip协议来实现节点之间的信息交换和故障检测,同时支持动态扩容和缩容。
Redis Cluster在实现上比较复杂,但是提供了完整的解决方案,适用于大规模的数据存储和在线应用场景。

Redis Sentinel

Redis Sentinel是Redis官方提供的高可用方案,它通过监控Redis Master和Slave节点的状态,实现自动故障转移和故障恢复。
Redis Sentinel最少需要3个节点建立监控集群,其中一个节点为主节点(Master),另外两个节点为从节点(Slave),负责监控主节点的状态并在主节点故障时自动将从节点升级为主节点。
当主节点恢复时,从节点会自动降级为从节点,并重连到主节点。

Redis Sentinel通过RedLock算法实现故障转移,保证在任何时候只有一个节点被选举为主节点,从而避免了数据的损坏和冲突。
Redis Sentinel相比Redis Cluster来说,实现上比较简单,适用于中小型的高可用场景。

Kafka能够实现高吞吐主要有以下几个方面:

分布式架构

    Kafka是基于分布式架构设计的,在集群中每个Broker都是一个节点,可以水平扩展,因此Kafka可以无限扩展集群的存储和处理能力,从而实现高吞吐的消息传输。

零拷贝技术

    Kafka采用零拷贝技术,避免了数据的复制和传输,减少了I/O操作,提高了磁盘和网络带宽的利用率,从而提升了数据的传输效率和系统性能。

批量发送

    Kafka支持批量发送消息,将多个消息打包成一个批次一次性发送出去,减少了网络传输的开销和消息的延迟,提高了系统的吞吐量。

内存和磁盘结合存储

    Kafka使用了磁盘和内存结合的方式来存储消息,可以将消息存储在内存中,同时使用磁盘做后备存储,可以直接将消息写入磁盘中,减少了I/O次数和磁盘的开销。

多副本机制

    Kafka采用了多副本机制,将数据副本放到不同的Broker中,可以保证数据的高可用性,同时也能提高系统的吞吐量,因为消费者可以从不同的副本中读取数据,增加了消费的并发度。

综合来说,Kafka能够实现高吞吐主要得益于其分布式架构、零拷贝技术、批量发送、内存和磁盘结合存储、多副本机制等多个方面的技术优势。

HDFS

HDFS 是基于分布式架构设计的,它将文件拆成多个数据块并存储在多个不同的节点(DataNode)上,一个文件可以被分散到大量数据块,并存储在数百台或数千台计算机上,从而实现了分布式存储。

下面是HDFS如何实现分布式的几个因素:

Namenode与Datanode架构
    HDFS由Namenode和DataNode两个组件构成。

    Namenode负责管理集群中所有数据块和DataNode的位置,DataNode则实际存储数据块。
    Namenode采用单点模式,因为只需要一个节点来管理整个文件系统的元数据。

数据块大小和副本数
    HDFS将大文件切分成多个固定大小的数据块并存储在不同的DataNode中。每个数据块也会有多个副本,HDFS默认为3个副本存储在不同的节点上以保证数据的可靠性。

块的放置策略
    HDFS采用了默认的块放置策略,它根据网络拓扑结构和块位置选择一些节点,以最大化数据本地性,并使所有节点的负载尽可能均衡。

重复数据删除和数据的可靠性
    HDFS提供了一种称为“写时复制”的机制来保护数据。当数据写入时,它首先存储在临时文件中,然后复制到多个节点,直到所有副本都确认存储成功。当删除数据块时,HDFS还提供一个机制,在Namenode上记录数据块已被删除的信息,因此该块将不再被复制。

综上所述,HDFS通过NamenodeDataNode两个组件的架构、数据块大小和副本数、块的放置策略以及重复数据删除和数据的可靠性机制等多个方面, 实现了文件的均衡存储、可靠性保障和高效访问等特性,从而有效的实现了分布式存储。

RabbitMQ

RabbitMQ 是一种基于AMQP(高级消息队列协议)实现的消息中间件,它提供了可靠的消息传递、灵活的路由、消息确认和不同互连协议支持等特点。它的使用场景如下:

异步任务处理
RabbitMQ可以用于异步任务处理。例如,当用户在网站上提交一个作业时,可以使用RabbitMQ将其放入任务队列,而不必等待此作业的执行完成。

延迟消息
RabbitMQ支持延迟消息的发送和接收,可以在延迟时间达到后再处理消息。

消息通知
RabbitMQ可用于将通知、警报等消息传递给不同的消费者或程序,以便快速地处理和响应。

支持分布式系统的集成
RabbitMQ可以与不同的分布式系统和框架集成,如Apache Kafka、Apache Storm、Spring等。

任务分发
RabbitMQ可以将任务分发到不同的工作者(workers)中,以便更加高效的执行任务。

日志收集
RabbitMQ可用于将应用程序、设备等发送的日志消息统一收集到中心化位置,以便后续处理和分析。

多个数据中心
RabbitMQ可用于多个数据中心之间的数据传递,确保数据的可靠性和一致性。

总之,RabbitMQ是一种高性能、可靠的消息中间件,其使用场景非常广泛,灵活性和可扩展性都很好,可为不同的应用程序和业务提供服务。

RabbitMQ是怎么实现消费幂等

RabbitMQ 是一个能够实现消息队列的开源软件。在 RabbitMQ 中,保证消费者幂等性通常可以通过以下几种方式来实现:

    消费端处理唯一标识(message id)
        在 RabbitMQ 中,每一个发送到消息队列的消息都有唯一标识(message id),消费者可以通过对消息唯一标识进行记录,避免重复消费。当消费者接收到一条消息时,可以先检查自己是否已经处理过该消息,如果已经处理过则直接返回,否则再进行消费操作。
    
    去重表(idempotency table)
        去重表是一种用于存储已经被消费过的消息的表格。消费者在处理消息前,可以先查询去重表中是否有该条消息的记录,如果有,则直接返回,否则再进行消费操作,并将已经消费过的消息记录到去重表中。通过去重表,可以避免重复消费消息。
    
    分布式锁
        分布式锁可以协调多个消费端一起参与消费任务,通过锁机制来保证同一条消息只被一个消费者处理。当消费者要处理某个消息时,可以先尝试获取分布式锁,获取到锁后再进行消费操作,否则就放弃消费该消息。这种方式可以避免多个消费端同时处理同一条消息。

综上所述,RabbitMQ 实现消费者幂等性的方式主要有三种:

记录唯一标识、使用去重表和使用分布式锁。

除了上述的方式外,还可以使用消息拥有者机制和手动确认机制来保证消费者幂等性。

InnoDB MyIsam 的区别

InnoDBMyISAM 是MySQL两种流行的存储引擎,这两种存储引擎之间有许多区别。

数据库事务处理:InnoDB支持事务处理,可以保证在一个事务内进行的所有操作要么全部成功,要么全部失败;
             MyISAM不支持事务处理。

并发处理能力: InnoDB执行多个处理器和操作系统线程上的操作,支持高并发
            MyISAM只支持表级锁定,不支持多个处理器或操作系统线程同时操作一个表。

关键字索引:InnoDB支持关键字索引,可以提高查询的效率。
          MyISAM通过B树索引实现索引,但不支持关键字索引。

数据缓存:InnoDB支持缓存池,可以缓存数据和索引。
        MyISAM不支持缓存池。

外键约束:InnoDB支持外键约束,可以保证数据的完整性;
        MyISAM不支持外键约束。

数据表锁定:InnoDB支持行锁定,可以锁定指定行的数据;
          MyISAM只支持表锁定,锁定整个表的数据。

性能:InnoDB在处理大量并发连接时会比MyISAM更加稳定和高效,
     在处理查询操作时,MyISAM比InnoDB更快速。

InnoDB更加强大和稳定,支持事务处理和规范的外键约束,但在性能上稍逊于MyISAM。

MyISAM比较适合在需要 频繁查询 的Web应用中,而InnoDB适合在需要提高数据的完整性和并发处理能力的应用中。

一个完善的分布式系统

一个完善的分布式系统需要设计合适的架构模式,可以通过以下几个方面来考虑:

数据存储:使用分布式数据库来存储数据,例如Cassandra、HBase等。分布式数据库可以提高数据的可靠性、可用性和扩展性。同时,可以考虑使用缓存技术,例如Redis、Memcached等。

任务调度:使用分布式任务调度器,例如Celery、Dask等。分布式任务调度器可以管理任务的调度、分发和执行,保证任务的相对独立性和高效性。

消息中间件:使用分布式消息中间件,例如RabbitMQ、Kafka等。消息中间件可以实现分布式系统之间的高效通信和数据传输,保证系统的可靠性和快速响应。

负载均衡:使用分布式负载均衡器,例如Nginx、HAProxy等。负载均衡器可以将流量分配到多个服务器上,保持系统的稳定性和高可用性。

微服务架构:使用微服务架构,将系统划分为多个小型服务,每个服务独立部署、升级、扩容等,同时通过API网关实现服务之间的调用和集成。
          这种方式可以提高系统的灵活性和可维护性。

在Python语言中,可以使用Flask、Django等Web框架来开发分布式系统,通过使用Python库或者RPC协议进行节点之间的通信。 可以使用Python的第三方模块,如Pyro、RPyC等来实现分布式计算、任务调度等功能 。同时,Python还有许多逻辑处理和数据分析库,如numpy、pandas、scikit-learn等,可以实现大规模数据的处理和分析。

python 实现常量的方式

在 Python 中没有真正的常量,但是 Python 中的变量可以被视为常量,或者说,变量的值可以被视为常量
所以在 Python 中,实现常量的方式是通过使用全大写字母命名的变量

1 使用模块级别的变量来模拟常量,例如:

# constants.py
PI = 3.1415926
GRAVITY = 9.8

2 用枚举类(Enum)来定义常量,例如:

from enum import Enum

class Constants(Enum):
    PI = 3.1415926
    GRAVITY = 9.8

3 全局变量共享

一个代码中有30多个if else, 如何优化

一个代码中有 30 多个 if-else 分支,可能会让代码难以维护、可读性下降、效率低下。考虑如何对代码进行优化:

使用多态或策略模式。
    如果有多个条件分支都需要执行不同的代码块,可以考虑将这些代码块封装为不同的类或函数。这样做可以将 if-else 语句转化为具有高内聚性的类或函数,代码可维护性更高。

使用 switch-case 替换 if-else。 
    Python 中没有提供内置的 switch-case 语句,可以使用字典和普通语句的组合来实现 switch-case 的功能。例如,可以将不同 case 对应的代码块插入到字典中,然后根据字典中的键值来匹配取出相应的代码块。

对于可枚举的条件,使用迭代之类的逻辑处理。
    例如,如果条件是一个列表、集合、字典,可以使用循环结构处理它们,而不是一个个判断条件。这样能有效减少分支的数量,提高代码的可读性。

使用函数式编程。
    Python 中提供了一些函数式编程的语法和工具,例如,map、filter、reduce、lambda、itertools 等。使用这些工具,能让代码更加简洁,也能使得复杂的逻辑更好地组织起来。

总之,一个代码中有 30 多个 if-else 分支并不是一个好的设计,需要针对具体的情况进行相应的优化,来提高代码的可读性和可维护性。

python 中 生成器协程(coroutine)以及feature

在 Python 中,生成器(generator)协程(coroutine)是两个不同的概念,它们都与异步编程有关,但是实现的方式和用途略有不同。

生成器

生成器简单来说就是可迭代对象的一种,它是用函数语法定义的,在函数中使用 yield 关键字来返回一个值,并在调用时暂停函数执行,直到下一次调用才继续执行。
可以通过 for 循环来迭代生成器对象。

生成器的优点是节省内存,并且使用起来很方便。常用的生成器包括 range() 函数和简单的生成器函数等。

协程

协程是一种轻量级的线程,它是由程序员在代码中自行控制的。协程之间可以随时暂停和恢复,通过协同作用可以实现 异步编程 的效果。
在 Python 中,要想实现协程,可以使用 asyncio 和 yield from 等语法。

协程通常用来解决某些需要并发处理的问题,比如网络 IO 操作。
在网络 IO 中,当等待服务器返回数据时,程序会闲置占用资源,而使用协程可以在等待响应时释放资源,提高程序效率。

future

在 Python 中,future 用于管理异步任务的状态,通常用于实现异步操作。

在 Python 3.5 及以上版本中,future 被替换为 asyncio.Future。

future 对象可以表示异步操作的结果或者是异步任务的状态。在程序中使用 future 可以避免阻塞程序。

future 通常与 async/await 等语法一起使用,以实现异步编程。比如,在异步读取文件时,程序通常会返回一个 future 对象,然后在异步执行时再进行处理。

下面是简单的示例代码:

import asyncio

async def read_file_async(file):
    with open(file, 'r') as f:
        content = await f.read()
    return content

loop = asyncio.get_event_loop()
future = loop.create_task(read_file_async('example.txt'))
loop.run_until_complete(future)
print(future.result())

# 在上面的代码中,read_file_async 函数使用 async/await 语法实现了异步读取文件,返回了一个 future 对象,然后使用事件循环执行 future 对象并输出结果。

聚簇索引非聚簇索引

存储方式不同

    聚簇索引 是将整张表的数据按照索引顺序存放在一起,数据的存储顺序与主键的顺序一致。
    非聚簇索引 则是将索引存储在一棵B+树中,数据存储在另外的地方,并通过B+树上的指针与数据关联。

数据修改的效率不同

    聚簇索引 将数据与索引存储在一起,当需要修改聚簇索引上的数据时,需要将整个数据页都加载到内存中, 修改完成后需要将整个数据页写回磁盘,这样的效率相对较低。
    非聚簇索引  只需要更新索引,不涉及数据的移动,因此修改的效率相对较高。

索引访问效率不同

    非聚簇索引 将 索引和数据分开存储,并通过指针关联,因此在查询时需要先查询索引,再通过指针查找数据, 这样的效率相对于聚簇索引要低
    聚簇索引 由于将数据和索引存储在一起,查询时只需要访问一次索引即可获取数据,因此查询效率相对非聚簇索引要高。

总的来说,聚簇索引适合范围查询和顺序扫描等操作,而非聚簇索引适合于单条数据的查询和局部更新等操作。 在数据库设计和优化中,需要根据实际情况选择合适的索引类型以提高数据库的性能和效率。

mysqlselect 在什么情况下会 锁表

在 MySQL 中,SELECT 查询通常不会锁定表,因为默认情况下它们使用共享锁。但是,在以下情况下,SELECT 查询可能锁定表:

没有索引且使用SELECT全表查询: 

    (即WHERE条件中未指定值,或者指定的值未通过索引筛选),会产生全表扫描,这时 MySQL 会对整个表加锁,也就是说会锁定整个表,直到该读取操作完成为止。
     全表锁定会阻止其他事务对该表进行任何修改操作,直到查询操作完成。因此,在并发场景中,全表锁定可能会导致其他事务出现阻塞。
     为了避免全表锁的影响,建议在 MySQL 中创建索引,以便加快查询速度,并减少锁定的范围。还可以使用一些优化技术,如分页、限制结果集大小等,以缩小查询范围,从而减小锁定的时间和范围。

UNION 查询:如果 SELECT 语句中使用 UNION,那么 MySQL 将使用临时表来处理结果集,并锁定所使用的表。

子查询:如果 SELECT 语句中包含子查询,则MySQL可能会锁定涉及的表。如果子查询使用相同的表,则可能会锁定该表。

锁定表:如果在执行 SELECT 语句之前有一个显式的表锁定命令,那么 SELECT 查询可能会等待该锁定释放后才能执行。

阻塞情况:如果另一个事务正在修改或锁定同一张表,那么 SELECT 查询可能会等待该事务的释放或提交,从而导致表锁定。

总之,SELECT 查询通常不会锁定表,但在某些情况下可能会锁定表。为避免该问题,可以避免使用显式的表锁定命令,避免在 SELECT 语句中使用子查询,并在可能的情况下使用 WHERE 子句来筛选结果集,从而减少涉及的表数量。

脏读、幻读和不可重复读

1、脏读(读取未提交的数据)

脏读又称无效数据的读出,是指在数据库访问中,事务 A 对一个值做修改,事务 B 读取这个值,但是由于某种原因事务 A 回滚撤销了对这个值得修改,这就导致事务 B 读取到的值是无效数据。

2、不可重复读(前后数据多次读取,结果集内容不一致)

不可重复读即当事务 A 按照查询条件得到了一个结果集,这时事务 B 对事务 A 查询的结果集数据做了  修改操作,之后事务 A 为了数据校验继续按照之前的查询条件得到的结果集与前一次查询不同,导致不可重复读取原始数据。

3、幻读(前后数据多次读取,结果集数量不一致)

幻读是指当事务 A 按照查询条件得到了一个结果集,这时事务 B 对事务 A 查询的结果集数据做    新增操作,之后事务 A 继续按照之前的查询条件得到的结果集平白无故多了几条数据,好像出现了幻觉一样。

查询数据库中当前有哪些锁

SELECT INDEX_NAME,LOCK_TYPE,LOCK_MODE,LOCK_STATUS,LOCK_DATA FROM performance_schema.data_locks;

什么情况下,RR 产生幻读?(能看到数据--多条数据)

            当前读(SELECT..FOR UDPDATE、  SELECT ... LOCK IN SHARE MODE)

什么情况下,RR 解决幻读?(不能看到数据)

     1) 快照读 (快照读,普通 SELECT)
    
     2) 加锁
            (更新不存在的记录)  
            事务 A 使用 UPDATE 加锁,事务 B 无法在这之间插入新数据,这样事务 A在 UPDATE 前后读的数据保持一致,避免了幻读。

                1  update 一个不存在的 索引, 产生锁

                    一开始先加 临键锁Next-key lock; 因为是唯一索引,且更新的记录不存在,临键锁退化成 间隙锁Gap

                2 SELECT ... FOR UPDATE 

                    临键锁Next-key lock

Gap 锁(间隙锁)

是加在索引上的。

记录锁: 行锁,只会锁定一条记录。

间隙锁 :是在索引记录之间的间隙上的锁,区间为前开后开 (,)。

临键锁(Next-Key Lock): 由 记录锁 和 间隙锁Gap 组合起来。

加锁的基本单位临键锁,其加锁区间为前开后闭 (,]。

1) 索引上的等值查询,给唯一索引加锁的时候,如果满足条件,临键锁 退化为 行锁。

2) 索引上的等值查询,给唯一索引加锁的时候,如果不满足条件,临键锁 退化为 间隙锁。 注意,非等值查询是不会优化的。                 
Buy me a 肥仔水!