Java 程序员越来越被唾弃,我看 BOSS 直聘上有的公司在要求里面明文写不要 Java 程序员,感觉自己受到了侮辱,一怒之下怒了一下,既然如此别怪我心狠手辣用 Python + FastAPI 把之前 Java + Spring 的项目重写一遍。
此文涉及 MySQL、Kafka、Redis 组件和用户、资金、订单模块,整体从 Java 迁移到 Python 所需的知识点。
语言思维转换
Java 的语法很臃肿,如果会 Java 再看其他语言简直降维打击,学起来没什么难度,顶多是底层的一些设计、架构不一样。
Python 是动态语言,比如
Python
# 传统 Python
def add(a, b):
return a + b
add(1, 2) # 结果是 3
add("a", "b") # 结果是 "ab"
没有类型校验,没有运行前编译,看起来非常不安全。于是有了 Pydantic。
Python
def add_with_hints(a: int, b: int) -> int:
return a + b
括号里面是参数,-> int 是返回值,用了 Type Hints,相当于一种提示,让 Pydantic 读取到后进行校验和转换。
列举一个接口的例子,比如 Java
Java
// 1. DTO 类 (POJO)
public class CreateUserDTO {
// 2. Validation (javax.validation)
@NotNull
@Size(min = 3, max = 50)
private String username;
@Min(18)
private Integer age;
// Getters and Setters... (由 Jackson 用于序列化)
}
// 3. Controller (Jackson + Validation)
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody CreateUserDTO userDTO) {
// 到了这里,userDTO 已经被 Jackson 解析,并被 Validation 校验过了
// ...
}
而 Python + FastAPI + Pydantic 是
Python
from pydantic import BaseModel, Field
from typing import Optional # 对应 Java 的 Optional 或 null
# 1. Pydantic 模型 (这一个东西 = Java 的 DTO + Validation + Jackson 注解)
class CreateUserDTO(BaseModel):
# 对应 @NotNull 和 @Size
username: str = Field(..., min_length=3, max_length=50)
# 对应 @Min(18) 和 可选 (Integer 而非 int)
age: Optional[int] = Field(None, ge=18) # ge = Greater than or Equal
# 注意:
# 1. ... (三个点) 表示这个字段是必填的
# 2. Field(None, ...) 表示这个字段是可选的 (默认值为 None)
# 2. FastAPI 路由
@app.post("/users")
async def create_user(user_dto: CreateUserDTO):
# 到了这里,Pydantic 已经自动完成了:
# 1. 读取 JSON 请求体 (像 Jackson)
# 2. 按照类型提示 (str, int) 校验和转换数据
# 3. 运行校验规则 (min_length, ge) (像 javax.validation)
# 4. 如果校验失败,自动返回一个 422 错误的 JSON 响应
# 你可以直接使用这个对象
# user_dto.username
return user_dto
Python 里面的 Optional 和 Java 里面是同一个意思,写法不一样,Python 里面把八大基本类型的包装类也用 Optional 表示。

Python 里没有 Lombok,直接.属性就行。
并发模型
这是 Java 和 Python 最大的不同,Java 是多线程,在 Spring Web 中一个请求就是一个线程(非 WebFlux),Tomcat 里面配置最大线程数就这个作用。线程内是阻塞的,比如查数据库线程就停那了,等待数据完成了继续执行。
Python 用的 asyncio 单线程事件循环。查数据库的时候,线程去执行别的,直到数据拿到了再接着执行。(具体流程后面详细分解)
比如这个接口逻辑
-
查用户
-
查商品
-
写订单
-
写订单详情
Java 是
Java
// 线程 1
public Order createOrder(...) {
User user = userRepo.findById(...); // (线程 1 阻塞,等待 5ms)
Product product = productRepo.findById(...); // (线程 1 阻塞,等待 5ms)
Order order = new Order(...);
orderRepo.save(order); // (线程 1 阻塞,等待 10ms)
OrderDetail detail = new OrderDetail(...);
detailRepo.save(detail); // (线程 1 阻塞,等待 10ms)
return order; // 总耗时:5+5+10+10 = 30ms (线程 1 被独占 30ms)
}
Python 是
Python
# 单个事件循环线程
async def create_order(...):
# 'await':我把控制权交回事件循环
user = await user_repo.get_by_id(...) # (I/O 开始,任务暂停)
# ... 某个未来的时刻,事件循环恢复了我的执行 ...
product = await product_repo.get_by_id(...) # (I/O 开始,任务再次暂停)
# ... 再次恢复 ...
order = Order(...)
await order_repo.save(order) # (I/O 开始,任务再次暂停)
# ... 再次恢复 ...
detail = OrderDetail(...)
await detail_repo.save(detail) # (I/O 开始,任务再次暂停)
# ... 再次恢复 ...
return order # 总耗时:还是 30ms (I/O 总时间)
这导致的第一重大变化是 ThreadLocal 没了,之前用户登录的 token 可以用 AOP 一路带到线程里,在 Python 里面用 FastAPI 的 Depends 解决,后面想说。
第二个是 async 必须全程到底,一个方法用了,所有上下游调用都用,外部的 MySQL、Kafka 这种组件也要用 async 的版本。
那为什么 Python 是单线程的?
主要是 Python 的 RAM 内存管理用的 GLC(Global Interpreter Lock),不像 Java 有复杂的 GC 垃圾回收机制,GLC 在用到对象的引用计数 + 1,没用到 -1,为 0 则清理,为了防止多线程 RAM 内存出问题,有了 GLC。
asyncio 就是用单线程多进程(协程)的方式,提高并发。
进程、线程、协程有什么区别?

打开 Win 的任务管理器,最外层的 Micosoft Edge 就一个进程,括号里的 18 就是 18 的线程,协程一种特殊的线程,可以很快速的在不同任务之间切换,asyncio 里面这么叫。
Python + asyncio 这么搞效率岂不是很低?多核处理器怎么办?
其实 CPU 的速度是远超 IO 的,老早之前单核 CPU 就有时间切片模拟成多核,服务器大都是 IO 密集型,卡在查 MySQL、Kafka、Redis 等,这样其实更高效。
每个线程的启动都要消耗几 M 的 RAM,线程上下文切换也比较耗资源,进程之间效率高得多。
线上真实的多核处理器服务器,会用进程管理器 Gunicorn 根据服务器核心数启动多个 Python 进程,由 Gunicorn 将请求分发。
asyncio 的原理
asyncio 底层有两个组件,Ready Queue 和 Waiting Map。Ready Queue 是可以立即执行的队列,Waiting Map 是异步等待唤醒的任务。
以为一个请求流程为例
Python
async def create_order(...):
# 你的代码从这里开始执行
user_data = await request.json() # 假设这是第一个 await (I/O)
...
这些代码都先当做任务放到 Ready Queue,直到遇到第一个 await 则暂停,把任务移动到 Waiting Map,然后从 Ready Queue 取下一个任务执行。直到所有的 Ready Queue 都执行完成。
一个是 Queue 一个是 Map,Queue 存的是任务,Map 中存的是任务 + 回调,这样数据回调的时候知道去执行哪个任务。
await 交给操作系统系统 OS 处理,在日常开发中比如付款场景等支付宝回调,在这里类似。
await 告诉操作系统帮我查下 MySQL,有结果了通知我。但不同的操作系统通知方式不一样,Mac UNIX 的 kqueue、Linux 的 epoll 属于Reactor 的就绪通知 ,Win 的 IOPC 属于Proactor 的回调。
他俩区别是系统 OS 层的,涉及网卡、数据流那些,Proactor 帮忙把数据流复制了一份,Reactor 则自己去 read() write(),代码层面没有区别,asyncio 已经帮忙封装好了,根据操作系统自动调用。