“12306”的架构到底有多牛逼?转载
原创我在网上看到一篇文章关于我在网上看到一篇关于 12306 看了那篇关于抢票的文章,感觉文章写得很完整。
它不仅给出了模拟场景的代码,而且还使用压力测试工具测试了并发性,这是一个很好的学习案例,可以分享给普通读者阅读。
提纲
作者:为你描绘一个爱的世界作者:为你描绘一个奉献的世界 | 来源:https://juejin.im/post/5d84e21f6fb9a06ac8248149
12306 抢票,极端并发带来思考
虽然现在大多数情况下都可以订票,但我相信我们都知道门票一发行就买不到的情况。
特别是在农历新年期间,人们不仅用 12306该公司还将考虑使用“Smart”等票务软件,这段时间全国上下数以亿计的人都在抢票。
“12306 “服务”承载着世界上任何第二个系统都无法超越的重担。 QPS数以百万计的并发并不少见!拥有数百万个并发是很正常的!
笔者专门研究了《红楼梦》。12306服务器端的架构,学到了很多关于它的系统设计的亮点,在这里和大家分享 模拟实例的模拟 :如何在 100 1万人同时抢夺1万人同时抢夺1万人同时抢夺 1 当有1万张火车票时,系统提供正常、稳定的服务。
大规模、高并发的系统架构
高并发的系统架构部署在分布式集群中,在服务之上有多层负载均衡,以及多种容灾手段(双火机房、节点容错、服务器容灾等)。确保系统高可用,并根据不同的负载能力和配置策略将流量均衡到不同的服务器。
以下是一个简单的原理图。下面是一个简单的图表。下面是一个简单的图表。以下是一个简单的原理图。
负载平衡简介负载平衡简介负载平衡概述
上图描述了用户对服务器的请求经历了三层负载平衡,下面是对这三层负载平衡的简要描述。
①OSPF(开放最短链路优先)是一种内部网关协议(Interior Gateway Protocol,简称 IGP)
OSPF 通过通知路由器之间网络接口的状态来生成最短路径树,从而建立链路状态数据库。OSPF 将自动计算上的路由接口 Cost 值,但也可以通过指定接口的 Cost 值时,手动指定的值优先于自动计算的值。
OSPF 计算的 Cost同样,这与接口带宽成反比,带宽越高,Cost 值越小。达到同样的目标 Cost 值,则可以执行负载均衡,最高可达 6 链路同时执行负载均衡。链路同时实现负载均衡。
②LVS (Linux Virtual Server)
它是一种集群(它是一种集群)它是一种集群(它是一种集群)Cluster技术,使用)技术,使用)技术 IP 负载均衡技术和基于内容的请求分发技术。
该调度器具有良好的吞吐速率,将请求以均衡的方式转移到不同的服务器上执行,调度器自动屏蔽服务器故障,从而将一组服务器组成高性能、高可用的虚拟服务器。
③Nginx
我相信你们都很熟悉,这是一场非常高的演出 HTTP 代理/反向代理服务器,也经常用于服务开发以实现负载均衡。
Nginx 实现负载均衡的方法主要有三种。
-
轮询
-
加权轮询
-
IP Hash 轮询
我们将在下面寻址下面我们将寻址在这里寻址我们寻址 Nginx 用于进行专门配置和测试的加权轮询。
Nginx 加权投票演示加权投票演示加权投票演示
Nginx 通过实施负载均衡实现负载均衡通过通过 Upstream 模块实现,其中加权轮询的配置可以为相关业务增加权重值,该配置可以根据服务器的性能和负载能力来设置适当的负载。
以下是我将在本地监听的加权轮询负载的配置 3001-3004 分别配置端口,分别配置 1,2,3,4 的权重:
#配置负载平衡配置负载平衡
upstream load_rule {
server 127.0.0.1:3001 weight=1;
server 127.0.0.1:3002 weight=2;
server 127.0.0.1:3003 weight=3;
server 127.0.0.1:3004 weight=4;
}
...
server {
listen 80;
server_name load_balance.com www.load_balance.com;
location / {
proxy_pass http://load_rule;
}
我在本地 /etc/hosts 目录已配置目录 www.load_balance.com 虚拟域的地址。虚拟域地址。虚拟域名地址。
下一步使用Next,使用 Go 语言打开四种语言 HTTP 端口侦听服务,以下是正在侦听的 3001 端口的 Go 过程,其他几个过程只需要修改端口:
package main
import (
"net/http"
"os"
"strings"
)
func main() {
http.HandleFunc("/buy/ticket", handleReq)
http.ListenAndServe(":3001", nil)
}
//处理请求函数请求处理函数请求处理函数处理请求函数请求处理函数请求处理函数处理请求函数请求处理函数请求处理函数处理请求函数,根据请求将响应结果信息写入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
failedMsg := "handle in port:"
writeLog(failedMsg, "./stat.log")
}
//写入日志
func writeLog(msg string, logPath string) {
fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
defer fd.Close()
content := strings.Join([]string{msg, "
"}, "3001") buf := []byte(content) fd.Write(buf) }
我将请求的端口日志信息写入我将请求的端口日志信息写入 ./stat.log 文件,然后使用该文件,然后使用文件,然后使用 AB 压缩测试工具进行压缩测试。
ab -n 1000 -c 100 http://www.load_balance.com/buy/ticket
结果记录在统计日志中。结果记录在统计日志中。3001-3004 端口分别获取,端口分别获取,端口分别获取 100、200、300、400 请求量。请求量。请求量。请求的数量。
这和我在 Nginx 配置的权重份额匹配良好,加载后流量非常均匀和随机。
你可以参考的具体实现你可以参考的特定实现 Nginx 的 Upsteam 模块实现源代码,这里有一篇推荐文章《TheNginx 中 Upstream 使用机制实现负载均衡。机制的负载平衡。机构的负载平衡。用于负载平衡的机制。
https://www.kancloud.cn/digest/understandingnginx/202607
秒购买系统选择秒购买系统选择秒抢购系统选择秒购买系统选择
回到我们最初提到的问题:火车票抢票系统如何在高并发下提供正常稳定的服务?
从上面的介绍我们知道,用户峰值流量通过多层负载均衡均匀地分配到不同的服务器,即使如此,集群中的单机也会受到 QPS 它也很高。如何最大限度地优化单机性能?
要解决这个问题,我们需要弄清楚一件事。 通常,预订系统处理订单生成、库存减少和用户付款这三个基本阶段。
我们的系统所要做的是确保火车票订单不会超售,也不是没有售出,每一张售出的车票都必须支付才能有效,并且系统能够承受极高的并发性。
这三个阶段的顺序应该如何分配才更合理?让我们来分析一下。
下单减去库存下单减少库存
当并发用户请求到达服务器时,首先创建订单,然后扣除库存,用户等待付款。
这个订单是我们通常想到的第一个解决方案。这种情况也确保了订单不会超卖,因为订单创建后库存会减少,这是一个原子操作。
但这也带来了一些问题。但这也带来了一些问题。但这也引发了一些问题。但这也引发了一些问题。
-
在极端并发的情况下,任何内存操作的细节都对性能至关重要,特别是对于订单创建这样的逻辑,通常需要存储在磁盘数据库中,数据库的压力是可以想象的。
-
如果用户有恶意订单,只有没有付款的订单才会让库存变少,订单会卖得少很多,虽然服务器可以限制 IP 以及来自用户的购买订单数量,这也不是一个真正好的方法。
付款减去库存付款减去库存
如果你在减少库存之前等待用户支付订单费用,第一感觉是你不会卖得更少。但这是并发体系结构的一大禁忌,因为在极端的并发情况下,用户可能会创建许多订单。
当库存降至零时,很多用户发现自己的订单无法支付,这也被称为“超卖”。也无法避免数据库磁盘的并发操作 IO。
预扣库存
从以上两个选项的考虑中,我们可以得出结论,一旦创建订单,我们就必须频繁地操作数据库 IO。
那么有没有不需要直接操作的数据库呢 IO 该方案是预扣减库存。先扣减库存,确保不超卖,然后异步生成用户订单,这样对用户的反应会快很多;那么如何确保不减卖呢?如果用户收到订单但不付款怎么办?
我们都知道,订单现在是有到期日的,例如,如果用户在五分钟内没有付款,订单就会过期,一旦订单过期,它就会被添加到新的库存中,现在很多在线零售商都在使用该程序来确保商品不会少卖。
订单生成是异步的,通常放入 MQ、Kafka 这样的即时消费队列在相对较小的订单量中处理,非常快速地生成订单,并且用户几乎不需要排队。
库存扣减的艺术扣留库存的艺术库存扣减的艺术
从上面的分析可以明显看出,预扣库存的解决方案最有意义。我们进一步分析一下库存抵扣的细节,这里还有很大的优化空间,库存在哪里?如何在高并发的情况下保证正确的库存扣减,同时仍然快速响应用户请求?
在单机低并发的情况下,我们通常按如下方式实现库存扣减。
为了保证库存扣减和订单生成的原子性,需要使用交易处理,然后进行库存判断,减少库存,最后提交交易,整个过程有很多 IO对数据库的操作再次阻塞。
这种方法根本不适合高并发的尖峰系统。接下来,我们对独立的库存扣除方案进行优化:本地库存扣除。
我们将一定数量的库存分配给本地机器,直接在内存中减去库存,然后按照前面的逻辑异步创建订单。
改进后的独立系统如下所示。
这避免了对频繁数据库的需要这避免了频繁这消除了对频繁的需要 IO 操作,只在内存中执行操作,极大地提高了单机抵抗并发的能力。
但在任何情况下,一台机器都无法拒绝一百万个用户请求,尽管 Nginx 处理网络请求使用处理Web请求 Epoll 模型,c10k 这个问题在业内早已得到解决。
但是 Linux 在该系统下,所有资源都是文件,网络请求也是文件。大量文件描述符可能会导致操作系统立即失去响应。
如上所述我们已经提到了我们上面提到了 Nginx 对于加权均衡策略,我们可以假设我们将 100W 用户请求量平均均衡到用户请求量平均均衡到 100 单台服务器上的并发计算机数量要少得多。
然后我们有每台计算机的本地库存,然后是每台计算机的本地库存,然后是每台计算机的本地库存 100 一张火车票比一张火车票。买火车票的火车票。100 服务器上的总库存仍然是服务器上的总库存或服务器上的总库存仍然是服务器上的总库存或 1 百万,这确保库存订单不会超卖,以下是我们描述的集群体系结构。
问题出现了,在高并发的情况下,我们不能保证系统的高可用性,并且如果这样 100 一台服务器上的两台或三台机器停机,因为它们无法承载并发流量或其他原因。然后,这些服务器上的订单不会被出售,这会导致更少的订单被出售。
要解决这个问题,我们需要对总订单量做统一管理,这是下一步的容错解决方案。服务器不仅要在本地减少库存,还要远程、统一地减少库存。
通过远程统一库存削减操作,我们可以根据机器负载为每台机器分配一些多余的库存。Buffer 库存“用于防止机器中的机器停机。
让我们结合下面的架构图来分析它。
我们采用 Redis 将统一库存存储为存储统一库存作为存储统一库存 Redis 性能非常高,只需要一台机器 QPS 能抗 10W 的并发。
在本地库存减少后,如果有本地订单,我们会返回并请求 Redis 远程降库存、本地降库存、远程降库存成功后才返回给用户抢票成功提示,也有效保障了订单不超卖。
当其中一台机器出现故障时,因为每台机器都有一个预留的 Buffer 剩余的车票,所以下机上的剩余车票仍然可以在其他机器上补齐,确保了大量的销售。
Buffer 理论上,剩余门票的合适数量是多少? Buffer 您设置的越多,系统允许停机的计算机就越多,但是 Buffer 设置得太大也会对 Redis 会造成一些影响。造成了一些影响。会造成一定的影响。造成了一些影响。
虽然 Redis 内存中的数据库对并发的抵抗力很强,请求仍然会通过网络一次 IO其实抢票的过程其实就是抢票的过程 Redis 请求数是本地库存的函数,并且 Buffer 库存总量。库存总额。总库存量。股票总数。
因为当当地库存低的时候,系统直接给用户返回一条售罄的消息,所以不会遵循统一扣库存的逻辑。
这在某种程度上也避免了将大量网络请求放入 Redis 压过去,所以压过去,这样压过去,所以 Buffer 设置多少值需要架构师仔细考虑系统的负载能力。
代码演示
Go 该语言本身就是为并发而设计的,我使用 Go 该语言为您演示了在一台机器上抢票的确切过程。
初始化工作初始化工作初始化工作
Go 包中的 Init 函数先于 Main 功能执行,在这一阶段主要做一些准备工作。
我们的系统需要通过初始化本地库存、初始化远程 Redis 统一库存的存储统一库存的存储统一库存的存储 Hash 键值,初始化键值,初始化键,初始化 Redis 连接池。
您还需要初始化文件的大小,还需要初始化 1 的 Int 类型 Chan目的是实现分布式锁定功能。
您也可以直接使用读/写锁或使用 Redis 和其他避免资源竞争的方法,但使用 Channel 更高效,那个更高效,那个更高效,这个更高效,就是这样 Go 这门语言的理念是:不要通过分享记忆来交流,而要通过交流来分享记忆。
Redis 库使用库使用的库正在使用的 Redigo以下是代码实现。以下是代码实现。实现了以下代码。
...
//localSpike包结构定义包结构定义
package localSpike
type LocalSpike struct {
LocalInStock int64
LocalSalesVolume int64
}
...
//remoteSpike对hash结构的定义和结构的定义以及结构和结构的定义redis连接池
package remoteSpike
//远程订单存储健壮值远程订单存储KIN值远程订单存储KIN值远程订单存储稳健值
type RemoteSpikeKeys struct {
SpikeOrderHashKey string //redis中等尖峰顺序中等尖峰顺序中秒顺序hash结构key
TotalInventoryKey string //hash结构中的总订单库存结构中的总订单库存结构中的总订单库存key
QuantityOfOrderKey string //hash结构中的现有订单数量结构中已有的订单数量结构中的现有订单数量结构中的订单数量key
}
//初始化redis连接池
func NewPool() *redis.Pool {
return &redis.Pool{
MaxIdle: 10000,
MaxActive: 12000, // max number of connections
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", ":6379")
if err != nil {
panic(err.Error())
}
return c, err
},
}
}
...
func init() {
localSpike = localSpike2.LocalSpike{
LocalInStock: 150,
LocalSalesVolume: 0,
}
remoteSpike = remoteSpike2.RemoteSpikeKeys{
SpikeOrderHashKey: "ticket_hash_key",
TotalInventoryKey: "ticket_total_nums",
QuantityOfOrderKey: "ticket_sold_nums",
}
redisPool = remoteSpike2.NewPool()
done = make(chan int, 1)
done <- 1
}
本地库存扣除和统一库存扣除本地和统一库存扣除本地和统一库存扣除
本地库存扣减逻辑很简单,用户请求,将销售额相加,然后比较销售额是否大于本地库存和退货 Bool 值:
package localSpike
//本地库存扣除从库存本地扣除本地扣除库存本地从库存扣除,返回bool值
func (spike *LocalSpike) LocalDeductionStock() bool{
spike.LocalSalesVolume = spike.LocalSalesVolume + 1
return spike.LocalSalesVolume < spike.LocalInStock
}
请注意此处的共享数据 LocalSalesVolume 的操作是要使用锁来实现的,但是因为本地库存扣除和统一库存扣除本地和统一库存扣除本地和统一库存扣除是一个原子性操作,所以在最上层使用 Channel 为了实现这一点,这一点将在后面讨论。为了实现这一点,这一段将在稍后发言。
统一库存扣除操作统一库存扣除操作统一库存扣除操作 Redis,因为 Redis 是单线程的,我们想要实现从它获取数据、写入数据和计算一些列的步骤,我们必须配合 Lua 编写打包命令的脚本以确保操作的原子性。
package remoteSpike
......
const LuaScript = `
local ticket_key = KEYS[1]
local ticket_total_key = ARGV[1]
local ticket_sold_key = ARGV[2]
local ticket_total_nums = tonumber(redis.call(HGET, ticket_key, ticket_total_key))
local ticket_sold_nums = tonumber(redis.call(HGET, ticket_key, ticket_sold_key))
-- 检查是否还有票检查是否还有剩余的票检查是否还有剩余的票检查是否还有票,增加订单数增加订单数增加订单量增加订单量,返回结果值返回结果值返回结果值
if(ticket_total_nums >= ticket_sold_nums) then
return redis.call(HINCRBY, ticket_key, ticket_sold_key, 1)
end
return 0
`
//远程统一库存扣除远程统一库存扣除统一远程库存扣除远程统一库存暂挂
func (RemoteSpikeKeys *RemoteSpikeKeys) RemoteDeductionStock(conn redis.Conn) bool {
lua := redis.NewScript(1, LuaScript)
result, err := redis.Int(lua.Do(conn, RemoteSpikeKeys.SpikeOrderHashKey, RemoteSpikeKeys.TotalInventoryKey, RemoteSpikeKeys.QuantityOfOrderKey))
if err != nil {
return false
}
return result != 0
}
我们使用 Hash 该结构存储有关总库存和总销售额的信息,当用户请求该信息时,它确定总销售额是否大于库存,并返回相关的 Bool 值。
在启动服务之前,我们需要初始化 Redis 的初始库存信息。的初始库存信息。
hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0
响应用户消息响应用户信息响应用户信息响应用户消息
我们打开一个我们打开一个让我们打开一个 HTTP 服务,侦听一个端口。服务,侦听位于的端口。服务,侦听位于的端口
package main
...
func main() {
http.HandleFunc("/buy/ticket", handleReq)
http.ListenAndServe(":3005", nil)
}
上面我们做完了所有的初始化工作初始化工作初始化工作,接下来 handleReq 逻辑很清楚,判断抢票成功与否,返回用户信息就可以了。
package main
//处理请求函数请求处理函数请求处理函数处理请求函数请求处理函数请求处理函数处理请求函数请求处理函数请求处理函数处理请求函数,根据请求将响应结果信息写入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
redisConn := redisPool.Get()
LogMsg := ""
<-done
//全局读/写锁全局读/写锁全局读写锁
if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
util.RespJson(w, 1, "抢票成功", nil)
LogMsg = LogMsg + "result:1,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
} else {
util.RespJson(w, -1, "已售罄", nil)
LogMsg = LogMsg + "result:0,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
}
done <- 1
//写入要写入票证状态的票证状态log中
writeLog(LogMsg, "./stat.log")
}
func writeLog(msg string, logPath string) {
fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
defer fd.Close()
content := strings.Join([]string{msg, "
"}, "") buf := []byte(content) fd.Write(buf) }
正如前面提到的,我们在扣除库存时必须考虑竞争条件,这里我们使用 Channel 避免并发读取和写入可确保高效地顺序执行请求。我们将接口的返回信息写入 ./stat.log 该文件便于进行试压统计。
独立服务压力测试独立服务压力测试独立服务压力测试
要打开服务,我们使用打开服务,我们使用 AB 压缩测试工具执行以下测试。
ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket
这是我的本地低点这是我的本地低点这是我的本地低点 Mac 的测压信息。的测压信息。的试压信息
This is ApacheBench, Version 2.3 <$revision: 1826891="">
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 127.0.0.1 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests
Server Software:
Server Hostname: 127.0.0.1
Server Port: 3005
Document Path: /buy/ticket
Document Length: 29 bytes
Concurrency Level: 100
Time taken for tests: 2.339 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 1370000 bytes
HTML transferred: 290000 bytes
Requests per second: 4275.96 [#/sec] (mean)
Time per request: 23.387 [ms] (mean)
Time per request: 0.234 [ms] (mean, across all concurrent requests)
Transfer rate: 572.08 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 8 14.7 6 223
Processing: 2 15 17.6 11 232
Waiting: 1 11 13.5 8 225
Total: 7 23 22.8 18 239
Percentage of the requests served within a certain time (ms)
50% 18
66% 24
75% 26
80% 28
90% 33
95% 39
98% 45
99% 54
100% 239 (longest request)
根据指标,我可以在一台机器上每秒处理它 4000+ 请求,正常的服务器是多核配置,处理 1W+ 这个请求根本不是问题。这一请求根本不是问题。这个请求根本不成问题。这个要求根本不成问题。
查看日志显示,请求正常,流量均匀贯穿整个服务。Redis 也很正常。也很正常。这也是正常的。这也是正常的。
//stat.log
...
result:1,localSales:145
result:1,localSales:146
result:1,localSales:147
result:1,localSales:148
result:1,localSales:149
result:1,localSales:150
result:0,localSales:151
result:0,localSales:152
result:0,localSales:153
result:0,localSales:154
result:0,localSales:156
...
总结回顾
总体而言,扣球系统非常复杂。我们在这里简单介绍一个模拟如何将单机优化到高性能,如何避免集群中单点故障,保证订单不超卖,不减卖的一些策略
完整的订单系统还具有订单进度的视图、每个服务器上的任务以同步来自总库存的剩余票证和库存信息以定期向用户显示,以及用户在订单有效期内不付款、释放订单、将其补充到库存等。
我们已经实现了高并发售票的核心逻辑,可以说系统设计得非常巧妙,巧妙地避免了需要 DB 数据库 IO 的操作。
对 Redis 网络 IO 请求的高并发性,几乎所有的计算都在内存中完成,它有效地确保了没有超卖、没有低价,并能够容忍一些机器停机。
我认为有两点特别值得学习总结。
①负载均衡,分治①负载均衡,分治①负载均衡,分治
通过负载均衡,将不同的流量划分到不同的机器上,每台机器处理自己的请求,最大化其性能。
这样,整个系统也能承受极高的并发性,就像一个工作中的团队,每个人都把自己的价值发挥在最前面,团队成长自然很棒。
②合理使用并发和异步②合理使用并发和异步②合理使用并发和异步
自 Epoll 网络架构模型解决了网络架构模型所解决的问题 c10k 自该问题以来,异步越来越被服务器端开发人员接受,可以用异步完成的工作都是用异步完成的,这在功能拆卸方面可以达到意想不到的结果。
这点在 Nginx、Node.JS、Redis 上都能体现,他们处理网络请求使用处理Web请求的 Epoll 模型,在实践中向我们展示了单线程仍然可以很强大。
服务器已经进入多核时代。服务器进入多核时代,服务器进入多核时代Go 像这样为并发而生的语言完美地利用了服务器多核,许多可以并发处理的任务都可以使用并发来解决,例如 Go 处理 HTTP 每个请求都是在a中发出的,每个请求都是在a中发出的 Goroutine 中执行。
总之,如何合理挤压,如何合理挤压,如何明智挤压,如何明智挤压 CPU这是一个我们始终需要探索和学习的方向,以便使其发挥应有的作用。
推荐阅读:
入门: 最完整的零基学习最全面的零基学习最完整的零基学习Python的问题 | 从零开始学习从零基础学习从零基础学习8个月的Python | 实战项目 |学Python这是捷径,这是捷径,这是捷径
干货:爬行豆瓣短评,电影《后来的我们》 | 38年NBA最佳球员分析最佳球员分析 | 从万众期待到口碑惨败!唐探3令人失望 | 笑新伊田图龙记笑新伊田图龙记笑新伊田图龙记 | 谜语之王回答灯谜之王灯谜之王谜语之王 |用Python人山人海素描图人山人海素描图人山人海 Dishonor太火了,我用机器学习做了一个迷你推荐系统电影
趣味:弹球游戏 | 九宫格 | 漂亮的花 | 两百行Python日常酷跑游戏日常酷跑游戏日常酷跑游戏!
AI: 会写诗的机器人会写诗的机器人会写诗的机器人 | 给图片上色给图片上色给图片上色 | 预测收入 | 《耻辱》太火了,我用机器学习做了一部迷你推荐系统电影
小工具: Pdf转Word易于修复表单和水印!易于处理的表单和水印!轻松修复桌子和水印!易于修复的形式和水印! | 一键把html将页面另存为网页另存为网页另存为pdf!| 再见PDF提款费!提款费!提款费!提款费用! | 用90构建最强大的代码行构建最强大的代码行构建最强大的代码行PDF转换器,word、PPT、excel、markdown、html一键转换 | 制作一个固定的低成本机票提醒!制作一张别针的低价机票提醒! |60代码行做了一个语音墙纸切换,天天见女士!
年度弹出文案年度弹出文案年度爆炸性文案
-
1). 卧槽!Pdf转Word用Python轻松搞定 !
-
2).学Python闻起来好香!我用100一行代码做了一个网站,帮助人们做了一行代码,做了一个网站,帮助了人们做了一行代码,帮助了人们PS旅行图片赚鸡腿吃旅行图片赚鸡腿
-
3).第一次播放量过亿,火爆全网,我分析了《波妹》,发现了这些秘密
-
4). 80一行行代码!使用Python让救济金做正确的事做做的人做好事的人A梦分身
-
5).你必须掌握的东西你必须掌握20个python代码,简短而紧凑,永无止境的有用代码,简短而甜蜜,永无止境的有用的代码,简短而紧凑,永无止境的使用代码,简短而甜蜜,永无止境的用途
-
6). 30个Python古怪技能集古怪小贴士收藏古怪技能集
-
7). 我总结的80《菜鸟学习专页》《菜鸟学习专页》《菜鸟学习》Python精选干货.pdf》,都是干货
-
8). 再见Python!我要学Go了!2500词深度分析词深度分析词深度分析 !
-
9).发现了一只舔狗的福利!这Python爬虫神器太酷了,不能自动下载女孩的照片
点击阅读原文点击查看点击点击阅读点击阅读原文点击查看B放我鸽子看录像!站在我的录像带上!在视频里放我鸽子!站在我的录像带上!
版权声明
所有资源都来源于爬虫采集,如有侵权请联系我们,我们将立即删除