Go Web 编程之 数据库(go数据库使用指南)
nanshan 2024-11-02 12:12 10 浏览 0 评论
热烈欢迎你,相识是一种缘分,Echa 哥为了你的到来特意准备了一份惊喜,go学习资料《「转」go 语言实战笔记教程系列大纲汇总-值得收藏》
概述
数据库用来存储数据。只要不是玩具项目,每个项目都需要用到数据库。现在用的最多的还是 MySQL,PostgreSQL的使用也在快速增长中。 在 Web 开发中,数据库也是必须的。本文将介绍如何在 Go 语言中操作数据库,基于 MySQL。本文假定大家已经掌握了数据库和 MySQL 的基础知识。 关于 MySQL 有一个非常详细的免费教程我放在参考中了,需要的自取。
Go 语言标准库database/sql只是提供了一组查询和操作数据库的接口,没有提供任何实现。在 Go 中操作数据库只能使用第三方库。 各种类型的数据库都有对应的第三方库。Go 中支持 MySQL 的驱动中最常见的是go-sql-driver/mysql。 该库支持database/sql,全部采用 go 实现。
数据库操作
准备工作
创建一个数据库department,表示公司中的某个部门。 在该库中创建两张表employees和teams。employees记录员工信息,teams记录小组信息。 每个员工都属于一个小组,每个小组都有若干名员工。
SET NAMES utf8mb4;
CREATE DATABASE IF NOT EXISTS `department`
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
USE `department`;
CREATE TABLE IF NOT EXISTS `employees` (
`id` INT(11) AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL DEFAULT '',
`age` INT(11) NOT NULL DEFAULT 0,
`salary` INT(11) NOT NULL DEFAULT 0,
`team_id` INT(11) NOT NULL DEFAULT 0
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `teams` (
`id` INT(11) AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL DEFAULT ''
) ENGINE=InnoDB;
INSERT INTO `teams`(`name`)
VALUES
('策划'),
('开发'),
('运营'),
('运维');
INSERT INTO `employees`(`name`, `age`, `salary`, `team_id`)
VALUES
('张三', 28, 1200, 1),
('李四', 38, 4000, 1),
('王五', 36, 3500, 1),
('赵六', 31, 3100, 2),
('田七', 29, 2900, 2),
('吴八', 27, 1500, 3),
('朱九', 26, 1600, 3),
('钱十', 27, 1800, 3),
('陶十一', 28, 1900, 4),
('汪十二', 25, 2000, 4),
('剑十三', 24, 30000, 4);
复制代码
插入一些测试数据。将这个department.sql文件保存到某个目录,然后在该目录打开命令行:
$ mysql -u root -p
复制代码
输入密码连接到数据库,然后输入以下命令:
mysql> source department.sql
Query OK, 0 rows affected (0.00 sec)
Query OK, 2 rows affected (0.02 sec)
Query OK, 1 row affected (0.00 sec)
Database changed
Query OK, 0 rows affected, 4 warnings (0.02 sec)
Query OK, 0 rows affected, 1 warning (0.02 sec)
Query OK, 4 rows affected (0.01 sec)
Records: 4 Duplicates: 0 Warnings: 0
Query OK, 11 rows affected (0.00 sec)
Records: 11 Duplicates: 0 Warnings: 0
mysql>
复制代码
这样数据库和表就创建好了。
连接数据库
go-sql-driver/mysql是第三方库,需要安装:
$ go get github.com/go-sql-driver/mysql
复制代码
使用:
package main
import (
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/department")
if err != nil {
log.Fatal("connect database failed: ", err)
}
defer db.Close()
}
复制代码
我们操作数据库并不是直接使用mysql库,而是通过database/sql的接口。
import _ "github.com/go-sql-driver/mysql"
复制代码
上面代码导入mysql,但并不直接使用,而是利用导入的副作用执行mysql库的init函数,将mysql驱动注册到database/sql中:
// go-sql-driver/mysql/driver.go
func init() {
sql.Register("mysql", &MySQLDriver{})
}
复制代码
然后在程序中使用sql.Open创建一个sql.DB结构,参数一即为mysql库注册的名字,参数二实际上就是指定数据库连接信息的。 每个数据库接受的连接信息是不同的。对于 MySQL 来说,连接信息实际上是一个 DSN (Data Source Name)。DSN 的一般格式为:
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...?mN=valueN]
复制代码
示例中使用的就是一个 DSN,指定用户名为root,密码为12345, 通过 tcp 协议连接到 ip 为127.0.0.1,端口为 3306 的 MySQL 的department数据库上。
在使用完成后,需要调用db.Close关闭sql.DB。
**需要特别注意的是,sql.Open并不会建立到数据库的连接,它也不会检测驱动的连接参数。它仅仅创建了一个数据库抽象层给后面使用。 到数据库的连接实际上会在需要的时候惰性地创建。**所以,我们使用一个非法的用户名或密码,连接一个主机上不存在的库,sql.Open也不会报错。 将上面的 DSN 改为user:password@tcp(127.0.0.1:6666)/not_exist_department,运行程序,没有报错。
如果想要检测数据库是否可访问,可以使用db.Ping()函数:
err = db.Ping()
if err != nil {
log.Fatal("ping failed: ", err)
}
复制代码
这时连接not_exist_department会报错:
2020/01/20 22:16:12 ping failed: Error 1049: Unknown database 'not_exist_department'
exit status 1
复制代码
sql.DB对象一般作为某种形式的全局变量长期存活。不要频繁打开、关闭该对象。这对性能会有非常大的影响。
查询
先看一个简单示例:
package main
import (
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/department")
if err != nil {
log.Fatal("open database failed: ", err)
}
defer db.Close()
var id int
var name string
var age int
var salary int
var teamId int
rows, err := db.Query("select id, name, age, salary, team_id from employees where id = ?", 1)
if err != nil {
log.Fatal("query failed: ", err)
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&id, &name, &age, &salary, &teamId)
if err != nil {
log.Fatal("scan failed: ", err)
}
log.Printf("id: %d name:%s age:%d salary:%d teamId:%d\n", id, name, age, salary, teamId)
}
err = rows.Err()
if err != nil {
log.Fatal(err)
}
}
复制代码
运行程序,输出:
2020/01/20 22:27:21 id: 1 name:张三 age:28 salary:1200 teamId:1
复制代码
从上面程序中,我们看到一个查询操作的基本流程:
- 使用db.Query()查询数据库;
- 在循环中遍历返回的行,rows.Scan()读取各列的值,rows.Next()将“指针”移动到下一行;
- 遍历完所有行时,rows.Next()将返回 false,循环退出。
数据库操作可能会遇到各种各样的错误,所以错误处理很重要。例如,在循环中调用rows.Scan可能产生错误。
遍历结束后,一定要关闭rows。因为它持有连接的指针,不关闭会造成资源泄露。rows.Next()遇到最后一行时会返回一个 EOF 错误,并关闭连接。 另外,如果rows.Next()由于产生错误返回 false,rows也会自动关闭。其它情况下,如果提前退出循环,可能会忘记关闭rows。 所以一般使用defer rows.Close()确保正常关闭。
Tips:
调用Scan方法时,其内部会根据传入的参数类型执行相应的数据类型转换。利用这个特性可以简化代码。 例如,MySQL 中某一列是VARCHAR/CHAR或类似的文本类型,但是我们知道它保存的是一个整数。 那么就可以传入一个int类型的变量,Scan内部会帮助我们将字符串转为int。免除了我们手动调用strconv相关方法的麻烦。
database/sql中函数的命名特别讲究:
- Query*这种以Query开头的函数,肯定返回若干行(可能为 0)数据;
- 不返回行数据的语句,不能使用Query*函数,应该使用Exec。
Prepare
当我们需要多次执行同一条语句时,最好的做法是先创建一个PreparedStatement。这个PreparedStatement可以包含参数占位符,后续执行时再提供参数。
每种数据库都有自己参数占位符,MySQL 使用的是?。使用参数占位符有一个明显的好处:能避免SQL 注入攻击。
需要执行 SQL 时,传入参数调用PreparedStatement的Query方法即可:
func main() {
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/department")
if err != nil {
log.Fatal("open failed: ", err)
}
defer db.Close()
stmt, err := db.Prepare("select id, name, age, salary from employees where id = ?")
if err != nil {
log.Fatal("prepare failed: ", err)
}
defer stmt.Close()
rows, err := stmt.Query(2)
if err != nil {
log.Fatal("query failed: ", err)
}
defer rows.Close()
var (
id int
name string
age int
salary int
)
for rows.Next() {
err := rows.Scan(&id, &name, &age, &salary)
if err != nil {
log.Fatal("scan failed: ", err)
}
log.Printf("id:%d name:%s age:%d salary:%d\n", id, name, age, salary)
}
err = rows.Err()
if err != nil {
log.Fatal(err)
}
}
复制代码
实际上,在db.Query()函数内部,会先创建一个PreparedStatement,执行它,然后关闭。这会与数据库产生 3 次通信。所以尽量先创建PreparedStatement,再使用。
单行查询
如果查询最多只返回一行数据,我们不用写循环处理,使用QueryRow可以简化代码编写。
直接调用db.QueryRow:
var name string
err = db.QueryRow("select name from employees where id = ?", 1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
复制代码
也可以在PreparedStatement上调用QueryRow:
stmt, err := db.Prepare("select name from employees where id = ?").Scan(&name)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
var name string
err = stmt.QueryRow(1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
复制代码
注意,QueryRow遇到的错误会延迟到调用Scan时才返回。
插入/修改/删除
INSERT/UPDATE/DELETE这些操作,由于都不返回行,应该使用Exec函数。建议先创建PreparedStatement再执行。
现在“策划组”新加入了一名员工:
func main() {
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/department")
if err != nil {
log.Fatal("open failed: ", err)
}
defer db.Close()
stmt, err := db.Prepare("INSERT INTO employees(name, age, salary, team_id) VALUES(?,?,?,?)")
if err != nil {
log.Fatal("prepare failed: ", err)
}
defer stmt.Close()
res, err := stmt.Exec("柳十四", 32, 5000, 1)
if err != nil {
log.Fatal("exec failed: ", err)
}
lastId, err := res.LastInsertId()
if err != nil {
log.Fatal("fetch last insert id failed: ", err)
}
rowCnt, err := res.RowsAffected()
if err != nil {
log.Fatal("fetch rows affected failed: ", err)
}
log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)
}
复制代码
Exec方法返回一个sql.Result接口类型的值:
// src/database/sql/sql.go
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
复制代码
有些表设置了自增的 id,插入时不需要设置 id,数据库会自动生成一个返回。LastInsertId()返回插入时生成的 id。 RowsAffected()返回受影响的行数。
运行程序,输出:
2020/01/21 07:20:26 ID = 12, affected = 1
复制代码
事务
在 Go 中,事务本质上是一个对象,它持有一个到数据库的连接。通过该对象执行我们上面介绍的方法时, 都会使用这个相同的连接。调用db.Begin()创建一个事务对象,然后在该对象上执行上面的方法, 最后成功调用Commit(),失败调用Rollback()关闭事务。
func main() {
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/department")
if err != nil {
log.Fatal("open failed: ", err)
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
log.Fatal("begin failed: ", err)
}
defer tx.Rollback()
stmt, err := tx.Prepare("UPDATE employees SET team_id=? WHERE id=?")
if err != nil {
log.Fatal("prepare failed: ", err)
}
defer stmt.Close()
_, err = stmt.Exec(2, 1)
if err != nil {
log.Fatal("exec failed: ", err)
}
tx.Commit()
}
复制代码
注意,在事务内部不能再直接调用db的方法了,因为db使用的是与事务不同的连接,可能会导致执行结果的不一致。
错误处理
database/sql中几乎所有的操作最后一个返回值都是一个error类型。数据库会出现各种各样的错误,我们应该时刻检查是否出现了错误。下面介绍几种特殊情况产生的错误。
遍历结果集
for rows.Next() {
// ...
}
if err = rows.Err(); err != nil {
}
复制代码
``rows.Err()返回的错误可能是rows.Next()循环中的多种错误。循环可能由于某些原因提前退出了。我们应该检测循环是否正常退出。 异常退出时,database/sql会自动调用rows.Close()。提前退出时,我们需要手动调用rows.Close()。**可以多次调用rows.Close()`**。
关闭结果集
实际上,rows.Close()也返回一个错误。但是,对于这个错误,我们能做的事情比较有限。通常就是记录日志。 如果不需要记录日志,通常会忽略这个错误。
QueryRow
考虑下面的代码:
var name string
err = db.QueryRow("SELECT name FROM employees WHERE id = ?", 1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
复制代码
如果没有id = 1的员工,Scan()要如何处理?
Go 定义了一个特殊的错误常量,sql.ErrNoRows。如果没有符合要求的行,QueryRow将返回这个错误。 这个错误在大多数情况下需要特殊处理,因为没有结果在应用层通常不认为是错误。
var name string
err = db.QueryRow("SELECT name FROM employees WHERE id = ?", 1).Scan(&name)
if err != nil {
if err == sql.ErrNoRows {
} else {
log.Fatal(err)
}
}
fmt.Println(name)
复制代码
那为什么QueryRow在没有符合要求的行时返回一个错误?
因为要区分是否返回了行,如果返回空结果集,由于Scan()不会做任何时间,我们就不能区分name读取到了空字符串,还是初始值。
特定的数据库错误
为了辨别发生了何种错误,有一种做法是检查错误描述中是否有特定的文本:
rows, err := db.Query("SELECT someval FROM sometable")
if err != nil {
if strings.Contains(err.Error(), "Access denied") {
}
}
复制代码
但是不推荐这种做法,因为不同的数据库版本,这些描述不一定能保持一致。
比较好的做法是将错误转成特定数据库驱动的错误,然后比较错误码:
if driverErr, ok := err.(*mysql.MySQLError); ok {
if driverErr.Number == 1045 {
}
}
复制代码
不同驱动间判断方法可能不同。另外,直接写数字1045也不太好,VividCortex 整理了 MySQL 错误码,GitHub 仓库为mysqlerr。使用库后续便于修改:
if driverErr, ok := err.(*mysql.MySQLError); ok {
if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
}
}
复制代码
处理未知列
有时候,可能我们不能确定查询返回多少列。但是Scan()要求传入正确数量的参数。为此,我们可以先使用rows.Columns()返回所有列名,然后创建同样大小的字符串指针切片传给Scan()函数:
func main() {
db, err := sql.Open("mysql", "root:12345@tcp(127.0.0.1:3306)/department")
if err != nil {
log.Fatal("open failed: ", err)
}
defer db.Close()
stmt, err := db.Prepare("SELECT * FROM employees")
if err != nil {
log.Fatal("prepare failed: ", err)
}
defer stmt.Close()
rows, err := stmt.Query()
if err != nil {
log.Fatal("exec failed: ", err)
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
log.Fatal("columns failed: ", err)
}
data := make([]interface{}, len(cols), len(cols))
for i := range data {
data[i] = new(string)
}
for rows.Next() {
err = rows.Scan(data...)
if err != nil {
log.Fatal("scan failed: ", err)
}
for i := 0; i < len(cols); i++ {
fmt.Printf("%s: %s ", cols[i], *(data[i].(*string)))
}
fmt.Println()
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
}
复制代码
运行程序:
id: 1 name: 张三 age: 28 salary: 1200 team_id: 2
id: 2 name: 李四 age: 38 salary: 4000 team_id: 1
id: 3 name: 王五 age: 36 salary: 3500 team_id: 1
id: 4 name: 赵六 age: 31 salary: 3100 team_id: 2
id: 5 name: 田七 age: 29 salary: 2900 team_id: 2
id: 6 name: 吴八 age: 27 salary: 1500 team_id: 3
id: 7 name: 朱九 age: 26 salary: 1600 team_id: 3
id: 8 name: 钱十 age: 27 salary: 1800 team_id: 3
id: 9 name: 陶十一 age: 28 salary: 1900 team_id: 4
id: 10 name: 汪十二 age: 25 salary: 2000 team_id: 4
id: 11 name: 剑十三 age: 24 salary: 30000 team_id: 4
id: 12 name: 柳十四 age: 32 salary: 5000 team_id: 1
复制代码
连接池
database/sql实现了一个基本的连接池。连接池有一些有趣的特性,了解一下,避免踩坑:
- 对同一个数据库连续执行两个语句,这两个语句可能在不同的数据库连接上进行的。结果可能让人误解。例如先LOCK TABLES,然后执行INSERT可能会阻塞;
- 需要新的连接且池中没有空闲连接时,创建一个新连接;
- 默认,连接数没有限制。如果同时执行很多操作,可能会同时创建很多连接。数据库可能出现too many connections错误;
- 调用db.SetMaxIdleConns(N)限制池中最大空闲连接数;db.SetMaxOpenConns(N)限制所有打开的连接数;
- 一个连接很长时间不使用可能会出现问题,如果遇到连接超时,可以试试将最大空闲连接数设置为 0;
- 重用长时间存活的连接可能会导致网络问题,可以调用db.SetConnMaxLifeTime(duration)设置连接最大存活时间。
总结
本文介绍了如何在 Go 中查询和修改数据库,主要是database/sql和go-sql-driver/mysql库的用法。database/sql的接口并不复杂,但是很多细节需要注意。一不留神可能就有资源泄露。
参考
- MySQL 教程,非常详细的教程
- Go database/sql 教程
- Build Web Application with Golang
相关推荐
- 实战派 | Java项目中玩转Redis6.0客户端缓存
-
铺垫首先介绍一下今天要使用到的工具Lettuce,它是一个可伸缩线程安全的redis客户端。多个线程可以共享同一个RedisConnection,利用nio框架Netty来高效地管理多个连接。放眼望向...
- 轻松掌握redis缓存穿透、击穿、雪崩问题解决方案(20230529版)
-
1、缓存穿透所谓缓存穿透就是非法传输了一个在数据库中不存在的条件,导致查询redis和数据库中都没有,并且有大量的请求进来,就会导致对数据库产生压力,解决这一问题的方法如下:1、使用空缓存解决对查询到...
- Redis与本地缓存联手:多级缓存架构的奥秘
-
多级缓存(如Redis+本地缓存)是一种在系统架构中广泛应用的提高系统性能和响应速度的技术手段,它综合利用了不同类型缓存的优势,以下为你详细介绍:基本概念本地缓存:指的是在应用程序所在的服务器内...
- 腾讯云国际站:腾讯云服务器如何配置Redis缓存?
-
本文由【云老大】TG@yunlaoda360撰写一、安装Redis使用包管理器安装(推荐)在CentOS系统中,可以通过yum包管理器安装Redis:sudoyumupdate-...
- Spring Boot3 整合 Redis 实现数据缓存,你做对了吗?
-
你是否在开发互联网大厂后端项目时,遇到过系统响应速度慢的问题?当高并发请求涌入,数据库压力剧增,响应时间拉长,用户体验直线下降。相信不少后端开发同行都被这个问题困扰过。其实,通过在SpringBo...
- 【Redis】Redis应用问题-缓存穿透缓存击穿、缓存雪崩及解决方案
-
在我们使用redis时,也会存在一些问题,导致请求直接打到数据库上,导致数据库挂掉。下面我们来说说这些问题及解决方案。1、缓存穿透1.1场景一个请求进来后,先去redis进行查找,redis存在,则...
- Spring boot 整合Redis缓存你了解多少
-
在前一篇里面讲到了Redis缓存击穿、缓存穿透、缓存雪崩这三者区别,接下来我们讲解Springboot整合Redis中的一些知识点:之前遇到过,有的了四五年,甚至更长时间的后端Java开发,并且...
- 揭秘!Redis 缓存与数据库一致性问题的终极解决方案
-
在现代软件开发中,Redis作为一款高性能的缓存数据库,被广泛应用于提升系统的响应速度和吞吐量。然而,缓存与数据库之间的数据一致性问题,一直是开发者们面临的一大挑战。本文将深入探讨Redis缓存...
- 高并发下Spring Cache缓存穿透?我用Caffeine+Redis破局
-
一、什么是缓存穿透?缓存穿透是指查询一个根本不存在的数据,导致请求直接穿透缓存层到达数据库,可能压垮数据库的现象。在高并发场景下,这尤其危险。典型场景:恶意攻击:故意查询不存在的ID(如负数或超大数值...
- Redis缓存三剑客:穿透、雪崩、击穿—手把手教你解决
-
缓存穿透菜小弟:我先问问什么是缓存穿透?我听说是缓存查不到,直接去查数据库了。表哥:没错。缓存穿透是指查询一个缓存中不存在且数据库中也不存在的数据,导致每次请求都直接访问数据库的行为。这种行为会让缓存...
- Redis中缓存穿透问题与解决方法
-
缓存穿透问题概述在Redis作为缓存使用时,缓存穿透是常见问题。正常查询流程是先从Redis缓存获取数据,若有则直接使用;若没有则去数据库查询,查到后存入缓存。但当请求的数据在缓存和数据库中都...
- Redis客户端缓存的几种实现方式
-
前言:Redis作为当今最流行的内存数据库和缓存系统,被广泛应用于各类应用场景。然而,即使Redis本身性能卓越,在高并发场景下,应用于Redis服务器之间的网络通信仍可能成为性能瓶颈。所以客户端缓存...
- Nginx合集-常用功能指导
-
1)启动、重启以及停止nginx进入sbin目录之后,输入以下命令#启动nginx./nginx#指定配置文件启动nginx./nginx-c/usr/local/nginx/conf/n...
- 腾讯云国际站:腾讯云怎么提升服务器速度?
-
本文由【云老大】TG@yunlaoda360撰写升级服务器规格选择更高性能的CPU、内存和带宽,以提供更好的处理能力和网络性能。优化网络配置调整网络接口卡(NIC)驱动,优化TCP/IP参数...
- 雷霆一击服务器管理员教程
-
本文转载莱卡云游戏服务器雷霆一击管理员教程(搜索莱卡云面版可搜到)首先你需要给服务器设置管理员密码,默认是空的管理员密码在启动页面进行设置设置完成后你需要重启服务器才可生效加入游戏后,点击键盘左上角E...
你 发表评论:
欢迎- 一周热门
-
-
爱折腾的特斯拉车主必看!手把手教你TESLAMATE的备份和恢复
-
如何在安装前及安装后修改黑群晖的Mac地址和Sn系列号
-
[常用工具] OpenCV_contrib库在windows下编译使用指南
-
WindowsServer2022|配置NTP服务器的命令
-
Ubuntu系统Daphne + Nginx + supervisor部署Django项目
-
WIN11 安装配置 linux 子系统 Ubuntu 图形界面 桌面系统
-
解决Linux终端中“-bash: nano: command not found”问题
-
Linux 中的文件描述符是什么?(linux 打开文件表 文件描述符)
-
K3s禁用Service Load Balancer,解决获取浏览器IP不正确问题
-
NBA 2K25虚拟内存不足/爆内存/内存占用100% 一文速解
-
- 最近发表
-
- 实战派 | Java项目中玩转Redis6.0客户端缓存
- 轻松掌握redis缓存穿透、击穿、雪崩问题解决方案(20230529版)
- Redis与本地缓存联手:多级缓存架构的奥秘
- 腾讯云国际站:腾讯云服务器如何配置Redis缓存?
- Spring Boot3 整合 Redis 实现数据缓存,你做对了吗?
- 【Redis】Redis应用问题-缓存穿透缓存击穿、缓存雪崩及解决方案
- Spring boot 整合Redis缓存你了解多少
- 揭秘!Redis 缓存与数据库一致性问题的终极解决方案
- 高并发下Spring Cache缓存穿透?我用Caffeine+Redis破局
- Redis缓存三剑客:穿透、雪崩、击穿—手把手教你解决
- 标签列表
-
- linux 查询端口号 (58)
- docker映射容器目录到宿主机 (66)
- 杀端口 (60)
- yum更换阿里源 (62)
- internet explorer 增强的安全配置已启用 (65)
- linux自动挂载 (56)
- 禁用selinux (55)
- sysv-rc-conf (69)
- ubuntu防火墙状态查看 (64)
- windows server 2022激活密钥 (56)
- 无法与服务器建立安全连接是什么意思 (74)
- 443/80端口被占用怎么解决 (56)
- ping无法访问目标主机怎么解决 (58)
- fdatasync (59)
- 405 not allowed (56)
- 免备案虚拟主机zxhost (55)
- linux根据pid查看进程 (60)
- dhcp工具 (62)
- mysql 1045 (57)
- 宝塔远程工具 (56)
- ssh服务器拒绝了密码 请再试一次 (56)
- ubuntu卸载docker (56)
- linux查看nginx状态 (63)
- tomcat 乱码 (76)
- 2008r2激活序列号 (65)