一、介绍
在 Redis 中,事务是由必须以原子方式提交的多个命令组成的单个工作单元。也就是说,要么执行所有命令,要么不执行任何命令。Redis 使用 、、 和 函数来实现此功能。MULTI
EXEC
DISCARD
WATCH
要通过该工具创建事务,您只需先运行命令,然后运行其他后续命令。最后,应执行命令来处理事务或命令刷新排队的命令。redis-cli
MULTI
EXEC
DISCARD
该命令允许您在事务的生存期内实现锁定机制,如果您的密钥被另一个会话修改,该命令应无法避免将 Redis 数据库置于不一致状态。WATCH
WATCHed
EXEC
在本指南中,您将使用 Redis 事务函数在 Linux 服务器上使用 Golang 和 MySQL 创建抢票应用程序。
二、准备工作
若要继续本教程,请确保具有以下各项:
- 一个 Linux 服务器。
- 具有
sudo
权限的非 root 用户。 - 一个MySQL服务器。
- 一个 Redis 服务器。
- 一个戈朗包。
三、 创建 MySQL 数据库、用户帐户和表
Redis 是一个内存数据库,虽然它可以将数据持久保存到磁盘,但它不是为此目的而设计的,可能无法以最佳方式执行。因此,在本指南中,您将使用 MySQL 数据库在 Redis 服务器生成票证信息后将其永久存储到 MySQL 表中。
通过 SSH 连接到您的服务器,然后按照以下步骤创建数据库。
-
- 以 身份登录到 MySQL 服务器。
root
$ sudo mysql -uroot -p
- 出现提示时输入您的 MySQL 密码,然后按继续。然后,执行以下命令以创建数据库和帐户。替换为强值。
root
ENTERbookings
bookings_user
EXAMPLE_PASSWORD
mysql> CREATE DATABASE bookings DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; CREATE USER 'bookings_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'EXAMPLE_PASSWORD'; GRANT ALL PRIVILEGES ON bookings.* TO 'bookings_user'@'localhost'; FLUSH PRIVILEGES;
- 切换到新数据库。
mysql> USE bookings;
- 接下来,创建一个表。在此示例应用程序中,您将使用 Redis 服务器从可用座位池中抓取乘客的座位。然后,您将在表中永久存储分配的信息和信息。
tickets
seat_no's
ticket_id's
tickets
mysql> CREATE TABLE tickets ( ticket_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, seat_no BIGINT ) ENGINE = InnoDB;
- 您的数据库、用户帐户和表现已就位。从 MySQL 服务器注销。
mysql> QUIT;
在下一步中,您将创建一个 Golang 脚本来接受通过 HTTPS 传入的工单请求。
- 以 身份登录到 MySQL 服务器。
四、创建文件main.go
要将此应用程序与其他 Linux 文件分开,您需要一个单独的源代码目录。
- 创建目录.
project
$ mkdir project
- 然后,切换到新目录.
project
$ cd project
- 接下来,用于创建文件。此文件包含运行应用程序时触发的主脚本.
nano
main.go
$ nano main.go
- With the file opened, paste the following information into the file.
main.go
package main import ( "encoding/json" "fmt" "net/http" "strconv" ) func main() { http.HandleFunc("/tickets", httpHandler) http.ListenAndServe(":8080", nil) } func httpHandler(w http.ResponseWriter, req *http.Request) { var err error resp := map[string]interface{}{} resp, err = newTicket() enc := json.NewEncoder(w) enc.SetIndent("", " ") if err != nil { resp = map[string]interface{}{"error": err.Error(),} } if err := enc.Encode(resp); err != nil { fmt.Println(err.Error()) } } func newTicket() (map[string]interface{}, error) { seatNo, err := createTicket("test") if err != nil { return nil, err } resp := map[string]interface{}{"Response" : "Seat # " + strconv.FormatInt(seatNo, 10) + " booked successfully.",} return resp, nil }
- Save and close the file when you’re through with editing.
- 在上面的文件中,你将导入包,该包允许你格式化 JSON 数据。接下来,你已包含用于格式化和输出字符串的包。该包允许您将其他数据类型转换为字符串格式,同时库提供 HTTP 实现。
main.go
encoding/json
fmt
strconv
net/http
- 在 main 函数 () 下,您正在侦听 URL 中端口上的传入 HTTP 请求。然后,您将 HTTP 请求重定向到函数,该函数又使用该语句调用该函数。
func main() {...}
8080
/tickets
func httpHandler(){...}
newTicket()
resp, err = newTicket()
- 在该函数下,您将使用该语句调用该函数,以从 Redis 服务器获取乘客的座位号。在下一步中,您将在新文件中创建函数。
func newTicket(){}
createTicket(...)
seatNo, err := createTicket("test")
createTicket(...)
tickets.go
五、 创建文件tickets.go
在此步骤中,您将创建一个连接到 Redis 服务器的 Golang 脚本。首先,脚本将读取一个键,用于检查可供预订的座位总数。然后,如果剩余席位数大于或等于 1,则脚本将保留一个席位号,将剩余席位数减 1 并返回分配给调用脚本的席位。test
seat_no
- 用于创建文件。
nano
tickets.go
$ nano tickets.go
- 然后,在文件中输入以下信息。
tickets.go
package main import ( "context" "errors" "strconv" "github.com/go-redis/redis" ) func createTicket(key string) (int64, error) { ctx := context.Background() redisClient := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", DB: 0, }) var seatNo int64 err := redisClient.Watch(ctx, func(tx *redis.Tx) error { val, err := tx.Get(ctx, key).Int64() if err != nil && err != redis.Nil { return err } seatNo = val if (seatNo - 1) < 0 { return errors.New("Unable to secure a seat.\r\n") } _, err = tx.Pipelined(ctx, func(pipe redis.Pipeliner) error { pipe.Set(ctx, key, strconv.FormatInt(seatNo - 1, 10), 0) return nil }) if err == nil { insertRecord(seatNo) } return err }, key) if err == redis.TxFailedErr { return createTicket(key) } return seatNo, err }
- 保存并关闭文件。
- 在上面的文件中,您已导入包,以使用语句为 Redis 调用提供不受限制的截止时间。然后,使用包将自定义错误返回到调用函数。该软件包允许您在 Golang 脚本中实现 Redis 函数。
context
ctx := context.Background()
errors
github.com/go-redis/redis
- 在 中,您接受 1 个参数。这是您用于在应用程序中保留可用席位的名称。在本教程中,你将用作键名称。在生产环境中,您可以考虑使用更有意义/更具描述性的名称,例如 。
func createTicket(key string) (int64, error){}
key
test
available_seats
- 该语句允许您连接并创建新的 Redis 客户端实例。然后,您使用语句初始化一个空变量。脚本分配座位号后,您将填充此变量。
redisClient := redis.NewClient(...)
seatNo
var seatNo int64
- 接下来,您将使用 Redis 函数,该语句在事务的生存期内监视密钥。如果另一个会话以任何方式修改了密钥,则整个事务应中止,并且您已对脚本进行了编码,以便使用语句重试脚本。请记住,在生产环境中,客户可以从不同的应用程序(例如移动应用程序、API、桌面应用程序、门户等)购买票证。这里的想法是一次发行一张票以避免超额预订。
WATCH
err := redisClient.Watch(ctx, func()...{...}, key)
test
test
if err == redis.TxFailedErr { return createTicket(key) }
- 在函数内部,您可以使用语句检索剩余席位的值。如果没有剩余席位,您将使用 语句 抛出自定义错误。
WATCH
val, err := tx.Get(ctx, key).Int64()
if (seatNo - 1) < 0 { return errors.New("Unable to secure a seat.\r\n") }
- 接下来,预订座位后,您将使用语句减少可用座位数。Redis 管道允许您在一次网络调用中将多个命令传输到 Redis 服务器。在本教程中仅执行一个命令时,应始终使用管道模型,以便在应用程序逻辑发生更改时更轻松地进行修改。
pipe.Set(ctx, key, strconv.FormatInt(seatNo - 1, 10), 0)
- 然后,您调用该函数以将票证信息保存到 MySQL 数据库,以防使用该语句执行流水线命令时没有错误。一旦整个函数运行,它应该返回文件或任何错误,以防遇到任何错误。
insertRecord()
if err == nil { insertRecord(seatNo) }
createTicket()
seatNo
main.go
- 在下一步中,您将在不同的文件中创建要在此调用的函数。
insertRecord()
tickets.go
database.go
六、创建文件database.go
您将为此抢票应用程序创建的最后一个脚本是文件。此文件包含将票证信息永久存储到 MySQL 数据库的逻辑。database.go
- 使用 Nano 创建文件。
database.go
$ nano database.go
- 然后,在文件中输入以下信息。
database.go
package main import ( "database/sql" _ "github.com/go-sql-driver/mysql" ) func insertRecord(seatNo int64) error { dbUser := "bookings_user" dbPassword := "EXAMPLE_PASSWORD" dbName := "bookings" db, err := sql.Open("mysql", dbUser + ":" + dbPassword + "@tcp(127.0.0.1:3306)/" + dbName) if err != nil { return err } defer db.Close() queryString := "insert into tickets (seat_no) values (?)" stmt, err := db.Prepare(queryString) if err != nil { return err } defer stmt.Close() _, err = stmt.Exec(seatNo) if err != nil { return err } return nil }
- 保存并关闭文件。
- 在上面的文件中,你正在使用 and 包来实现 Golang 中的 SQL 和 MySQL 功能。在该函数下,你将使用之前创建的凭据连接到 MySQL 数据库。然后,您将工单信息保存到表中。
database/sql
github.com/go-sql-driver/mysql
func insertRecord(...) error {...}
tickets
- 现在,您已经编写了使用 MySQL 和 Golang 运行 Redis 事务的所有脚本。在下一步中,您将测试一切是否按预期工作。
七、 测试 Redis 事务应用程序
您的 Golang 事务应用程序现在已经准备好进行测试了。
- 在执行应用程序之前,请导入已在应用程序中实现的所有包。
$ go get github.com/go-redis/redis $ go get github.com/go-sql-driver/mysql
- 接下来,打开 Redis 命令行界面。
$ redis-cli
- 通过将键的值设置为 .
10
test
10
$ SET test 10
- 从 Redis 服务器注销。
$ QUIT
- 确保您仍在目录下,然后执行以下命令以运行 Golang 应用程序。
project
$ go run ./
- 上面的命令有一个阻塞功能,可以在端口下旋转 Web 服务器。不要在此终端窗口上运行任何其他命令。
8080
- 接下来,在新的终端窗口中通过 SSH 连接到您的服务器并安装 Apache Bench () 软件包。你将使用此工具向应用程序发送并行票证请求命令,以查看它是否可以处理事务,而不会出现任何超额预订或争用情况。
ab
$ sudo apt install -y apache2-utils
- 接下来,向应用程序发送并行票证请求。请记住,您只在 Redis 服务器中使用了席位。因此,只有事务应该成功,其余事务应该失败。此外,由于您已使用该函数实现了 Redis 锁,因此不应出现不同会话具有相同情况的情况。
20
10
10
seat_no
WATCH
$ ab -v 2 -n 20 -c 20 http://localhost:8080/tickets
- 您应该会收到以下响应。
... { "Response": "Seat # n booked successfully." } ... { "error": "Unable to secure a seat.\r\n" } ...
- 接下来,在仍然登录到第二个终端窗口的同时,登录到MySQL数据库以确认新的更改。
root
$ sudo mysql -u root -p
- 输入 MySQL 服务器的密码,然后按继续。然后,切换到数据库。
root
ENTERbooking
mysql> USE bookings;
- 对表运行语句。
SELECT
tickets
mysql> SELECT ticket_id, seat_no FROM tickets;
- 您现在应该看到以下票证和关联的 .从以下输出中可以看出,没有超额预订的情况。此外,该脚本已成功消除了任何竞争条件的机会,因为没有两张票具有相同的。
seat_no's
seat_no
+-----------+---------+ | ticket_id | seat_no | +-----------+---------+ | 1 | 10 | | 2 | 9 | | 3 | 7 | | 4 | 8 | | 5 | 6 | | 6 | 5 | | 7 | 3 | | 8 | 4 | | 9 | 1 | | 10 | 2 | +-----------+---------+ 10 rows in set (0.00 sec)
- 您的脚本现在按预期工作。
八、结论
在本指南中,您已经在 Linux 服务器中实现了 Redis 事务并使用 Golang 和 MySQL 数据库锁定,以创建抢票应用程序。使用本指南中的逻辑可避免在创建多用户应用程序时出现争用条件和数据库不一致。