golang web学习随便记4-内存、文件、数据库

这篇具有很好参考价值的文章主要介绍了golang web学习随便记4-内存、文件、数据库。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

我们来开始学习如何存储数据。书中有一点不错,就是并不是一上来就告诉你存储数据使用数据库,因为不同的数据存储适合不同的手段。

用内存存储数据

先来看在内存中存储数据:下面的例子用结构体方式在内存存放数据,然后利用两个map来表示“索引”,键值对中的值是指向内存中结构体实例的指针。以下main函数的主要步骤是,用make初始化两个索引用的map,生成数据存放到结构体实例中,调用store创建索引,验证两种索引方式

package main

import "fmt"

type Post struct {
	Id      int
	Content string
	Author  string
}

var PostById map[int]*Post
var PostsByAuthor map[string][]*Post

func store(post Post) {
	PostById[post.Id] = &post
	PostsByAuthor[post.Author] = append(PostsByAuthor[post.Author], &post)
}

func main() {
	PostById = make(map[int]*Post)
	PostsByAuthor = make(map[string][]*Post)

	post1 := Post{Id: 1, Content: "你好, Golang", Author: "张三"}
	post2 := Post{Id: 2, Content: "你好, C++", Author: "李四"}
	post3 := Post{Id: 3, Content: "你好, Java", Author: "王五"}
	post4 := Post{Id: 4, Content: "你好, C", Author: "张三"}
	store(post1)
	store(post2)
	store(post3)
	store(post4)

	fmt.Println(PostById[1])
	fmt.Println(PostById[3])

	for _, post := range PostsByAuthor["张三"] {
		fmt.Println(post)
	}
	for _, post := range PostsByAuthor["李四"] {
		fmt.Println(post)
	}

}

go  run  . 运行后输出如下:

sjg@sjg-PC:~/go/src/memory_store$ go run .
&{1 你好, Golang 张三}
&{3 你好, Java 王五}
&{1 你好, Golang 张三}
&{4 你好, C 张三}
&{2 你好, C++ 李四}

正如书中所说,这个例子非常简单,但是,在实际应用中,对于需要在内存中缓存数据来提升性能的场合,并非都要用redis那样厚重的外部内存数据库,或许我们简单构建一下内存数据存储就能很好解决问题。

用文件存储数据

读写文本文件

用Golang读写字节数组数据并不复杂,而且和PHP类似,既可以一次性直接将数据写入文件或者从文件读取数据,也可以先创建或者打开文件,再读写数据

package main

import (
	"fmt"
	"io/ioutil"
	"os"
)

func main() {
	data := []byte("欢迎使用 Golang 编程语言\n")
	err := ioutil.WriteFile("datafile1", data, 0644) // 直接写入字节数组数据到文件
	if err != nil {
		panic(err)
	}
	readBuf1, _ := ioutil.ReadFile("datafile1") // 直接读取文件数据到缓冲字节数组
	fmt.Print(string(readBuf1))

	file1, _ := os.Create("datafile2")
	defer file1.Close()

	byteCnt, _ := file1.Write(data) // 创建文件再写入字节数组数据
	fmt.Printf("写入 %d 字节到文件 datafile2\n", byteCnt)

	file2, _ := os.Open("datafile2")
	defer file2.Close()

	readBuf2 := make([]byte, len(data))
	byteCnt, _ = file2.Read(readBuf2) // 打开文件再读取数据到缓冲字节数组
	fmt.Printf("从文件 datafile2 读取 %d 字节\n", byteCnt)
	fmt.Println(string(readBuf2))
}

运行结果如下(注意观察在项目目录下生成的数据文件datafile1和datafile2)

sjg@sjg-PC:~/go/src/file_store1$ go run .
欢迎使用 Golang 编程语言
写入 33 字节到文件 datafile2
从文件 datafile2 读取 33 字节
欢迎使用 Golang 编程语言

读写CSV

在各种应用中,CSV是非常常用的数据格式,golang标准库提供了专门的读写csv的包encoding/csv。下面的例子演示了csv文件的写入和读取:

package main

import (
	"encoding/csv"
	"fmt"
	"os"
	"strconv"
)

type Post struct {
	Id      int
	Content string
	Author  string
}

func main() {
	csv_file, err := os.Create("posts.csv") // 创建 csv 文件
	if err != nil {
		panic(err)
	}
	defer csv_file.Close()

	data_posts := []Post{
		{Id: 1, Content: "你好, Golang", Author: "张三"},
		{Id: 2, Content: "你好, C++", Author: "李四"},
		{Id: 3, Content: "你好, Java", Author: "王五"},
		{Id: 4, Content: "你好, C", Author: "张三"},
	}

	writer := csv.NewWriter(csv_file) // 创建写入器(Writer型对象),参数为目标写入文件
	for _, post := range data_posts {
		line := []string{strconv.Itoa(post.Id), post.Content, post.Author}
		err := writer.Write(line) // 用写入器写入字符串数组(每个元素对应一个字段)
		if err != nil {
			panic(err)
		}
	}
	writer.Flush() // 写入器是带缓冲的,需要刷写确保全部写完

	file, err := os.Open("posts.csv") // 打开 csv 文件
	if err != nil {
		panic(err)
	}
	defer file.Close()

	reader := csv.NewReader(file)   // 创建读取器(Reader型对象),参数为目标读取文件
	reader.FieldsPerRecord = -1     // 正的指定字段数,0按第一个记录确定字段数,负的变长字段数
	record, err := reader.ReadAll() // 一次性读取所有记录(返回二维字符串数组)
	if err != nil {
		panic(err)
	}

	var posts []Post
	for _, item := range record { // 二维数组记录保存到 posts
		id, _ := strconv.ParseInt(item[0], 0, 0) // 注意:strconv.ParseInt返回int64
		post := Post{Id: int(id), Content: item[1], Author: item[2]}
		posts = append(posts, post)
	}
	fmt.Println(posts[1].Id)
	fmt.Println(posts[1].Content)
	fmt.Println(posts[1].Author)
}

上述代码中,对于csv文件的写入,是一行行写入的,对于读取,则是一次性读取到二维数组中,然后解析该数组还原结构体对象的。对于需要读取的数据量非常大的情况,csv.Reader对象是提供了Read()方法来一行行读取的。同时,为了提高性能,csv.Reader对象有一个ReuseRecord字段来控制是否复用返回的slice(默认每次调用都会分配新的内存)。csv.Reader对象还有其他一些字段来控制是否去除前导空格等。

编解码方式读写文件

某种程度上,前述csv例子我们是手动对写入的数据和读取的数据进行编码和解码的,encoding/gob包提供了更通用的编码和解码方式,而且它不限于文本文件,可以用于二进制文件。

下面的例子演示了gob包中编码器和解码器的使用:

package main

import (
	"bytes"
	"encoding/gob"
	"fmt"
	"io/ioutil"
)

type Post struct {
	Id      int
	Content string
	Author  string
}

func store(data interface{}, filename string) {
	buffer := new(bytes.Buffer)       // 用new初始化编码器所需的缓冲
	encoder := gob.NewEncoder(buffer) // 创建编码器
	err := encoder.Encode(data)       // 用编码器编码数据,数据为接口类型
	if err != nil {
		panic(err)
	}
	err = ioutil.WriteFile(filename, buffer.Bytes(), 0600) // 写入缓冲中编码好的数据
	if err != nil {
		panic(err)
	}
}

func load(data interface{}, filename string) {
	raw, err := ioutil.ReadFile(filename) // 一次性读取文件中所有数据
	if err != nil {
		panic(err)
	}
	buffer := bytes.NewBuffer(raw)    // 将数据放入缓冲
	decoder := gob.NewDecoder(buffer) // 创建解码器
	err = decoder.Decode(data)        // 用解码器解码数据,数据为接口类型
	if err != nil {
		panic(err)
	}
}

func main() {
	post := Post{Id: 1, Content: "你好, Golang", Author: "张三"}
	store(post, "datafile")         // 编码存放到文件,数据是“读”
	var post_read Post
	load(&post_read, "datafile")    // 解码存放到结构体,数据是“写”
	fmt.Println(post_read)
}

上面的代码表明:1、创建编码器和解码器,都需要一个buffer,编码器需要new初始化的buffer,解码器需要放入了原始字节切片数据的buffer(使用bytes.NewBuffer(..)函数完成)。2、上面的代码store(post, "datafile")改成store(&post, "datafile")结果不变,而且似乎传递地址更好一点,可以避免结构体拷贝。3、调用编码器的Encode(..)方法或者调用解码器的Decode(..)方法,都需要传入空接口类型(interface{})的数据data。实际调用方传入参数时,对于编码既可以传值,也可以传地址,因为编码时data是“读”状态;对于解码只能传地址,因为解码时data是“写”状态。这个道理和C语言scanf函数传地址,printf传值是一样的——只是golang空接口类型具有动态类型和动态值,从而“读”时既可以是值形式,也可以地址形式,因为是空接口,内部用反射机制来获得运行时类型。

关于结构体、接口和空接口,可以参考 golang学习随便记4-类型:map、结构体_sjg20010414的博客-CSDN博客

golang学习随便记8-接口_sjg20010414的博客-CSDN博客

golang Interface_golang interface{}_jenrain的博客-CSDN博客

用数据库存储数据

我们终于来到用数据库存储数据的了解。书上是使用 PostgreSQL 数据库,我打算把例子改写成使用 MariaDB数据库。

启动 mariadb 10.3数据库 (我是安装在docker中,用 ./start_mariadb.sh  bash即可启动并进入容器内终端),mysql -u root -p 登录,CREATE DATABASE gwp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; 创建数据库,GRANT ALL ON gwp.* TO 'gwp'@'%' IDENTIFIED BY 'dbpassword';  创建用户并授权。用下面的语句创建表

MariaDB [gwp]> CREATE TABLE post (
    -> id int NOT NULL AUTO_INCREMENT,
    -> content text,
    -> author varchar(255),
    -> PRIMARY KEY (id)
    -> );

添加驱动:GitHub - go-sql-driver/mysql: Go MySQL Driver is a MySQL driver for Go's (golang) database/sql package

sjg@sjg-PC:~/go/src/db_store1$ go get -u github.com/go-sql-driver/mysql
go: downloading github.com/go-sql-driver/mysql v1.7.1
go get: added github.com/go-sql-driver/mysql v1.7.1

改写书上这部分代码有一点点障碍,一个是书上使用$1、$2、$3等占位符报错,无论是查阅别人帖子还是golang官网例子代码(sql package - database/sql - Go Packages),占位符都是?。另一个是 mariadb 10.3 版本不够高,因此和mysql一样(不清楚高版本mysql情况)不支持插入时 RETURNING id值,我们需要额外工作来获取id (还好 mariadb/mysql  有 LAST_INSERT_ID() 函数):

package main

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

	_ "github.com/go-sql-driver/mysql"
)

type Post struct {
	Id      int
	Content string
	Author  string
}

var Db *sql.DB

func init() { // 此 init 不显式调用,自动隐式调用,实现初始化全局变量 Db
	var err error
	Db, err = sql.Open("mysql", "gwp:dbpassword@tcp(172.17.0.1:3306)/gwp?charset=utf8mb4,utf8")
	if err != nil {
		panic(err)
	}
}

func Posts(limit int) (posts []Post, err error) {
	rows, err := Db.Query("SELECT id, content, author FROM post LIMIT ?", limit) // Query(..) 预期返回多行结果集
	if err != nil {
		return
	}
	for rows.Next() { // 用循环遍历多行结果集
		post := Post{}
		err = rows.Scan(&post.Id, &post.Content, &post.Author) // Scan(..) 将结果集当前列值绑定到变量
		if err != nil {
			return
		}
		posts = append(posts, post) // 结果依次放入切片 posts
	}
	rows.Close() // 关闭结果集,清理内存
	return
}

func GetPost(id int) (post Post, err error) {
	post = Post{}
	err = Db.QueryRow("SELECT id, content, author FROM post WHERE id = ?", id). // QueryRow(..) 预期返回单行结果集
											Scan(&post.Id, &post.Content, &post.Author) // pgsql 可以 RETURNING id
	return
}

func (post *Post) Create() (err error) {
	sql := "INSERT INTO post (content, author) VALUES (?, ?)"
	stmt, err := Db.Prepare(sql) // 对于插入使用准备语句
	if err != nil {
		log.Fatal(err)
		return
	}
	defer stmt.Close()

	// err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)  // pgsql 可以一步设置 post.id
	if result, err := stmt.Exec(post.Content, post.Author); err != nil { // Exec(..)返回执行结果
		log.Fatal(err)
	} else {
		if last_insert_id, err := result.LastInsertId(); err != nil {
			log.Fatal(err)
		} else {
			post.Id = int(last_insert_id) // Mariadb/MySQL应该支持执行结果的 LastInsertId()
		}
	}
	return
}

func (post *Post) Update() (err error) {
	_, err = Db.Exec("UPDATE post SET content = ?, author = ? WHERE id = ?",
		post.Content, post.Author, post.Id)
	return
}

func (post *Post) Delete() (err error) {
	_, err = Db.Exec("DELETE FROM post WHERE id = ?", post.Id)
	return
}

func main() {
	post := Post{Content: "你好, C++", Author: "李四"}

	fmt.Println(post) // 插入记录前
	post.Create()
	fmt.Println(post) // 插入记录后

	post_read, _ := GetPost(post.Id)
	fmt.Println(post_read) // 获取刚刚插入的记录

	post_read.Content = "你好, Java"
	post_read.Author = "赵六"
	post_read.Update()

	posts, _ := Posts(5)
	fmt.Println(posts) // 获取所有记录

	post_read.Delete() // 删除记录
}

输出结果:

sjg@sjg-PC:~/go/src/db_store1$ go run .
{0 你好, C++ 李四}
{1 你好, C++ 李四}
{1 你好, C++ 李四}
[{1 你好, Java 赵六}]
sjg@sjg-PC:~/go/src/db_store1$ go run .
{0 你好, C++ 李四}
{2 你好, C++ 李四}
{2 你好, C++ 李四}
[{2 你好, Java 赵六}]

值得注意的是,Db变量的类型 *sql.DB,其实不是数据库连接的意义,它是一个数据库句柄,它代表包含0个或者多个数据库连接的连接池,因此,有些代码会命名为pool。

代码的头部使用了匿名导入,另外,代码中使用了包的init()函数用来初始化Db变量,可以参考  golang学习随便记14-包和工具_sjg20010414的博客-CSDN博客golang中的init初始化函数_golang init函数_六月的的博客-CSDN博客

Golang中有context的概念(context包),database/sql包支持context,可以实现超时控制、性能日志等功能,具体表现是很多函数有2个版本,例如DB类型有Prepare(query)方法和PrepareContext(ctx, query)方法。关于context可以参考 详解golang中的context - 知乎

要执行事务,并不复杂,大致步骤是:调用 Db.Begin() 返回事务对象tx,Tx类型具有和DB相似的一些方法,因此,原来用 Db 的地方换成tx,然后就是提交事务。我们把 Create() 改成事务方式,大致如下:

func (post *Post) Create() (err error) {
	sql := "INSERT INTO post (content, author) VALUES (?, ?)"
	tx, err := Db.Begin() // 启动事务 tx
	if err != nil {
		log.Fatal(err)
		return
	}
	defer tx.Rollback() // 事务被提交后此句无效

	// stmt, err := Db.Prepare(sql)
	stmt, err := tx.Prepare(sql) // Tx 类型 有和 DB 类型相似的一些方法
	if err != nil {
		log.Fatal(err)
		return
	}
	defer stmt.Close()

	// err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)  // pgsql 可以一步设置 post.id
	if result, err := stmt.Exec(post.Content, post.Author); err != nil {
		log.Fatal(err)
	} else {
		if last_insert_id, err := result.LastInsertId(); err != nil {
			log.Fatal(err)
		} else {
			post.Id = int(last_insert_id) // Mariadb/MySQL应该支持执行结果的 LastInsertId()
		}
	}

	if err := tx.Commit(); err != nil { // 提交事务 tx
		log.Fatal(err)
	}
	return
}

我们来看看带关联表时如何操作数据库。用下面的语句创建关联表 comment

MariaDB [gwp]> CREATE TABLE comment (
    -> id int NOT NULL AUTO_INCREMENT,
    -> content text,
    -> author varchar(255),
    -> post_id int,
    -> PRIMARY KEY (id),
    -> FOREIGN KEY (post_id) REFERENCES post(id)
    -> );

新建一个项目 db_store2,编写如下代码(大量代码和前述相同,就省略了):

package main

import (
	"database/sql"
	"errors"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"log"
	"time"
)

type Post struct {
	Id       int
	Content  string
	Author   string
	Comments []Comment
}

type Comment struct {
	Id      int
	Content string
	Author  string
	Post    *Post
}

var Db *sql.DB

func init() { // 此 init 不显式调用,自动隐式调用,实现初始化全局变量 Db
// ..................
}

func (comment *Comment) Create() (err error) {
	if comment.Post == nil {
		err = errors.New("帖子未找到")
		return
	}
	var result sql.Result
	result, err = Db.Exec(`INSERT INTO comment (content, author, post_id) 
		VALUES (?, ?, ?)`, comment.Content, comment.Author, comment.Post.Id)
	if err != nil {
		return
	}
	var last_insert_id int64
	last_insert_id, err = result.LastInsertId()
	if err != nil {
		return
	}
	comment.Id = int(last_insert_id)
	return
}

func Posts(limit int) (posts []Post, err error) {
// .................................
}

func GetPost(id int) (post Post, err error) {
	post = Post{}
	post.Comments = []Comment{}
	err = Db.QueryRow("SELECT id, content, author FROM post WHERE id = ?", id). // QueryRow(..) 预期返回单行结果集
											Scan(&post.Id, &post.Content, &post.Author) // pgsql 可以 RETURNING id
	rows, err := Db.Query("SELECT id, content, author FROM comment")
	if err != nil {
		return
	}
	for rows.Next() {
		comment := Comment{Post: &post}
		err = rows.Scan(&comment.Id, &comment.Content, &comment.Author)
		if err != nil {
			return
		}
		post.Comments = append(post.Comments, comment)
	}
	rows.Close()
	return
}

func (post *Post) Create() (err error) {
// ......................................
}

func (post *Post) Update() (err error) {
// ......................................
}

func (post *Post) Delete() (err error) {
// ......................................
}

func main() {
	post := Post{Content: "你好, C++! " + time.Now().Format("15:04:05"), Author: "李四"}
	post.Create()

	comment := Comment{Content: "C++确实好,就是太难学" + time.Now().Format("15:04:05"), Author: "张三", Post: &post}
	comment.Create()

	post_read, _ := GetPost(post.Id)

	fmt.Println(post_read)                  // 获取帖子
	fmt.Println(post_read.Comments)         // 获取帖子的评论
	fmt.Println(post_read.Comments[0].Post) // 验证帖子第一条评论对应的帖子是否为自身
}

显示结果如下:

sjg@sjg-PC:~/go/src/db_store2$ go run .
{3 你好, C++! 15:54:29 李四 [{1 C++确实好,就是太难学15:54:29 张三 0xc000032140}]}
[{1 C++确实好,就是太难学15:54:29 张三 0xc000032140}]
&{3 你好, C++! 15:54:29 李四 [{1 C++确实好,就是太难学15:54:29 张三 0xc000032140}]}

我们从代码发现,要构建一对多关系,就在代表“一”的结构体里,添加代表“多”的切片(切片本质上是指针);反过来,在代表“多”的结构体里,也添加一个指向“多”的指针成员。可以认为,post有一个指针指向comments列表,列表成员有一个指针指向post,这么设计和yii2中的Model对关系的处理是类似的。文章来源地址https://www.toymoban.com/news/detail-461927.html

到了这里,关于golang web学习随便记4-内存、文件、数据库的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 041-WEB攻防-ASP应用&HTTP.SYS&短文件&文件解析&Access注入&数据库泄漏

    1、ASP-SQL注入-Access数据库 2、ASP-默认安装-数据库泄漏下载 3、ASP-IIS-CVE短文件解析写入 演示案例: ➢ASP-默认安装-MDB数据库泄漏下载 ➢ASP-中间件-CVE短文件解析写权限 ➢ASP-SQL注入-SQLMAP使用ACCESS注入 由于大部分 ASP程序与ACCESS数据库搭建 ,但ACCESS无需连接 在== 脚本文件中定义

    2024年04月13日
    浏览(43)
  • .net 6 web api项目添加日志(Serilog)管理,将日志输出到控制台、文件、数据库

    1.在nuget安装下面几个包 Serilog Serilog.AspNetCore //用于日志输出到控制台 Serilog.Formatting.Compact //用于日志输出到mysql数据库 Serilog.Sinks.MySQL //用于日志输出到文件 Serilog.Sinks.RollingFile 2.在Program.cs文件配置日志参数,依赖注入 别忘了在appsettings.json添加数据库连接字符串 3. 使用日志

    2024年02月10日
    浏览(78)
  • Go语言(Golang)数据库编程

    要想连接到 SQL 数据库,首先需要加载目标数据库的驱动,驱动里面包含着于该数据库交互的逻辑。 sql.Open() 数据库驱动的名称 数据源名称 得到一个指向 sql.DB 这个 struct 的指针 sql.DB 是用来操作数据库的,它代表了0个或者多个底层连接的池,这些连接由sql 包来维护,sql 包会

    2024年02月03日
    浏览(88)
  • MySQL数据库内存配置与性能优化:合理分配内存,提升数据库性能

             引言 :MySQL是广泛使用的关系型数据库管理系统,而合理配置数据库的内存是保障其高性能运行的关键之一.本文将介绍如何根据MySQL数据库内存值大小来定义,以及这样配置如何影响数据库的性能   内存配置的基本原则 : innodb_buffer_pool_size :该参数定义了InnoDB存储引擎

    2024年02月22日
    浏览(56)
  • golang操作数据库--gorm框架、redis

    ①引入 ②初始化 ③增删改查 官网: http://gorm.io/ ①引入 ②初始化 ③增删改查 说明:Debug()可以查看执行的sql语句。 ④gorm gen的使用 a.先安装 (会安装到gopath的bin目录下,windows电脑,需要将该路径加入到系统路径) eg : b.举例: 说明1:windows电脑go install之后,把exe添加到系统路

    2024年02月10日
    浏览(39)
  • golang 连接 oracle 数据库 增删改查

     1,golang 连接 oracle 数据库  2,增删改查

    2024年02月09日
    浏览(49)
  • golang中给数据库datetime格式赋值

    1、定义数据库表映射结构体如上,create_time字段在表里面是datetime格式。 2、如果CreateTime不给值,在存库时,create_time字段的值为NULL。 3、赋值时,如下代码

    2024年02月09日
    浏览(48)
  • 内存数据库如何发挥内存优势?

    与以磁盘存储为主的普通数据库相比,内存数据库的数据访问速度可以高出几个数量级,能大幅提高运算性能,更适合高并发、低延时的业务场景。 不过,当前大部分内存数据库仍然采用 SQL 模型,而 SQL 缺乏一些必要的数据类型和运算,不能充分利用内存的特征实现某些高

    2024年02月03日
    浏览(42)
  • Golang 程序漏洞检测利器 govulncheck(二):漏洞数据库详解

    上一篇文章详细介绍了 Golang 程序漏洞扫描工具 govulncheck 的使用方法,govulncheck 强大功能的背后,离不开 Go 漏洞数据库(Go vulnerability database)的支持,接下来详细讲解下 Go 漏洞数据库相关的知识。 在当今数字化的世界中,软件安全是至关重要的。随着 Golang 在开发领域的日

    2024年02月10日
    浏览(33)
  • Redis内存数据库

    Redis内存数据库 NoSQL数据库简介 Redis简介 Redis应用场景 windows下安装和使用Redis 在linux下安装redis Redis数据可视化RedisDesktopManager Redis配置 Redis 数据类型 Redis 字符串(String) Redis 哈希(Hash) Redis 列表(List) Redis 集合(Set) Redis 有序集合(sorted set) Redis key命令 Redis连接命令 Redis服务器命令

    2024年02月09日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包