集群开发学习(一)(安装GO和MySQL,K8S基础概念)

完成gin小任务

参考文档:
https://www.kancloud.cn/jiajunxi/ginweb100/1801414
https://github.com/hanjialeOK/going

最终代码地址:https://github.com/qinliangql/gin_mini_test.git

学习

1.安装go

bash 复制代码
wget https://dl.google.com/go/go1.20.2.linux-amd64.tar.gz
tar -C /usr/local -zxf go1.20.2.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
// All Tools installed by go are in $GOPATH.
export GOPATH=$PATH:/usr/local/go
export PATH=$PATH:$GOPATH/bin
// https://goproxy.cn is faster than aliyun.
export GOPROXY=https://goproxy.cn

验证安装是否成功

bash 复制代码
go version

hello world

bash 复制代码
mkdir hello && cd /hello
go mod init example/hello

创建一个hello.go

go 复制代码
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

在hello下运行

bash 复制代码
go run hello.go

做一个映射出来的

安装

bash 复制代码
go get github.com/gin-gonic/gin

我映射了端口8001

bash 复制代码
package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

func main() {
    router := gin.Default()
    router.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Hello World")
    })
    router.Run(":8001") 
}
bash 复制代码
go run hello.go


2.安装MySQL

bash 复制代码
// For ubuntu 18.04
sudo apt install mysql-server
// start mysql
service mysql start

Change the password for mysql

bash 复制代码
// login
mysql -u root
// change the password for mysql
use mysql;
update user set authentication_string='' where user='root';
// newpassword: 123456
alter user 'root'@'localhost' identified with mysql_native_password by '123456';
// exit
quit
// restart mysql
service mysql restart

Create database and table.

bash 复制代码
mysql -u root -p
create database gobase;
use gobase;
CREATE TABLE `account` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(20) NOT NULL,
    `email` varchar(50) NOT NULL,
    `password` varchar(30) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

查看数据库信息(先quit再重新打开mysql)

bash 复制代码
show databases;

查看table

bash 复制代码
use gobase;
\\ List Table info
show tables;
\\ Look structure of table
desc account;

Task

1.使用Gin创建一个API,允许用户通过提供姓名,电子邮件和密码来创建新账户。该API应验证所有字段是否存在以及电子邮件格式是否正确。

安装依赖

go 复制代码
go get github.com/gin-gonic/gin
go get gorm.io/driver/mysql
go get gorm.io/gorm

代码:

go 复制代码
package main

import (
	"fmt"
    "net/http"
	"net/mail"

    "github.com/gin-gonic/gin"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
    // "regexp"
)

type UserInfo struct {
	ID       int    `form:"id"`
	Name     string `form:"name" binding:"required"`
	Email    string `form:"email" binding:"required,email"`
	Password string `form:"password" binding:"required,min=6"`
}



func main() {
    // 创建一个默认的 Gin 路由器
    r := gin.Default()

    // 创建一个路由,允许用户提交新账户信息
    r.POST("/api/register", create)

    // 启动 Gin 服务器,默认监听在 8001 端口
    r.Run(":8001")
}

// isValidEmail 检查电子邮件格式是否正确
func isValidEmail(email string) bool {
    _, err := mail.ParseAddress(email)
	// 如果解析正确,err会变为nil
	return err == nil
}

func create(c *gin.Context) {
	// 绑定请求体到 UserInfo 结构体类型的form 
	var form UserInfo
	if err := c.ShouldBindJSON(&form); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 检查电子邮件格式是否正确
	if !isValidEmail(form.Email) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email format"})
		return
	}

	// 打开数据库,3306是MySQL默认端口
	dsn := "root:123456@tcp(127.0.0.1:3306)/gobase"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// check whether the name exists.
	var user UserInfo
	db.Table("account").Where("name = ?", form.Name).First(&user)
	if (user != UserInfo{}) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "name exists!"})
		return
	}

	// check whether the email has been used.
	db.Table("account").Where("email = ?", form.Email).First(&user)
	if (user != UserInfo{}) {
		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("email %s has been used!", form.Email)})
		return
	}

	// 保存用户到数据库(:= 对于没有声明的变量会自动声明)
	user = UserInfo{Name: form.Name, Email: form.Email, Password: form.Password}
	result := db.Table("account").Create(&user)
	if result.Error != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": result.Error})
		return
	}
	
	
	// 这里只是返回成功消息
	c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Account %s created successfully!",user.Name)})
}

测试代码:

-X 指定发送的请求类型为 POST 请求

-H 设置请求头,指定请求的内容类型为 JSON 格式

bash 复制代码
curl -X POST -H "Content-Type: application/json" -d '{"name":"QL","email":"qinl@example.com","password":"password"}' http://localhost:8001/api/register

查看数据库当前情况

bash 复制代码
service mysql start
mysql -u root -p
show databases;
use gobase;
show tables;
SELECT * FROM account;
quit

2.使用Gin实现一个API,允许用户通过提供电子邮件和密码来检索其帐户信息。该API应验证电子邮件和密码是否正确,然后返回用户信息。

新加代码:

go 复制代码
// 新增路由,允许用户通过提供电子邮件和密码来检索其帐户信息,相当于登陆
r.POST("/api/login", login)

/*
// 处理登录请求的函数 (这种必须要求输入完整的UserInfo类型信息才行,所以放弃)
func login(c *gin.Context) {
	// 绑定请求体到 UserInfo 结构体类型的 form
	var form UserInfo
	if err := c.ShouldBindJSON(&form); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 打开数据库
	dsn := "root:123456@tcp(127.0.0.1:3306)/gobase"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 查询数据库中是否存在匹配的用户记录
	var user UserInfo
	result := db.Table("account").Where("email = ? AND password = ?", form.Email, form.Password).First(&user)
	if result.Error != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email or password"})
		return
	}

	// 返回匹配的用户信息
	c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Welcom user %s !", user.Name)})
}
*/

func login(c *gin.Context) {
	// 绑定请求体到 UserInfo 结构体类型的 form
	email := c.Query("email")
	password := c.Query("password")

	// 打开数据库
	dsn := "root:123456@tcp(127.0.0.1:3306)/gobase"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 查询数据库中是否存在匹配的用户记录
	var user UserInfo
	result := db.Table("account").Where("email = ? AND password = ?", email, password).First(&user)
	if result.Error != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid email:%s or password:%s",email,password)})
		return
	}

	// 返回匹配的用户信息
	c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Welcom user %s !", user.Name)})
}

测试代码:

bash 复制代码
# 正确案例
curl -X POST "http://localhost:8001/api/login?email=qinl@example.com&password=password"
# 错误案例
curl -X POST "http://localhost:8001/api/login?email=qinl@example.com&password=654321"

3.添加一个API,使用Gin允许用户更新其帐户信息。该API应验证用户是否已通过身份验证并且所有字段是否存在,然后将用户信息更新到数据库中。

安装依赖

bash 复制代码
go get github.com/golang-jwt/jwt/v5

更新的代码:

go 复制代码
package main

import (
	"fmt"
    "net/http"
	"net/mail"
	"time"

    "github.com/gin-gonic/gin"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
    "github.com/golang-jwt/jwt/v5"
)

// 创建一个 JWT(JSON Web Token)字符串。
// JWT 是一种用于在网络应用之间安全传递声明的开放标准。

var jwtKey = []byte("my_secret_key")	// 用于签名和验证 JWT 的密钥

// input: email || output:string,error
func createToken(email string) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"email": email,
		"exp":   time.Now().Add(time.Minute * 10).Unix(), // 有效时长10分钟
	})

	return token.SignedString(jwtKey)
}

func validateToken(tokenString string) (*jwt.Token, error) {
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return jwtKey, nil
	})

	if err != nil {
		return nil, err
	}

	if _, ok := token.Claims.(jwt.MapClaims); !ok || !token.Valid {
		return nil, fmt.Errorf("bad token.")
	}

	return token, nil
}

func validateCookie(c *gin.Context) (*jwt.Token, error) {
	tokenString, err := c.Cookie("gin_cookie")
	if err != nil {
		return nil, err
	}
	token, err := validateToken(tokenString)
	if err != nil {
		return nil, err
	}
	return token, nil
}

func main() {
    // 创建一个默认的 Gin 路由器
    r := gin.Default()

    // 创建一个路由,允许用户提交新账户信息
    r.POST("/api/register", create)  // 新增路由,允许用户通过提供电子邮件和密码来检索其帐户信息,相当于登陆
	r.POST("/api/login", login)
	r.POST("/api/update", update) // 新增的路由,用于更新用户信息

    // 启动 Gin 服务器,默认监听在 8001 端口
    r.Run(":8001")
}


func login(c *gin.Context) {
	// ...

	// Create the JWT string.
	token, err := createToken(user.Email)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create token"})
		return
	}
	// tell the user it's ok. 3600 是最大存活时间1h
	c.SetCookie("gin_cookie", token, 3600, "/", "", false, true)

	// 返回匹配的用户信息
	c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Welcom user %s !", user.Name)})
}

func update(c *gin.Context) {
    token, err := validateCookie(c)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	// get email from cookie
	email := token.Claims.(jwt.MapClaims)["email"]
	// new password
	password := c.Query("password")
	name := c.Query("name")
	// update
	dsn := "root:123456@tcp(127.0.0.1:3306)/gobase"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	// retrieve the user by email.
	var user UserInfo
	db.Table("account").Where("email = ?", email).First(&user)
	if (user == UserInfo{}) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "this email has not been registered."})
		return
	}

    // 更新用户信息
	if name != "" {
		user.Name = name
	}
	if password != "" {
		user.Password = password
	}

    // 保存更新后的用户信息到数据库
    if err := db.Table("account").Save(&user).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"})
        return
    }

    // 返回更新成功的消息
    c.JSON(http.StatusOK, gin.H{"message": "User information updated successfully"})
}

测试代码,用 -i 可以更细致的看到信息,目前还是在curl里面传递cookie的

bash 复制代码
curl -i -X POST "http://localhost:8001/api/login?email=qinl@example.com&password=password"
curl -i -X POST "http://localhost:8001/api/update?password=newpassword" --cookie "gin_cookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InFpbmxAZXhhbXBsZS5jb20iLCJleHAiOjE3MTI4MDU0NTl9.VTLihEYTy-kWZjZhLrwpwIMuPgimpm-DVnYwvhoKnNs"

修改后的数据库

4. 使用Gin实现一个API,允许用户通过提供电子邮件和密码来删除其帐户。该API应验证电子邮件和密码是否正确,然后从数据库中删除用户帐户。

新加代码:

go 复制代码
r.POST("/api/delete",delete)	// 删除用户信息

func delete(c *gin.Context) {
	email := c.Query("email")
	password := c.Query("password")

	// 打开数据库
    dsn := "root:123456@tcp(127.0.0.1:3306)/gobase"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // 查询数据库中是否存在匹配的用户记录
    var user UserInfo
    result := db.Table("account").Where("email = ? AND password = ?", email, password).First(&user)
    if result.Error != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid email or password"})
        return
    }

    // 删除用户账户
    if err := db.Table("account").Delete(&user).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete account"})
        return
    }

    // 返回删除成功的消息
    c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Account %s deleted successfully",user.Name)})
}

测试代码:

bash 复制代码
curl -X POST "http://localhost:8001/api/delete?email=qinl@example.com&password=newpassword"

5. 添加一个API,使用Gin允许用户检索数据库中所有帐户的列表。

新加代码:

go 复制代码
func show_all(c *gin.Context) {
	if _, err := validateCookie(c); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 打开数据库
    dsn := "root:123456@tcp(127.0.0.1:3306)/gobase"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    // 查询数据库,获取所有帐户列表
    var accountNames []string
    if err := db.Table("account").Pluck("name", &accountNames).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve accounts"})
        return
    }

    // 返回帐户列表给客户端
    c.JSON(http.StatusOK, accountNames)
}

测试代码:

bash 复制代码
# 先加两个用户
curl -X POST -H "Content-Type: application/json" -d '{"name":"QL","email":"qinl@example.com","password":"password"}' http://localhost:8001/api/register
curl -X POST -H "Content-Type: application/json" -d '{"name":"ZS","email":"zs@example.com","password":"password"}' http://localhost:8001/api/register
# 登录一个用户获取token
curl -i -X POST "http://localhost:8001/api/login?email=qinl@example.com&password=password"
# 用token,获取用户信息
curl -X POST "http://localhost:8001/api/show_all" --cookie "gin_cookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InFpbmxAZXhhbXBsZS5jb20iLCJleHAiOjE3MTI4MDc0OTB9.hoB7TY0TorAlmdtm-jyqb8U5nSAehmP7d2n5EjSE1r8"

["QL","ZS"]

6. 调研了解目前主流的登录鉴权方式,选择一个在你的项目中进行实现。

主流鉴权方式:

  • 基于Token的认证(Token-based Authentication):用户在登录成功后会收到一个令牌(Token),之后每次请求都需要在请求头中携带这个令牌进行验证。常见的Token包括JWT(JSON Web Token)和OAuth2 Token。

  • OAuth2认证(OAuth2 Authentication):OAuth2是一种开放标准,允许用户授权第三方应用访问其资源。用户可以通过第三方认证提供者(如Google、Facebook等)进行登录,然后授权给应用访问其资源的权限。

  • 基于Session的认证(Session-based Authentication):用户在登录成功后,服务器会为其创建一个会话(Session),并将会话ID存储在Cookie中,用户在后续的请求中都会携带这个会话ID。服务器通过会话ID来验证用户的身份和权限。

原本的是基于token做的,这里改为基于session做

安装依赖:

bash 复制代码
go get github.com/gorilla/sessions

新代码:

go 复制代码
// 定义一个全局的会话存储对象
var store = sessions.NewCookieStore([]byte("my-secret"))

// 创建会话并将用户ID存储在会话中
session, _ := store.Get(c.Request, "session-name")
session.Values["userID"] = user.ID
session.Save(c.Request, c.Writer)

// 获取会话中的用户ID
   session, _ := store.Get(c.Request, "session-name")
   _, ok := session.Values["userID"].(int)
   if !ok {
       c.JSON(http.StatusBadRequest, gin.H{"error": "User not authenticated"})
       return
   }

测试代码:

bash 复制代码
# 登录一个用户
curl -i -X POST "http://localhost:8001/api/login?email=qinl@example.com&password=password"
# 用token,获取用户信息
curl -X POST "http://localhost:8001/api/show_all" --cookie "session-name=MTcxMjgyNjI1NHxEdi1CQkFFQ180SUFBUkFCRUFBQUhmLUNBQUVHYzNSeWFXNW5EQWdBQm5WelpYSkpSQU5wYm5RRUFnQUV8hXB2Vl_OaBCvi71u6KLgrh96uhu-Irt-CAzSbxFBMeY="

K8S 概念理解

核心

  1. Namespace(命名空间)

    • Namespace是Kubernetes中用于将集群中的资源划分为不同逻辑组的一种机制。
    • 它允许用户在同一集群中创建多个虚拟集群。
    • 命名空间提供了一种在不同组织或项目之间隔离资源的方法,以及控制资源访问和使用的方法。
    • 默认情况下,Kubernetes提供了一些预定义的命名空间,如"default"用于用户创建的资源,以及"kube-system"用于Kubernetes系统组件。
  2. Pod(Pod)

    • Pod是Kubernetes中最小的部署和管理单位。
    • 它是一个或多个相关容器的组合,共享网络和存储资源,并在同一宿主机上运行。
    • Pod可以包含单个容器(单容器Pod)或多个容器(多容器Pod),这些容器可以共享网络命名空间和存储卷。
    • Pod可以用来运行应用程序、批处理作业或其他需要共享资源的任务。
  3. Deployment(部署)

    • Deployment是Kubernetes中用于管理Pod副本的控制器。
    • 它定义了应用程序或服务的期望状态,并确保集群中运行的Pod数量符合该状态。
    • Deployment允许用户轻松地创建、更新和扩展Pod副本,以及执行滚动升级和回滚操作。
    • 通过Deployment,用户可以指定应用程序的副本数量、容器镜像和更新策略等参数。
  4. Service(服务)

    • Service是Kubernetes中用于将Pod公开为网络服务的抽象。
    • 它为Pod提供了一个稳定的网络端点,允许其他应用程序或服务通过网络访问它们。
    • Service可以通过标签选择器将一组Pod组合成一个逻辑集,并将流量负载均衡到这些Pod之间。
    • Kubernetes支持多种类型的Service,包括ClusterIP、NodePort、LoadBalancer和ExternalName等。

综上所述,Namespace用于组织和隔离资源,Pod是最小的部署单元,Deployment用于管理Pod副本,Service提供网络访问入口,使应用程序或服务可以在集群内部或外部可达。这些概念共同构成了Kubernetes的核心功能,为容器化应用程序的部署、管理和扩展提供了强大的支持。

了解

  1. Node(节点)

    • Node是Kubernetes集群中的工作节点,用于运行应用程序和服务的实际工作负载。
    • 每个Node都是一个单独的物理机器或虚拟机,它们一起组成了Kubernetes集群。
    • Node由Kubernetes Master进行管理和监控,并承载了容器运行时引擎(如Docker或containerd)来运行Pod。
  2. Cluster(集群)

    • Cluster是由一组Node组成的Kubernetes环境,用于运行和管理容器化应用程序和服务。
    • 它包括一个Master节点和一组工作节点,所有这些节点共同协作以提供高可用性、自动扩展和自我修复的特性。
  3. Resource Quota(资源配额)

    • Resource Quota是Kubernetes中的一种资源管理机制,用于限制Namespace中资源的使用量。
    • 它允许集群管理员为每个Namespace设置配额,以控制Pod、CPU、内存、存储等资源的使用量,防止资源耗尽和过度使用。
  4. ConfigMap(配置映射)

    • ConfigMap是Kubernetes中用于存储配置数据的API对象。
    • 它允许将配置数据(如环境变量、配置文件等)从应用程序中解耦,并将其存储在集群中的ConfigMap对象中。
    • ConfigMap可以在Pod中作为卷或环境变量挂载,使应用程序可以动态地读取配置数据。
  5. Role(角色)RoleBinding(角色绑定)

    • Role和RoleBinding是Kubernetes中用于授权和访问控制的两个重要对象。
    • Role定义了一组权限规则,允许用户或服务账户对指定的资源执行特定的操作。
    • RoleBinding将Role绑定到用户、组或ServiceAccount,并为其分配相应的权限,以控制对资源的访问。
  6. Service Account(服务账户)

    • Service Account是Kubernetes中用于身份验证和授权的一种机制。
    • 它为Pod中运行的应用程序提供了一个身份标识,并允许它们与Kubernetes API进行交互。
    • 每个Namespace都有一个默认的Service Account,可以为Pod分配具有不同权限的Service Account。
  7. PersistentVolume(持久化卷)PersistentVolumeClaim(持久化卷声明)

    • PersistentVolume(PV)是Kubernetes中用于存储数据的持久化存储资源。
    • PersistentVolumeClaim(PVC)是用户向Kubernetes请求PV的一种机制,它定义了用户对存储资源的需求和要求。
    • PVC允许用户独立于底层存储实现和配置,并根据需要动态地请求和释放PV。

配置.kube文件

bash 复制代码
mkdir ~/.kube
# 需要管理员权限才能复制配置文件
sudo cp /etc/kubernetes/admin.conf ~/.kube/config
# 把文件权限改为自己,xxx就是自己的用户名
sudo chown -R qinl:qinl ~/.kube/
# 修改为自己的命名空间,vim编辑~/.kube/config文件,在context一栏加上一行,然后保存并退出即可:
nano ~/.kube/config
- context:
    cluster: kubernetes
    user: kubernetes-admin
    namespace: xxx # 这里加上自己的命名空间xxx,一般用自己的用户名
  name: kubernetes-admin@kubernetes
# 创建自己的命名空间,和上面的xxx对应
kubectl create ns qinl

测试

bash 复制代码
kubectl get po
相关推荐
夜泉_ly32 分钟前
MySQL -安装与初识
数据库·mysql
StickToForever3 小时前
第4章 信息系统架构(五)
经验分享·笔记·学习·职场和发展
月光水岸New4 小时前
Ubuntu 中建的mysql数据库使用Navicat for MySQL连接不上
数据库·mysql·ubuntu
我爱松子鱼4 小时前
mysql之规则优化器RBO
数据库·mysql
闲猫4 小时前
go orm GORM
开发语言·后端·golang
丁卯4044 小时前
Go语言中使用viper绑定结构体和yaml文件信息时,标签的使用
服务器·后端·golang
人间打气筒(Ada)6 小时前
MySQL主从架构
服务器·数据库·mysql
leegong231116 小时前
学习PostgreSQL专家认证
数据库·学习·postgresql
和道一文字yyds6 小时前
MySQL 中的索引数量是否越多越好?为什么?如何使用 MySQL 的 EXPLAIN 语句进行查询分析?MySQL 中如何进行 SQL 调优?
数据库·sql·mysql
Moonnnn.7 小时前
51单片机学习——动态数码管显示
笔记·嵌入式硬件·学习·51单片机