将存储部分迁移到Redis
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
- 后端: Go 1.21
|
||||
- 前端: HTML + CSS + JavaScript
|
||||
- 通信: WebSocket
|
||||
- 存储: Redis
|
||||
- 代理: Nginx
|
||||
- 部署: Docker Compose
|
||||
|
||||
@@ -48,6 +49,7 @@ doudizhu-server/
|
||||
│ │ ├── models/ # 数据模型
|
||||
│ │ ├── game/ # 游戏逻辑
|
||||
│ │ ├── handlers/ # HTTP处理器
|
||||
│ │ ├── redis/ # Redis客户端封装
|
||||
│ │ └── ws/ # WebSocket处理
|
||||
│ ├── Dockerfile
|
||||
│ └── go.mod
|
||||
@@ -87,6 +89,10 @@ docker compose up -d --build
|
||||
### 本地运行
|
||||
|
||||
```bash
|
||||
# 启动Redis (Docker)
|
||||
docker run -d --name redis -p 6379:6379 redis:7-alpine
|
||||
|
||||
# 运行后端
|
||||
cd doudizhu-server/app
|
||||
go mod tidy
|
||||
go run ./cmd
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"doudizhu-server/internal/game"
|
||||
"doudizhu-server/internal/handlers"
|
||||
"doudizhu-server/internal/redis"
|
||||
"doudizhu-server/internal/ws"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
gameMgr := game.NewGameManager()
|
||||
redisAddr := os.Getenv("REDIS_ADDR")
|
||||
if redisAddr == "" {
|
||||
redisAddr = "localhost:6379"
|
||||
}
|
||||
|
||||
redisClient, err := redis.NewClient(redis.Config{
|
||||
Addr: redisAddr,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to Redis: %v", err)
|
||||
}
|
||||
defer redisClient.Close()
|
||||
log.Println("Connected to Redis:", redisAddr)
|
||||
|
||||
gameMgr := game.NewGameManager(redisClient)
|
||||
hub := ws.NewHub(gameMgr)
|
||||
go hub.Run()
|
||||
|
||||
@@ -32,8 +54,31 @@ func main() {
|
||||
})
|
||||
mux.HandleFunc("/api/ws", h.WebSocket)
|
||||
|
||||
log.Println("App server starting on :8080")
|
||||
if err := http.ListenAndServe(":8080", mux); err != nil {
|
||||
log.Fatal(err)
|
||||
srv := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Println("App server starting on :8080")
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Printf("Server shutdown error: %v", err)
|
||||
}
|
||||
|
||||
hub.Stop()
|
||||
gameMgr.Stop()
|
||||
log.Println("Server stopped")
|
||||
}
|
||||
|
||||
11
app/go.mod
11
app/go.mod
@@ -2,6 +2,13 @@ module doudizhu-server
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/gorilla/websocket v1.5.1
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/redis/go-redis/v9 v9.5.1
|
||||
)
|
||||
|
||||
require golang.org/x/net v0.17.0 // indirect
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
)
|
||||
|
||||
10
app/go.sum
10
app/go.sum
@@ -1,4 +1,14 @@
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
|
||||
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"doudizhu-server/internal/models"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRoomNotFound = errors.New("room not found")
|
||||
ErrRoomFull = errors.New("room is full")
|
||||
ErrGameStarted = errors.New("game already started")
|
||||
ErrGameNotStarted = errors.New("game not started")
|
||||
ErrPlayerNotFound = errors.New("player not found")
|
||||
ErrNotEnoughPlayers = errors.New("not enough players")
|
||||
ErrPlayerNotReady = errors.New("player not ready")
|
||||
ErrNotYourTurn = errors.New("not your turn")
|
||||
ErrCardsNotInHand = errors.New("cards not in hand")
|
||||
ErrInvalidCardType = errors.New("invalid card type")
|
||||
ErrCannotBeat = errors.New("cannot beat last play")
|
||||
ErrCannotPass = errors.New("cannot pass")
|
||||
)
|
||||
|
||||
func generateID() string {
|
||||
b := make([]byte, 8)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func cardKey(c models.Card) string {
|
||||
return fmt.Sprintf("%d_%d", c.Suit, c.Value)
|
||||
}
|
||||
@@ -1,13 +1,31 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"context"
|
||||
"doudizhu-server/internal/models"
|
||||
"doudizhu-server/internal/redis"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRoomNotFound = errors.New("room not found")
|
||||
ErrRoomFull = errors.New("room is full")
|
||||
ErrGameStarted = errors.New("game already started")
|
||||
ErrGameNotStarted = errors.New("game not started")
|
||||
ErrNotYourTurn = errors.New("not your turn")
|
||||
ErrPlayerNotFound = errors.New("player not found")
|
||||
ErrCardsNotInHand = errors.New("cards not in hand")
|
||||
ErrInvalidCardType = errors.New("invalid card type")
|
||||
ErrCannotBeat = errors.New("cannot beat last play")
|
||||
ErrCannotPass = errors.New("cannot pass")
|
||||
ErrNotEnoughPlayers = errors.New("not enough players")
|
||||
)
|
||||
|
||||
type CardLogic struct{}
|
||||
|
||||
func NewCardLogic() *CardLogic {
|
||||
@@ -227,30 +245,31 @@ func (cl *CardLogic) getMainValue(cards []models.Card) int {
|
||||
}
|
||||
|
||||
type GameManager struct {
|
||||
rooms map[string]*models.Room
|
||||
players map[string]*models.Player
|
||||
deck map[string][]models.Card
|
||||
discard map[string][]models.Card
|
||||
mu sync.RWMutex
|
||||
cl *CardLogic
|
||||
rdb *redis.Client
|
||||
cl *CardLogic
|
||||
mu sync.RWMutex
|
||||
stopCleanup chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func NewGameManager() *GameManager {
|
||||
return &GameManager{
|
||||
rooms: make(map[string]*models.Room),
|
||||
players: make(map[string]*models.Player),
|
||||
deck: make(map[string][]models.Card),
|
||||
discard: make(map[string][]models.Card),
|
||||
cl: NewCardLogic(),
|
||||
func NewGameManager(rdb *redis.Client) *GameManager {
|
||||
gm := &GameManager{
|
||||
rdb: rdb,
|
||||
cl: NewCardLogic(),
|
||||
stopCleanup: make(chan struct{}),
|
||||
}
|
||||
go gm.cleanupLoop()
|
||||
return gm
|
||||
}
|
||||
|
||||
func (gm *GameManager) CreateRoom(playerName string, maxPlayers int) (*models.Room, *models.Player) {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
ctx := context.Background()
|
||||
roomID := generateID()
|
||||
playerID := generateID()
|
||||
|
||||
player := &models.Player{ID: playerID, Name: playerName, IsOnline: true}
|
||||
room := &models.Room{
|
||||
ID: roomID,
|
||||
@@ -262,10 +281,9 @@ func (gm *GameManager) CreateRoom(playerName string, maxPlayers int) (*models.Ro
|
||||
deck := gm.cl.CreateDeck()
|
||||
gm.cl.Shuffle(deck)
|
||||
|
||||
gm.rooms[roomID] = room
|
||||
gm.players[playerID] = player
|
||||
gm.deck[roomID] = deck
|
||||
gm.discard[roomID] = make([]models.Card, 0)
|
||||
gm.saveRoom(ctx, room)
|
||||
gm.saveDeck(ctx, roomID, deck)
|
||||
gm.saveDiscard(ctx, roomID, []models.Card{})
|
||||
|
||||
return room, player
|
||||
}
|
||||
@@ -274,8 +292,9 @@ func (gm *GameManager) JoinRoom(roomID, playerName string) (*models.Room, *model
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
room, ok := gm.rooms[roomID]
|
||||
if !ok {
|
||||
ctx := context.Background()
|
||||
room, err := gm.loadRoom(ctx, roomID)
|
||||
if err != nil {
|
||||
return nil, nil, ErrRoomNotFound
|
||||
}
|
||||
if len(room.Players) >= room.MaxPlayers {
|
||||
@@ -288,8 +307,8 @@ func (gm *GameManager) JoinRoom(roomID, playerName string) (*models.Room, *model
|
||||
playerID := generateID()
|
||||
player := &models.Player{ID: playerID, Name: playerName, IsOnline: true}
|
||||
room.Players = append(room.Players, player)
|
||||
gm.players[playerID] = player
|
||||
|
||||
gm.saveRoom(ctx, room)
|
||||
return room, player, nil
|
||||
}
|
||||
|
||||
@@ -297,19 +316,20 @@ func (gm *GameManager) LeaveRoom(roomID, playerID string) error {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
room, ok := gm.rooms[roomID]
|
||||
if !ok {
|
||||
ctx := context.Background()
|
||||
room, err := gm.loadRoom(ctx, roomID)
|
||||
if err != nil {
|
||||
return ErrRoomNotFound
|
||||
}
|
||||
|
||||
for i, p := range room.Players {
|
||||
if p.ID == playerID {
|
||||
room.Players = append(room.Players[:i], room.Players[i+1:]...)
|
||||
delete(gm.players, playerID)
|
||||
|
||||
if len(room.Players) == 0 {
|
||||
delete(gm.rooms, roomID)
|
||||
delete(gm.deck, roomID)
|
||||
delete(gm.discard, roomID)
|
||||
gm.deleteRoomData(ctx, roomID)
|
||||
} else {
|
||||
gm.saveRoom(ctx, room)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -317,25 +337,68 @@ func (gm *GameManager) LeaveRoom(roomID, playerID string) error {
|
||||
return ErrPlayerNotFound
|
||||
}
|
||||
|
||||
func (gm *GameManager) LeaveRoomWithTTL(roomID, playerID string) error {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
ctx := context.Background()
|
||||
room, err := gm.loadRoom(ctx, roomID)
|
||||
if err != nil {
|
||||
return ErrRoomNotFound
|
||||
}
|
||||
|
||||
for _, p := range room.Players {
|
||||
if p.ID == playerID {
|
||||
p.IsOnline = false
|
||||
gm.saveRoom(ctx, room)
|
||||
gm.rdb.Set(ctx, redis.PlayerTTLKey(playerID), time.Now().Add(redis.PlayerTTL).Unix(), redis.PlayerTTL)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrPlayerNotFound
|
||||
}
|
||||
|
||||
func (gm *GameManager) MarkPlayerOnline(roomID, playerID string) {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
ctx := context.Background()
|
||||
room, err := gm.loadRoom(ctx, roomID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range room.Players {
|
||||
if p.ID == playerID {
|
||||
p.IsOnline = true
|
||||
gm.saveRoom(ctx, room)
|
||||
gm.rdb.Delete(ctx, redis.PlayerTTLKey(playerID))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gm *GameManager) SetReady(roomID, playerID string, ready bool) error {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
room, ok := gm.rooms[roomID]
|
||||
if !ok {
|
||||
ctx := context.Background()
|
||||
room, err := gm.loadRoom(ctx, roomID)
|
||||
if err != nil {
|
||||
return ErrRoomNotFound
|
||||
}
|
||||
|
||||
|
||||
if room.State == models.RoomStatePlaying {
|
||||
return ErrGameStarted
|
||||
}
|
||||
|
||||
|
||||
for _, p := range room.Players {
|
||||
if p.ID == playerID {
|
||||
p.IsReady = ready
|
||||
if room.State == models.RoomStateFinished {
|
||||
room.State = models.RoomStateWaiting
|
||||
}
|
||||
gm.saveRoom(ctx, room)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -346,8 +409,9 @@ func (gm *GameManager) StartGame(roomID string) error {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
room, ok := gm.rooms[roomID]
|
||||
if !ok {
|
||||
ctx := context.Background()
|
||||
room, err := gm.loadRoom(ctx, roomID)
|
||||
if err != nil {
|
||||
return ErrRoomNotFound
|
||||
}
|
||||
if len(room.Players) < 2 {
|
||||
@@ -357,6 +421,8 @@ func (gm *GameManager) StartGame(roomID string) error {
|
||||
return ErrGameStarted
|
||||
}
|
||||
|
||||
lastWinner := room.LastWinner
|
||||
|
||||
for _, p := range room.Players {
|
||||
p.Cards = make([]models.Card, 0)
|
||||
p.CardCount = 0
|
||||
@@ -365,29 +431,40 @@ func (gm *GameManager) StartGame(roomID string) error {
|
||||
|
||||
deck := gm.cl.CreateDeck()
|
||||
gm.cl.Shuffle(deck)
|
||||
gm.deck[roomID] = deck
|
||||
gm.discard[roomID] = make([]models.Card, 0)
|
||||
gm.saveDeck(ctx, roomID, deck)
|
||||
gm.saveDiscard(ctx, roomID, []models.Card{})
|
||||
|
||||
room.State = models.RoomStatePlaying
|
||||
room.RoundCount = 1
|
||||
room.CurrentTurn = 0
|
||||
room.LastPlay = nil
|
||||
room.LastWinner = ""
|
||||
|
||||
gm.dealCards(roomID, 5)
|
||||
if lastWinner != "" {
|
||||
for i, p := range room.Players {
|
||||
if p.ID == lastWinner {
|
||||
room.CurrentTurn = i
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
room.CurrentTurn = 0
|
||||
}
|
||||
|
||||
gm.dealCards(room, 5)
|
||||
gm.saveRoom(ctx, room)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gm *GameManager) dealCards(roomID string, count int) {
|
||||
room := gm.rooms[roomID]
|
||||
deck := gm.deck[roomID]
|
||||
func (gm *GameManager) dealCards(room *models.Room, count int) {
|
||||
ctx := context.Background()
|
||||
deck, _ := gm.loadDeck(ctx, room.ID)
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
for _, p := range room.Players {
|
||||
if len(deck) == 0 {
|
||||
deck = gm.discard[roomID]
|
||||
deck, _ = gm.loadDiscard(ctx, room.ID)
|
||||
gm.cl.Shuffle(deck)
|
||||
gm.discard[roomID] = make([]models.Card, 0)
|
||||
gm.saveDiscard(ctx, room.ID, []models.Card{})
|
||||
}
|
||||
if len(deck) > 0 {
|
||||
p.Cards = append(p.Cards, deck[0])
|
||||
@@ -395,7 +472,8 @@ func (gm *GameManager) dealCards(roomID string, count int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
gm.deck[roomID] = deck
|
||||
|
||||
gm.saveDeck(ctx, room.ID, deck)
|
||||
|
||||
for _, p := range room.Players {
|
||||
gm.cl.SortCards(p.Cards)
|
||||
@@ -407,8 +485,9 @@ func (gm *GameManager) PlayCards(roomID, playerID string, cards []models.Card) e
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
room, ok := gm.rooms[roomID]
|
||||
if !ok {
|
||||
ctx := context.Background()
|
||||
room, err := gm.loadRoom(ctx, roomID)
|
||||
if err != nil {
|
||||
return ErrRoomNotFound
|
||||
}
|
||||
if room.State != models.RoomStatePlaying {
|
||||
@@ -439,15 +518,20 @@ func (gm *GameManager) PlayCards(roomID, playerID string, cards []models.Card) e
|
||||
Cards: cards,
|
||||
CardType: cardType,
|
||||
}
|
||||
gm.discard[roomID] = append(gm.discard[roomID], cards...)
|
||||
|
||||
discard, _ := gm.loadDiscard(ctx, roomID)
|
||||
discard = append(discard, cards...)
|
||||
gm.saveDiscard(ctx, roomID, discard)
|
||||
|
||||
if len(player.Cards) == 0 {
|
||||
room.LastWinner = playerID
|
||||
room.State = models.RoomStateFinished
|
||||
return nil
|
||||
gm.rdb.Set(ctx, redis.RoomTTLKey(roomID), time.Now().Add(redis.RoomTTL).Unix(), redis.RoomTTL)
|
||||
} else {
|
||||
room.CurrentTurn = (room.CurrentTurn + 1) % len(room.Players)
|
||||
}
|
||||
|
||||
gm.nextTurn(room)
|
||||
gm.saveRoom(ctx, room)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -455,8 +539,9 @@ func (gm *GameManager) Pass(roomID, playerID string) error {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
room, ok := gm.rooms[roomID]
|
||||
if !ok {
|
||||
ctx := context.Background()
|
||||
room, err := gm.loadRoom(ctx, roomID)
|
||||
if err != nil {
|
||||
return ErrRoomNotFound
|
||||
}
|
||||
if room.State != models.RoomStatePlaying {
|
||||
@@ -469,18 +554,67 @@ func (gm *GameManager) Pass(roomID, playerID string) error {
|
||||
return ErrCannotPass
|
||||
}
|
||||
|
||||
gm.nextTurn(room)
|
||||
room.CurrentTurn = (room.CurrentTurn + 1) % len(room.Players)
|
||||
|
||||
// 一轮结束:回到最后出牌者,发牌并让他先出
|
||||
if room.Players[room.CurrentTurn].ID == room.LastPlay.PlayerID {
|
||||
winnerID := room.LastPlay.PlayerID
|
||||
room.LastPlay = nil
|
||||
gm.dealCards(roomID, 1)
|
||||
room.RoundCount++
|
||||
gm.dealCards(room, 1)
|
||||
|
||||
for i, p := range room.Players {
|
||||
if p.ID == winnerID {
|
||||
room.CurrentTurn = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gm.saveRoom(ctx, room)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gm *GameManager) nextTurn(room *models.Room) {
|
||||
room.CurrentTurn = (room.CurrentTurn + 1) % len(room.Players)
|
||||
func (gm *GameManager) GetRoom(roomID string) (*models.Room, error) {
|
||||
gm.mu.RLock()
|
||||
defer gm.mu.RUnlock()
|
||||
return gm.loadRoom(context.Background(), roomID)
|
||||
}
|
||||
|
||||
func (gm *GameManager) GetRoomState(roomID, playerID string) *models.Room {
|
||||
gm.mu.RLock()
|
||||
defer gm.mu.RUnlock()
|
||||
|
||||
room, err := gm.loadRoom(context.Background(), roomID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
players := make([]*models.Player, len(room.Players))
|
||||
for i, p := range room.Players {
|
||||
pp := &models.Player{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
IsReady: p.IsReady,
|
||||
IsOnline: p.IsOnline,
|
||||
CardCount: p.CardCount,
|
||||
}
|
||||
if p.ID == playerID {
|
||||
pp.Cards = p.Cards
|
||||
}
|
||||
players[i] = pp
|
||||
}
|
||||
|
||||
return &models.Room{
|
||||
ID: room.ID,
|
||||
Players: players,
|
||||
CurrentTurn: room.CurrentTurn,
|
||||
State: room.State,
|
||||
LastPlay: room.LastPlay,
|
||||
LastWinner: room.LastWinner,
|
||||
RoundCount: room.RoundCount,
|
||||
MaxPlayers: room.MaxPlayers,
|
||||
}
|
||||
}
|
||||
|
||||
func (gm *GameManager) hasCards(player *models.Player, cards []models.Card) bool {
|
||||
@@ -516,48 +650,118 @@ func (gm *GameManager) removeCards(player *models.Player, cards []models.Card) {
|
||||
player.Cards = newCards
|
||||
}
|
||||
|
||||
func (gm *GameManager) GetRoom(roomID string) (*models.Room, error) {
|
||||
gm.mu.RLock()
|
||||
defer gm.mu.RUnlock()
|
||||
room, ok := gm.rooms[roomID]
|
||||
if !ok {
|
||||
return nil, ErrRoomNotFound
|
||||
func (gm *GameManager) cleanupLoop() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
gm.cleanupExpired()
|
||||
case <-gm.stopCleanup:
|
||||
return
|
||||
}
|
||||
}
|
||||
return room, nil
|
||||
}
|
||||
|
||||
func (gm *GameManager) GetRoomState(roomID, playerID string) *models.Room {
|
||||
gm.mu.RLock()
|
||||
defer gm.mu.RUnlock()
|
||||
func (gm *GameManager) cleanupExpired() {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
|
||||
room, ok := gm.rooms[roomID]
|
||||
if !ok {
|
||||
ctx := context.Background()
|
||||
|
||||
gm.rdb.ScanKeys(ctx, "room:*:ttl", func(key string) error {
|
||||
var expiryTime int64
|
||||
if err := gm.rdb.Get(ctx, key, &expiryTime); err != nil {
|
||||
return nil
|
||||
}
|
||||
if time.Now().Unix() > expiryTime {
|
||||
roomID := key[len("room:") : len(key)-len(":ttl")]
|
||||
gm.deleteRoomData(ctx, roomID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
players := make([]*models.Player, len(room.Players))
|
||||
for i, p := range room.Players {
|
||||
pp := &models.Player{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
IsReady: p.IsReady,
|
||||
IsOnline: p.IsOnline,
|
||||
CardCount: p.CardCount,
|
||||
gm.rdb.ScanKeys(ctx, "player:*:ttl", func(key string) error {
|
||||
var expiryTime int64
|
||||
if err := gm.rdb.Get(ctx, key, &expiryTime); err != nil {
|
||||
return nil
|
||||
}
|
||||
if p.ID == playerID {
|
||||
pp.Cards = p.Cards
|
||||
if time.Now().Unix() > expiryTime {
|
||||
playerID := key[len("player:") : len(key)-len(":ttl")]
|
||||
gm.rdb.Delete(ctx, redis.PlayerKey(playerID), redis.PlayerTTLKey(playerID))
|
||||
}
|
||||
players[i] = pp
|
||||
}
|
||||
|
||||
return &models.Room{
|
||||
ID: room.ID,
|
||||
Players: players,
|
||||
CurrentTurn: room.CurrentTurn,
|
||||
State: room.State,
|
||||
LastPlay: room.LastPlay,
|
||||
LastWinner: room.LastWinner,
|
||||
RoundCount: room.RoundCount,
|
||||
MaxPlayers: room.MaxPlayers,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gm *GameManager) Stop() {
|
||||
gm.stopOnce.Do(func() {
|
||||
close(gm.stopCleanup)
|
||||
})
|
||||
}
|
||||
|
||||
func (gm *GameManager) saveRoom(ctx context.Context, room *models.Room) {
|
||||
gm.rdb.Set(ctx, redis.RoomKey(room.ID), room, redis.RoomTTL)
|
||||
}
|
||||
|
||||
func (gm *GameManager) loadRoom(ctx context.Context, roomID string) (*models.Room, error) {
|
||||
var room models.Room
|
||||
if err := gm.rdb.Get(ctx, redis.RoomKey(roomID), &room); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &room, nil
|
||||
}
|
||||
|
||||
func (gm *GameManager) saveDeck(ctx context.Context, roomID string, deck []models.Card) {
|
||||
gm.rdb.Set(ctx, redis.RoomDeckKey(roomID), deck, redis.RoomTTL)
|
||||
}
|
||||
|
||||
func (gm *GameManager) loadDeck(ctx context.Context, roomID string) ([]models.Card, error) {
|
||||
var deck []models.Card
|
||||
if err := gm.rdb.Get(ctx, redis.RoomDeckKey(roomID), &deck); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return deck, nil
|
||||
}
|
||||
|
||||
func (gm *GameManager) saveDiscard(ctx context.Context, roomID string, discard []models.Card) {
|
||||
gm.rdb.Set(ctx, redis.RoomDiscardKey(roomID), discard, redis.RoomTTL)
|
||||
}
|
||||
|
||||
func (gm *GameManager) loadDiscard(ctx context.Context, roomID string) ([]models.Card, error) {
|
||||
var discard []models.Card
|
||||
if err := gm.rdb.Get(ctx, redis.RoomDiscardKey(roomID), &discard); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return discard, nil
|
||||
}
|
||||
|
||||
func (gm *GameManager) deleteRoomData(ctx context.Context, roomID string) {
|
||||
room, err := gm.loadRoom(ctx, roomID)
|
||||
if err == nil {
|
||||
for _, p := range room.Players {
|
||||
gm.rdb.Delete(ctx, redis.PlayerKey(p.ID), redis.PlayerTTLKey(p.ID))
|
||||
}
|
||||
}
|
||||
gm.rdb.Delete(ctx,
|
||||
redis.RoomKey(roomID),
|
||||
redis.RoomDeckKey(roomID),
|
||||
redis.RoomDiscardKey(roomID),
|
||||
redis.RoomTTLKey(roomID),
|
||||
)
|
||||
}
|
||||
|
||||
func generateID() string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
b := make([]byte, 8)
|
||||
for i := range b {
|
||||
b[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func cardKey(c models.Card) string {
|
||||
data, _ := json.Marshal(c)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
252
app/internal/redis/redis.go
Normal file
252
app/internal/redis/redis.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Addr string
|
||||
Password string
|
||||
DB int
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
rdb *redis.Client
|
||||
}
|
||||
|
||||
func NewClient(cfg Config) (*Client, error) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: cfg.Addr,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := rdb.Ping(ctx).Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to redis: %w", err)
|
||||
}
|
||||
|
||||
return &Client{rdb: rdb}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
return c.rdb.Close()
|
||||
}
|
||||
|
||||
func (c *Client) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal value: %w", err)
|
||||
}
|
||||
return c.rdb.Set(ctx, key, data, ttl).Err()
|
||||
}
|
||||
|
||||
func (c *Client) Get(ctx context.Context, key string, dest interface{}) error {
|
||||
data, err := c.rdb.Get(ctx, key).Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(data, dest); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal value: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Exists(ctx context.Context, key string) (bool, error) {
|
||||
result, err := c.rdb.Exists(ctx, key).Result()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result > 0, nil
|
||||
}
|
||||
|
||||
func (c *Client) Delete(ctx context.Context, keys ...string) error {
|
||||
return c.rdb.Del(ctx, keys...).Err()
|
||||
}
|
||||
|
||||
func (c *Client) SetTTL(ctx context.Context, key string, ttl time.Duration) error {
|
||||
return c.rdb.Expire(ctx, key, ttl).Err()
|
||||
}
|
||||
|
||||
func (c *Client) GetTTL(ctx context.Context, key string) (time.Duration, error) {
|
||||
return c.rdb.TTL(ctx, key).Result()
|
||||
}
|
||||
|
||||
func (c *Client) LPush(ctx context.Context, key string, values ...interface{}) error {
|
||||
data := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal list value: %w", err)
|
||||
}
|
||||
data[i] = b
|
||||
}
|
||||
return c.rdb.LPush(ctx, key, data...).Err()
|
||||
}
|
||||
|
||||
func (c *Client) RPush(ctx context.Context, key string, values ...interface{}) error {
|
||||
data := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal list value: %w", err)
|
||||
}
|
||||
data[i] = b
|
||||
}
|
||||
return c.rdb.RPush(ctx, key, data...).Err()
|
||||
}
|
||||
|
||||
func (c *Client) LRange(ctx context.Context, key string, start, stop int64, dest interface{}) error {
|
||||
results, err := c.rdb.LRange(ctx, key, start, stop).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(results)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal results: %w", err)
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, dest)
|
||||
}
|
||||
|
||||
func (c *Client) LLen(ctx context.Context, key string) (int64, error) {
|
||||
return c.rdb.LLen(ctx, key).Result()
|
||||
}
|
||||
|
||||
func (c *Client) LPop(ctx context.Context, key string) (string, error) {
|
||||
return c.rdb.LPop(ctx, key).Result()
|
||||
}
|
||||
|
||||
func (c *Client) LTrim(ctx context.Context, key string, start, stop int64) error {
|
||||
return c.rdb.LTrim(ctx, key, start, stop).Err()
|
||||
}
|
||||
|
||||
func (c *Client) LSet(ctx context.Context, key string, index int64, value interface{}) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal value: %w", err)
|
||||
}
|
||||
return c.rdb.LSet(ctx, key, index, data).Err()
|
||||
}
|
||||
|
||||
func (c *Client) SAdd(ctx context.Context, key string, members ...interface{}) error {
|
||||
data := make([]interface{}, len(members))
|
||||
for i, m := range members {
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal set member: %w", err)
|
||||
}
|
||||
data[i] = b
|
||||
}
|
||||
return c.rdb.SAdd(ctx, key, data...).Err()
|
||||
}
|
||||
|
||||
func (c *Client) SMembers(ctx context.Context, key string) ([]string, error) {
|
||||
return c.rdb.SMembers(ctx, key).Result()
|
||||
}
|
||||
|
||||
func (c *Client) SRem(ctx context.Context, key string, members ...interface{}) error {
|
||||
data := make([]interface{}, len(members))
|
||||
for i, m := range members {
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal set member: %w", err)
|
||||
}
|
||||
data[i] = b
|
||||
}
|
||||
return c.rdb.SRem(ctx, key, data...).Err()
|
||||
}
|
||||
|
||||
func (c *Client) HSet(ctx context.Context, key string, field string, value interface{}) error {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal hash value: %w", err)
|
||||
}
|
||||
return c.rdb.HSet(ctx, key, field, data).Err()
|
||||
}
|
||||
|
||||
func (c *Client) HGet(ctx context.Context, key, field string, dest interface{}) error {
|
||||
data, err := c.rdb.HGet(ctx, key, field).Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, dest)
|
||||
}
|
||||
|
||||
func (c *Client) HDel(ctx context.Context, key string, fields ...string) error {
|
||||
return c.rdb.HDel(ctx, key, fields...).Err()
|
||||
}
|
||||
|
||||
func (c *Client) HGetAll(ctx context.Context, key string) (map[string]string, error) {
|
||||
return c.rdb.HGetAll(ctx, key).Result()
|
||||
}
|
||||
|
||||
func (c *Client) HExists(ctx context.Context, key, field string) (bool, error) {
|
||||
return c.rdb.HExists(ctx, key, field).Result()
|
||||
}
|
||||
|
||||
func (c *Client) Keys(ctx context.Context, pattern string) ([]string, error) {
|
||||
return c.rdb.Keys(ctx, pattern).Result()
|
||||
}
|
||||
|
||||
func (c *Client) ScanKeys(ctx context.Context, pattern string, callback func(key string) error) error {
|
||||
var cursor uint64
|
||||
for {
|
||||
var keys []string
|
||||
var err error
|
||||
keys, cursor, err = c.rdb.Scan(ctx, cursor, pattern, 100).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range keys {
|
||||
if err := callback(key); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
RoomTTL = 30 * time.Minute
|
||||
PlayerTTL = 10 * time.Minute
|
||||
)
|
||||
|
||||
func RoomKey(roomID string) string {
|
||||
return "room:" + roomID
|
||||
}
|
||||
|
||||
func PlayerKey(playerID string) string {
|
||||
return "player:" + playerID
|
||||
}
|
||||
|
||||
func RoomPlayersKey(roomID string) string {
|
||||
return "room:" + roomID + ":players"
|
||||
}
|
||||
|
||||
func RoomDeckKey(roomID string) string {
|
||||
return "room:" + roomID + ":deck"
|
||||
}
|
||||
|
||||
func RoomDiscardKey(roomID string) string {
|
||||
return "room:" + roomID + ":discard"
|
||||
}
|
||||
|
||||
func RoomTTLKey(roomID string) string {
|
||||
return "room:" + roomID + ":ttl"
|
||||
}
|
||||
|
||||
func PlayerTTLKey(playerID string) string {
|
||||
return "player:" + playerID + ":ttl"
|
||||
}
|
||||
@@ -20,11 +20,13 @@ var upgrader = websocket.Upgrader{
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ID string
|
||||
RoomID string
|
||||
Conn *websocket.Conn
|
||||
Send chan []byte
|
||||
Hub *Hub
|
||||
ID string
|
||||
RoomID string
|
||||
Conn *websocket.Conn
|
||||
Send chan []byte
|
||||
Hub *Hub
|
||||
done chan struct{}
|
||||
doneOnce sync.Once
|
||||
}
|
||||
|
||||
type Hub struct {
|
||||
@@ -33,6 +35,8 @@ type Hub struct {
|
||||
Unregister chan *Client
|
||||
GameMgr *game.GameManager
|
||||
mu sync.RWMutex
|
||||
stopChan chan struct{}
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func NewHub(gameMgr *game.GameManager) *Hub {
|
||||
@@ -41,6 +45,7 @@ func NewHub(gameMgr *game.GameManager) *Hub {
|
||||
Register: make(chan *Client, 64),
|
||||
Unregister: make(chan *Client, 64),
|
||||
GameMgr: gameMgr,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,39 +54,71 @@ func (h *Hub) Run() {
|
||||
select {
|
||||
case c := <-h.Register:
|
||||
h.mu.Lock()
|
||||
if old, exists := h.Clients[c.ID]; exists {
|
||||
old.close()
|
||||
delete(h.Clients, old.ID)
|
||||
}
|
||||
h.Clients[c.ID] = c
|
||||
h.mu.Unlock()
|
||||
|
||||
case c := <-h.Unregister:
|
||||
h.mu.Lock()
|
||||
if _, ok := h.Clients[c.ID]; ok {
|
||||
delete(h.Clients, c.ID)
|
||||
close(c.Send)
|
||||
c.close()
|
||||
}
|
||||
h.mu.Unlock()
|
||||
if c.RoomID != "" {
|
||||
h.GameMgr.LeaveRoom(c.RoomID, c.ID)
|
||||
h.GameMgr.LeaveRoomWithTTL(c.RoomID, c.ID)
|
||||
h.broadcastRoomState(c.RoomID)
|
||||
}
|
||||
|
||||
case <-h.stopChan:
|
||||
h.mu.Lock()
|
||||
for _, c := range h.Clients {
|
||||
c.close()
|
||||
}
|
||||
h.Clients = make(map[string]*Client)
|
||||
h.mu.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) Stop() {
|
||||
h.stopOnce.Do(func() {
|
||||
close(h.stopChan)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) close() {
|
||||
c.doneOnce.Do(func() {
|
||||
close(c.done)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) ReadPump() {
|
||||
defer func() {
|
||||
c.Hub.Unregister <- c
|
||||
c.Conn.Close()
|
||||
}()
|
||||
|
||||
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
c.Conn.SetPongHandler(func(string) error {
|
||||
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
return nil
|
||||
})
|
||||
|
||||
for {
|
||||
_, data, err := c.Conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
select {
|
||||
case <-c.done:
|
||||
return
|
||||
default:
|
||||
_, data, err := c.Conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.handleMessage(data)
|
||||
}
|
||||
c.handleMessage(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,8 +128,13 @@ func (c *Client) WritePump() {
|
||||
ticker.Stop()
|
||||
c.Conn.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.done:
|
||||
c.Conn.WriteMessage(websocket.CloseMessage, nil)
|
||||
return
|
||||
|
||||
case msg, ok := <-c.Send:
|
||||
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
if !ok {
|
||||
@@ -102,6 +144,7 @@ func (c *Client) WritePump() {
|
||||
if err := c.Conn.WriteMessage(websocket.TextMessage, msg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
@@ -210,6 +253,7 @@ func (c *Client) sendError(msg string) {
|
||||
})
|
||||
select {
|
||||
case c.Send <- data:
|
||||
case <-c.done:
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -222,6 +266,7 @@ func (h *Hub) broadcastToRoom(roomID string, msg models.Message) {
|
||||
if c.RoomID == roomID {
|
||||
select {
|
||||
case c.Send <- data:
|
||||
case <-c.done:
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -248,6 +293,7 @@ func (h *Hub) broadcastRoomState(roomID string) {
|
||||
})
|
||||
select {
|
||||
case c.Send <- data:
|
||||
case <-c.done:
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -274,15 +320,15 @@ func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
||||
Conn: conn,
|
||||
Send: make(chan []byte, 256),
|
||||
Hub: hub,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
hub.mu.Lock()
|
||||
hub.Clients[client.ID] = client
|
||||
hub.mu.Unlock()
|
||||
hub.GameMgr.MarkPlayerOnline(roomID, playerID)
|
||||
|
||||
go client.WritePump()
|
||||
go client.ReadPump()
|
||||
hub.Register <- client
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
go client.WritePump()
|
||||
go client.ReadPump()
|
||||
hub.broadcastRoomState(roomID)
|
||||
}
|
||||
|
||||
21
compose.yaml
21
compose.yaml
@@ -4,6 +4,10 @@ services:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- REDIS_ADDR=redis:6379
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
@@ -17,3 +21,20 @@ services:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: >
|
||||
redis-server
|
||||
--maxmemory 128mb
|
||||
--maxmemory-policy volatile-ttl
|
||||
--save ""
|
||||
--appendonly no
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
|
||||
Reference in New Issue
Block a user