Go语言在医疗IT中的MySQL高可用集群架构实践:从选型到百万QPS

大家好,我是阿亮。我在医疗信息技术(Health IT)这个行业摸爬滚打了 8 年多,主要工作是带队构建我们公司的核心业务系统,比如电子临床试验数据采集系统(EDC)、患者报告结果系统(ePRO)以及一系列的医院和临床研究管理平台。

这些系统有一个共同的特点:对数据的准确性、安全性和可用性要求极高。一条错误的患者数据、一次系统宕机,都可能带来无法估量的后果。今天,我想结合我们团队在实际项目中踩过的坑和积累的经验,跟大家聊聊如何用 Go 语言构建一个能支撑大流量、高可用的 MySQL 数据库集群架构。这篇文章不是纯理论,更多的是我们在一线项目中的实战总结。


第一章:万里长征第一步:为医疗系统选择合适的数据库

在项目初期,技术选型是重中之重。尤其在我们这个行业,数据就是生命线,选错数据库可能会给项目埋下巨大的隐患。

1.1 关系型数据库:数据完整性的"金标准"

我们处理的大部分业务,比如患者基本信息、临床试验方案、药品记录 等,都具有高度结构化的特点,而且数据之间存在强关联。更重要的是,这些操作必须保证事务性

想象一个场景:医生为一个临床试验项目招募一名新患者。这个操作至少需要两步:

  1. patients 表里创建一条新的患者记录。
  2. trial_enrollment 表里将这位患者与特定的试验项目关联起来。

这两步必须是一个原子操作:要么都成功,要么都失败。绝不允许出现患者被创建了,但没有成功入组的情况。这种场景下,支持 ACID(原子性、一致性、隔离性、持久性)事务的关系型数据库,比如 MySQL,就成了我们的不二之_选择_。

对于 Go 开发者来说,官方的 database/sql 包提供了一套标准的接口来与这类数据库交互。

go 复制代码
package main

import (
    "database/sql"
    "fmt"
    "log"
    "time"

    _ "github.comcom/go-sql-driver/mysql" // 匿名导入MySQL驱动,让database/sql知道如何与MySQL通信
)

// Patient 定义了患者信息结构体
type Patient struct {
    ID        int
    Name      string
    DateOfBirth string // 为简化示例,使用字符串
}

func main() {
    // DSN (Data Source Name) 格式: user:password@tcp(host:port)/dbname
    // 在生产环境中,这些信息应该从配置文件或环境变量中读取
    dsn := "root:your_password@tcp(127.0.0.1:3306)/clinical_trials_db"

    // sql.Open 只是验证参数格式,并不会真正建立连接
    db, err := sql.Open("mysql", dsn)
    if err != nil {
       log.Fatalf("数据库配置错误: %v", err)
    }
    // 在函数结束时关闭数据库连接池,释放资源
    defer db.Close()

    // 配置连接池参数,这对于生产环境至关重要
    db.SetConnMaxLifetime(time.Minute * 3) // 连接可复用的最大时间
    db.SetMaxOpenConns(10)                  // 打开的最大连接数
    db.SetMaxIdleConns(10)                  // 空闲连接池中连接的最大数量

    // db.Ping() 尝试与数据库建立连接,检查DSN是否有效
    err = db.Ping()
    if err != nil {
       log.Fatalf("无法连接到数据库: %v", err)
    }

    fmt.Println("成功连接到 MySQL!")

    // 示例:查询一个患者信息
    var p Patient
    patientID := 1
    // QueryRow 用于查询单行数据
    err = db.QueryRow("SELECT id, name, date_of_birth FROM patients WHERE id = ?", patientID).Scan(&p.ID, &p.Name, &p.DateOfBirth)
    if err != nil {
       if err == sql.ErrNoRows {
          fmt.Printf("未找到ID为 %d 的患者\n", patientID)
       } else {
          log.Printf("查询失败: %v", err)
       }
    } else {
       fmt.Printf("查询成功: %+v\n", p)
    }
}

关键点解读

  • _ "github.com/go-sql-driver/mysql" :这里的下划线 _ 是 Go 的一个特性,叫做匿名导入 。它的作用是告诉编译器:我需要这个包,请执行它的 init() 函数,但我不会在代码里直接使用这个包的任何标识符。对于数据库驱动来说,它的 init() 函数会把自己注册到 database/sql 中,这样 sql.Open 才能通过驱动名 "mysql" 找到它。
  • 连接池 (db.SetMaxOpenConns, db.SetMaxIdleConns):千万不要忽略这几行代码。在高并发系统中,频繁地创建和销毁数据库连接是非常耗费资源的操作。连接池会预先创建并维护一定数量的连接,当你的代码需要操作数据库时,它会从池里拿一个现成的连接,用完再还回去。这极大地提升了性能,并减少了服务器的压力。这是每个 Go 后端工程师必须掌握的知识点。

1.2 NoSQL 数据库:应对灵活多变的业务数据

当然,不是所有数据都适合放在关系型数据库里。比如我们的 ePRO(电子患者自报告结局)系统 ,患者会填写各种各样的问卷,这些问卷的结构可能经常变化,字段也不固定。如果用 MySQL,每次问卷一改,DBA 就得 ALTER TABLE,非常痛苦。

这种场景下,像 MongoDB 这样的文档型 NoSQL 数据库就派上用场了。它的数据以类似 JSON 的 BSON 格式存储,没有固定的表结构(Schema-less),非常灵活。

go 复制代码
// 这是一个概念性的 Go 结构体,用于映射 MongoDB 中的文档
// 在实际项目中,我们会使用官方的 Go Driver for MongoDB
package main

// PatientReportedOutcome 代表一份患者填写的问卷
type PatientReportedOutcome struct {
    ID          string    `bson:"_id,omitempty"` // MongoDB 的主键
    PatientID   int       `bson:"patientId"`
    TrialID     string    `bson:"trialId"`
    SubmissionDate time.Time `bson:"submissionDate"`
    // FormData 可以是任意的键值对,非常灵活
    FormData    map[string]interface{} `bson:"formData"`
}

小结 :技术选型没有银弹。我们的原则是:核心、结构化的业务数据(患者、医嘱、试验方案)用 MySQL;灵活、多变、非核心的数据(日志、问卷、用户行为)用 NoSQL。今天我们聚焦在 MySQL 上,因为它是我们绝大多数系统的基石。


第二章:从单点到集群:构建高可用的 MySQL 架构

单台 MySQL 服务器就像在走钢丝,一旦服务器宕机、硬盘损坏或者网络故障,整个业务就瘫痪了。在医疗领域,这是绝对不能接受的。所以,我们必须搭建一个高可用的集群。

2.1 核心基石:MySQL 主从复制

这是实现高可用的最基本、也是最重要的机制。

  • 原理一句话解释 :主数据库(Master)把所有的数据更改操作(INSERT, UPDATE, DELETE)都记录到一个叫 binlog(二进制日志)的文件里。从数据库(Slave)会伪装成一个客户端,连接到主库,把 binlog 拉到自己本地(存为 relay log),然后再一条一条地执行这些操作,从而保证自己的数据和主库一致。

  • 在我们业务中的价值

    1. 灾备:主库万一挂了,我们可以手动(或者通过工具自动)把一个从库提升为新的主库,业务可以快速恢复。
    2. 读写分离 :这是性能优化的关键。系统中的读操作(查询患者信息、拉取报告)通常远多于写操作(更新病历)。我们可以让所有的写操作都走主库 ,而把大量的读操作分摊到各个从库上,极大地减轻主库的压力。

2.2 读写分离的 Go-Zero 实战

理论说完了,我们来看代码。在我们的微服务体系中,广泛使用了 go-zero 框架。它对读写分离提供了开箱即用的支持,非常方便。

假设我们正在开发一个 patient-api 服务,提供查询患者信息的功能。

第一步:定义 API 文件 (patient.api)

api 复制代码
type (
    PatientInfoReq {
       PatientID int64 `path:"id"`
    }

    PatientInfoResp {
       ID   int64  `json:"id"`
       Name string `json:"name"`
       Age  int    `json:"age"`
    }
)

service patient-api {
    @handler GetPatientInfo
    get /patient/:id (PatientInfoReq) returns (PatientInfoResp)
}

第二步:配置数据库 (etc/patient-api.yaml)

go-zero 默认不支持读写分离的配置,但我们可以通过自定义配置来实现。一个更优雅的方式是使用像 GORM 这样的库,它内置了读写分离的支持。这里我们用 go-zero 自带的 sqlx 来模拟这个逻辑。为了演示,我们假设主库和从库在同一个配置文件中。

yaml 复制代码
Name: patient-api
Host: 0.0.0.0
Port: 8888
# 数据库配置
Database:
  Master:
    DataSource: root:your_password@tcp(127.0.0.1:3306)/clinical_trials_db?charset=utf8mb4&parseTime=true&loc=Local
  Slaves:
    - DataSource: root:your_password@tcp(127.0.0.1:3307)/clinical_trials_db?charset=utf8mb4&parseTime=true&loc=Local
    - DataSource: root:your_password@tcp(127.0.0.1:3308)/clinical_trials_db?charset=utf8mb4&parseTime=true&loc=Local

第三步:编写 Logic 代码 (internal/logic/getpatientinfologic.go)

我们需要稍微改造一下 ServiceContext 来持有主从库的连接。

首先,在 internal/svc/servicecontext.go 中:

go 复制代码
package svc

import (
    "math/rand"
    "time"

    "github.com/zeromicro/go-zero/core/stores/sqlx"
    "patient-api/internal/config"
)

type ServiceContext struct {
    Config      config.Config
    MasterDB    sqlx.SqlConn
    SlaveDBs    []sqlx.SqlConn
    PatientModel *model.PatientModel // 假设我们有一个 model 层
}

func NewServiceContext(c config.Config) *ServiceContext {
    // 创建主库连接
    masterConn := sqlx.NewMysql(c.Database.Master.DataSource)
    
    // 创建从库连接列表
    var slaveConns []sqlx.SqlConn
    for _, slaveConfig := range c.Database.Slaves {
       slaveConns = append(slaveConns, sqlx.NewMysql(slaveConfig.DataSource))
    }

    return &ServiceContext{
       Config:      c,
       MasterDB:    masterConn,
       SlaveDBs:    slaveConns,
       // 初始化 model,并传入数据库连接
       PatientModel: model.NewPatientModel(masterConn, slaveConns...),
    }
}

// 随机选择一个从库连接
func (svc *ServiceContext) GetSlaveDB() sqlx.SqlConn {
    if len(svc.SlaveDBs) == 0 {
       return svc.MasterDB // 如果没有配置从库,降级使用主库
    }
    // 设置随机种子
    rand.Seed(time.Now().UnixNano())
    return svc.SlaveDBs[rand.Intn(len(svc.SlaveDBs))]
}

然后,在 model 层,我们的查询方法可以利用这个机制:

go 复制代码
// internal/model/patientmodel.go

// ... (model 定义) ...
type PatientModel struct {
    masterConn sqlx.SqlConn
    slaveConns []sqlx.SqlConn
}

func NewPatientModel(master sqlx.SqlConn, slaves ...sqlx.SqlConn) *PatientModel {
    return &PatientModel{
        masterConn: master,
        slaveConns: slaves,
    }
}

// 随机选择一个从库
func (m *PatientModel) getReader() sqlx.SqlConn {
    if len(m.slaveConns) == 0 {
        return m.masterConn
    }
    rand.Seed(time.Now().UnixNano())
    return m.slaveConns[rand.Intn(len(m.slaveConns))]
}

func (m *PatientModel) FindOne(ctx context.Context, id int64) (*Patient, error) {
    query := "SELECT id, name, age FROM patients WHERE id = ? LIMIT 1"
    var resp Patient
    
    // 使用 getReader() 获取一个用于读取的连接
    err := m.getReader().QueryRowCtx(ctx, &resp, query, id)
    return &resp, err
}

func (m *PatientModel) Update(ctx context.Context, data *Patient) error {
    query := "UPDATE patients SET name = ?, age = ? WHERE id = ?"
    
    // 写入操作必须使用主库连接
    _, err := m.masterConn.ExecCtx(ctx, query, data.Name, data.Age, data.ID)
    return err
}

最后,我们的 logic 层调用 model 即可,完全不用关心底层是主库还是从库。

go 复制代码
// internal/logic/getpatientinfologic.go
func (l *GetPatientInfoLogic) GetPatientInfo(req *types.PatientInfoReq) (resp *types.PatientInfoResp, err error) {
    patient, err := l.svcCtx.PatientModel.FindOne(l.ctx, req.PatientID)
    if err != nil {
       if err == sqlc.ErrNotFound {
          return nil, errors.New("患者不存在")
       }
       return nil, err
    }

    return &types.PatientInfoResp{
       ID:   patient.ID,
       Name: patient.Name,
       Age:  patient.Age,
    }, nil
}

关键点解读

  • 透明化处理logic 层作为业务逻辑的核心,它只关心"根据 ID 获取患者信息",而不需要知道这个查询到底发给了哪个数据库实例。这种分层设计让代码更清晰、更易于维护。
  • 负载均衡策略 :我们这里用了最简单的随机 策略来选择从库。在实际生产中,还可以实现更复杂的策略,比如轮询加权轮询 (给性能好的机器更高权重),甚至是基于延迟的动态选择

2.3 数据量大了怎么办?分库分表

随着业务发展,我们的一个核心系统------临床试验电子数据采集系统(EDC)遇到了瓶颈。单个 clinical_data 表的数据量增长到了几十亿行。这时候,即使加了索引,单个查询也非常慢,而且整个数据库的备份和恢复都成了噩梦。

解决方案就是分库分表

  • 垂直拆分:按业务维度拆。比如,把用户中心、订单中心、产品中心的数据分别放到不同的数据库里。这个比较好理解,我们在微服务初期就已经做了。
  • 水平拆分 :这个是重点。就是把一张大表的数据,按照某个规则(比如 user_id 取模、按时间范围),拆分到多个结构相同的表里,这些表甚至可以分布在不同的数据库服务器上。

实战场景:按 hospital_id (医院ID) 水平分表

我们的 EDC 系统是给全国多家医院使用的,不同医院之间的数据天然隔离。这是一个非常理想的分片键(Sharding Key)。我们可以按照 hospital_id 进行哈希取模,决定一条数据具体落在哪张分表里。

比如,我们准备了 4 张分表 clinical_data_0, clinical_data_1, clinical_data_2, clinical_data_3

当一条 hospital_id 为 101 的数据进来时,我们计算 101 % 4 = 1,那么这条数据就应该存到 clinical_data_1 表里。

挑战 :分库分表后,应用程序的查询逻辑会变得异常复杂。SELECT * FROM clinical_data WHERE patient_id = 'P123' 这条简单的 SQL,现在程序需要先知道 P123 这位患者属于哪个医院,计算出分片,然后才能查询正确的表。

解决方案 :引入数据库中间件,比如 ShardingSphereMyCat。它会对应用层伪装成一个单一的 MySQL 数据库。你还是执行普通的 SQL,中间件会自动帮你解析 SQL、找到正确的分片并路由过去,对应用层完全透明。

Go 语言如何应对? 在 Go 中,我们通常不直接在业务代码里处理分片逻辑,而是依赖于这些中间件。我们的 Go 应用就像连接一个普通的 MySQL 一样连接中间件的代理地址即可。


第三章:保障稳定性:编码和运维中的魔鬼细节

架构设计好了,但日常的开发和运维中还有很多细节决定成-败。

3.1 GORM 事务处理的正确姿势

前面提到了事务的重要性。在 Go 项目中,很多人喜欢用 GORM 这个 ORM 框架来简化数据库操作。GORM 提供了非常方便的事务 API。

让我们用一个给患者添加用药记录 的场景来演示,这个操作需要同时更新 patients 表的 last_medication_date 字段,并在 medication_logs 表里插入一条新记录。这里我们使用 Gin 框架来构建一个简单的 API。

go 复制代码
package main

import (
    "time"
    "github.com/gin-gonic/gin"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

// ... (模型定义 Patient, MedicationLog) ...

var db *gorm.DB

// AddMedicationRecordService 封装了我们的业务逻辑
func AddMedicationRecordService(patientID uint, medicationName string) error {
    // gorm.Transaction 会自动处理 commit 和 rollback
    err := db.Transaction(func(tx *gorm.DB) error {
        // 1. 更新患者表中的最后用药时间
        // 在事务中,必须使用 tx 对象,而不是全局的 db 对象
        result := tx.Model(&Patient{}).Where("id = ?", patientID).
            Update("last_medication_date", time.Now())
        
        if result.Error != nil {
            return result.Error // 返回错误,事务会自动回滚
        }
        if result.RowsAffected == 0 {
            return errors.New("patient not found") // 同样会回滚
        }

        // 2. 在用药记录表中插入一条新记录
        log := MedicationLog{PatientID: patientID, MedicationName: medicationName}
        if err := tx.Create(&log).Error; err != nil {
            return err // 返回错误,事务会自动回滚
        }

        // 如果函数正常返回 nil,事务会自动提交
        return nil
    })
    
    return err
}

func main() {
    // ... (连接数据库,初始化 Gin) ...

    r := gin.Default()
    r.POST("/medication", func(c *gin.Context) {
        // ... (参数绑定和验证) ...
        var requestBody struct {
            PatientID      uint   `json:"patientId" binding:"required"`
            MedicationName string `json:"medicationName" binding:"required"`
        }

        if err := c.ShouldBindJSON(&requestBody); err != nil {
            c.JSON(400, gin.H{"error": "Invalid request"})
            return
        }

        if err := AddMedicationRecordService(requestBody.PatientID, requestBody.MedicationName); err != nil {
            c.JSON(500, gin.H{"error": "Failed to add record", "details": err.Error()})
            return
        }
        
        c.JSON(200, gin.H{"status": "success"})
    })

    r.Run(":8080")
}

绝对不能犯的错误

  1. Transaction 闭包里使用全局的 db 对象 :事务是基于连接的。tx 代表了这个事务专用的数据库连接。如果你在里面用了全局的 db,那这个操作就不在事务的保护范围内了!
  2. 吞掉错误 :在闭包里,一旦发生错误,必须 return errGORM 会捕获这个返回的 error 并触发 ROLLBACK。如果你把错误处理掉了然后 return nil,事务就会被错误地 COMMIT

3.2 性能压测与慢查询治理

系统上线前,必须进行充分的性能压测。我们会使用 wrkJMeter 等工具模拟成百上千的用户同时访问,特别是对核心接口,比如患者登录、查询病历、提交数据等。

压测过程中,最常发现的问题就是慢查询

如何发现慢查询? 在 MySQL 配置中开启慢查询日志:

ini 复制代码
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1  # 执行时间超过1秒的查询被认为是慢查询
log_queries_not_using_indexes = 1 # 记录没有使用索引的查询

如何分析和优化? 拿到慢查询 SQL 后,使用 EXPLAIN 命令分析它的执行计划。

例如,我们发现一个查询患者的接口很慢: SELECT * FROM patients WHERE last_name = '张' AND date_of_birth = '1980-01-15';

EXPLAIN 之后,发现在 type 这一列显示的是 ALL,意味着它在进行全表扫描 !即使 patients 表有几百万数据,它也会一行一行地去比对。

优化方案 :创建一个复合索引。 CREATE INDEX idx_lastname_dob ON patients (last_name, date_of_birth);

再次 EXPLAINtype 变成了 refrows(预估扫描行数)从几百万降到了个位数。接口性能可能从几秒提升到几毫秒。

索引的艺术

  • 不是越多越好:索引会占用磁盘空间,并且在写入数据时(INSERT, UPDATE)会增加额外的开销来维护索引。
  • 复合索引遵循最左前缀原则idx(a, b, c) 这个索引,相当于同时拥有了 (a), (a, b), (a, b, c) 三个索引的效果。但如果你的查询条件是 WHERE b = ?,这个索引就用不上。所以,字段的顺序很重要。

第四章:百万 QPS 下的终极考验:缓存与限流

对于一些大型的互联网医院平台或者 AI 辅助诊断系统,瞬时流量可能非常高。这时候光靠优化数据库是不够的,还需要在数据库前面加上更多的保护层。

4.1 Redis 缓存:数据库的"挡箭牌"

场景 :查询医院科室列表、药品目录、医生排班等。这些数据读多写少,变化不频繁,是使用缓存的完美场景。

基本流程(Cache-Aside Pattern)

  1. 应用先请求 Redis。
  2. 如果 Redis 里有数据(缓存命中),直接返回。
  3. 如果 Redis 里没有(缓存未命中): a. 应用请求 MySQL 数据库。 b. 从 MySQL拿到数据后,存入 Redis(并设置一个过期时间,比如 5 分钟)。 c. 返回数据给客户端。

缓存三大问题与我们的应对

  • 缓存穿透 :查询一个数据库里根本不存在 的数据。比如用一个恶意的、不存在的 patient_id 来频繁攻击我们的查询接口。每次请求都会穿过 Redis 打到数据库上。

    • 对策:对查询结果为空的情况也进行缓存,但设置一个很短的过期时间,比如 30 秒。或者使用布隆过滤器。
  • 缓存击穿 :一个热点数据(比如某位大 V 医生的简介页面)的缓存突然过期了。在这一瞬间,成千上万的请求会同时涌入,直接打向数据库,导致数据库压力剧增。

    • 对策 :使用互斥锁。当缓存失效时,只让第一个请求去查询数据库并重建缓存,其他请求稍等片刻,然后直接从重建好的缓存中获取数据。
  • 缓存雪崩 :大量的缓存 key 在同一时间集体失效,导致所有请求都打到数据库。

    • 对策 :在设置缓存过期时间时,增加一个随机值 ,比如 5分钟 + random(1, 300)秒,把过期时间打散。

4.2 限流与熔断:最后的防线

即使有缓存,如果遇到恶意的流量攻击或突发的业务高峰,系统还是有可能被冲垮。

  • 限流 :我们的"智能开放平台"会对外提供 API。我们必须对每个合作方进行限流,比如"每秒最多 100 次请求"。这可以用 Go 的标准库 golang.org/x/time/rate(基于令牌桶算法)轻松实现。在 go-zero 中,可以直接在 API 文件里配置。

    api 复制代码
    // 在 a.api 文件中
    @server(
        // ...
        jwt: Auth
        middleware: RateLimit // 引用中间件
    )

    然后在 config.yaml 中配置具体的限流参数。

  • 熔断 :我们的系统依赖很多第三方服务,比如短信验证码、药企的药品信息接口。如果某个第三方服务突然变慢或者宕机,而我们的系统还在不停地调用它,就会耗尽自身的线程和连接资源,导致整个系统被拖垮,这就是雪崩效应

    • 对策 :引入熔断器 。当对某个服务的调用失败率超过一个阈值(比如 1 分钟内失败了 50%),熔断器就会"跳闸"(状态变为 Open),后续的请求不再真正发出,而是直接返回一个错误(快速失败)。过一段时间后,熔断器会进入"半开"(Half-Open)状态,尝试放一两个请求过去,如果成功,就恢复正常(Closed);如果还是失败,就继续保持"打开"状态。go-zero 内置了基于 Google SRE 理论的自适应熔断器,非常强大。

总结

构建一个高性能、高可用的数据库架构是一个系统工程,它不仅仅是 DBA 的工作,更是我们每一个 Go 开发架构师需要深入理解和掌握的核心能力。

回顾一下我们走过的路:

  1. 选型:基于业务场景,在关系型和 NoSQL 之间做出明智选择。
  2. 架构:通过主从复制、读写分离、分库分表,搭建可扩展、高可用的集群。
  3. 编码:在代码层面,正确处理事务、优化 SQL、精细化配置连接池。
  4. 防护:利用缓存、限流、熔断等手段,为系统穿上层层"铠甲"。

在医疗 IT 领域,技术方案的选择总是趋于保守和稳健,但技术的演进又要求我们不断拥抱变化。希望我今天的分享,能帮助大家在理论和实践之间找到一个平衡点,构建出更加稳定、可靠的系统。

相关推荐
我不是混子3 小时前
MySQL中如何查看数据库容量大小、表容量大小、索引容量大小?
后端·mysql
似水流年流不尽思念3 小时前
Redis 如何配置 Key 的过期时间?它的实现原理?
后端
yunxi_053 小时前
RAG 项目中的向量化实战:让模型精准检索上传文档
后端·ai编程
程序员小富3 小时前
字节二面挂!面试官: Redis 内存淘汰策略 LRU 和传统 LRU 差异,我答懵了
后端
万粉变现经纪人3 小时前
如何解决 pip install 安装报错 ModuleNotFoundError: No module named ‘django’ 问题
ide·后端·python·django·beautifulsoup·pandas·pip
勇哥java实战分享3 小时前
聊聊五种 Redis 部署模式
后端
shark_chili3 小时前
Java开发者必知的零拷贝技术:RocketMQ/Kafka性能优化的核心原理
后端
这里有鱼汤3 小时前
如何用Python找到股票的支撑位和压力位?——斐波那契
后端·python
程序员爱钓鱼4 小时前
Go语言100个实战案例-进阶与部署篇:使用Go打包生成可执行文件
后端·google·go