1. 为什么选择Golang操作Redis和MySQL
在当今的互联网应用开发中,数据存储和缓存是两大核心需求。Golang凭借其出色的并发性能和简洁的语法,成为构建高性能后端服务的首选语言之一。而Redis作为内存数据库,MySQL作为关系型数据库,在实际项目中经常需要配合使用。
我最近在一个电商促销系统项目中,就遇到了需要同时操作Redis和MySQL的场景。系统需要在秒杀活动期间处理高并发请求,同时保证数据一致性。通过Golang的 database/sql 包和 redigo 库,我们实现了稳定可靠的数据库操作层。
2. 环境准备与依赖安装
2.1 安装Golang开发环境
首先确保你的系统已经安装了Golang 1.16或更高版本。可以通过以下命令检查:
go version
如果未安装,可以从 Golang官网 下载对应平台的安装包。安装完成后,设置GOPATH环境变量:
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
2.2 安装MySQL和Redis
对于MySQL,推荐使用官方提供的社区版。在Ubuntu系统上可以这样安装:
sudo apt update
sudo apt install mysql-server
sudo mysql_secure_installation
Redis的安装同样简单:
sudo apt install redis-server
sudo systemctl enable redis-server
2.3 安装必要的Golang库
我们需要以下几个核心库来实现Redis和MySQL操作:
go get github.com/go-sql-driver/mysql
go get github.com/gomodule/redigo/redis
这些库都是经过社区验证的成熟解决方案。 go-sql-driver/mysql 是MySQL官方推荐的Golang驱动,而 redigo 则是Redis的轻量级客户端。
3. MySQL数据库操作实战
3.1 初始化数据库连接
在Golang中连接MySQL数据库,我们需要先创建一个连接池:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func initDB() (*sql.DB, error) {
db, err := sql.Open("mysql", "username:password@tcp(127.0.0.1:3306)/dbname?parseTime=true")
if err != nil {
return nil, err
}
// 设置连接池参数
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
return db, nil
}
这里有几个关键点需要注意:
- 连接字符串格式为
username:password@protocol(address)/dbname?param=value -
parseTime=true参数确保能正确处理MySQL的日期时间类型 - 连接池参数需要根据实际负载调整
3.2 执行CRUD操作
让我们看一个完整的用户管理示例:
type User struct {
ID int64
Username string
Email string
CreatedAt time.Time
}
// 创建用户
func createUser(db *sql.DB, user *User) error {
result, err := db.Exec(
"INSERT INTO users (username, email) VALUES (?, ?)",
user.Username, user.Email,
)
if err != nil {
return err
}
user.ID, err = result.LastInsertId()
return err
}
// 查询用户
func getUser(db *sql.DB, id int64) (*User, error) {
user := &User{}
err := db.QueryRow(
"SELECT id, username, email, created_at FROM users WHERE id = ?", id,
).Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
if err != nil {
return nil, err
}
return user, nil
}
在实际项目中,我建议使用 sqlx 库来简化结构体映射:
go get github.com/jmoiron/sqlx
使用sqlx后,查询可以简化为:
func getUsers(db *sqlx.DB) ([]User, error) {
var users []User
err := db.Select(&users, "SELECT * FROM users")
return users, err
}
4. Redis操作实战
4.1 初始化Redis连接池
与MySQL类似,Redis也需要连接池管理:
import "github.com/gomodule/redigo/redis"
func initRedisPool() *redis.Pool {
return &redis.Pool{
MaxIdle: 10,
MaxActive: 100,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
return nil, err
}
return c, nil
},
}
}
4.2 常用Redis操作示例
Redis支持多种数据结构,以下是几种常见操作的示例:
// 字符串操作
func setValue(pool *redis.Pool, key, value string) error {
conn := pool.Get()
defer conn.Close()
_, err := conn.Do("SET", key, value)
return err
}
// 哈希表操作
func setHash(pool *redis.Pool, key, field, value string) error {
conn := pool.Get()
defer conn.Close()
_, err := conn.Do("HSET", key, field, value)
return err
}
// 设置过期时间
func setWithExpire(pool *redis.Pool, key, value string, expireSec int) error {
conn := pool.Get()
defer conn.Close()
_, err := conn.Do("SETEX", key, expireSec, value)
return err
}
4.3 使用Pipeline提升性能
当需要执行多个Redis命令时,使用Pipeline可以显著减少网络往返时间:
func batchIncr(pool *redis.Pool, keys []string) error {
conn := pool.Get()
defer conn.Close()
for _, key := range keys {
conn.Send("INCR", key)
}
return conn.Flush()
}
5. 实战中的经验与陷阱
5.1 连接泄露问题
无论是MySQL还是Redis,连接泄露都是常见问题。务必记得在使用后关闭连接:
rows, err := db.Query("SELECT * FROM users")
if err != nil {
return err
}
defer rows.Close() // 重要!
对于Redis连接,同样需要defer Close():
conn := pool.Get()
defer conn.Close()
5.2 事务处理
在需要保证数据一致性的场景,正确使用事务至关重要:
// MySQL事务示例
func transferMoney(db *sql.DB, from, to int64, amount float64) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
// 扣减转出账户余额
if _, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from); err != nil {
return err
}
// 增加转入账户余额
if _, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to); err != nil {
return err
}
return tx.Commit()
}
Redis事务使用MULTI/EXEC:
func redisTransaction(pool *redis.Pool) error {
conn := pool.Get()
defer conn.Close()
conn.Send("MULTI")
conn.Send("INCR", "counter1")
conn.Send("INCR", "counter2")
_, err := conn.Do("EXEC")
return err
}
5.3 连接池调优
连接池参数需要根据实际负载调整。过小的连接池会导致请求排队,过大的连接池会浪费资源。以下是一些经验值:
-
MySQL连接池:
- MaxOpenConns: CPU核心数的2-3倍
- MaxIdleConns: 与MaxOpenConns相同
- ConnMaxLifetime: 5-30分钟
-
Redis连接池:
- MaxActive: 根据并发请求量调整,通常100-500
- MaxIdle: 保持20-50个空闲连接
6. 性能优化技巧
6.1 批量操作优化
对于大量数据操作,批量处理可以显著提升性能:
// MySQL批量插入
func batchInsertUsers(db *sql.DB, users []User) error {
valueStrings := make([]string, 0, len(users))
valueArgs := make([]interface{}, 0, len(users)*2)
for _, user := range users {
valueStrings = append(valueStrings, "(?, ?)")
valueArgs = append(valueArgs, user.Username)
valueArgs = append(valueArgs, user.Email)
}
stmt := fmt.Sprintf("INSERT INTO users (username, email) VALUES %s",
strings.Join(valueStrings, ","))
_, err := db.Exec(stmt, valueArgs...)
return err
}
6.2 使用连接复用
对于高频调用的服务,可以考虑复用预处理语句:
var (
insertStmt *sql.Stmt
)
func init() {
var err error
insertStmt, err = db.Prepare("INSERT INTO users (username, email) VALUES (?, ?)")
if err != nil {
log.Fatal(err)
}
}
func createUser(user *User) error {
result, err := insertStmt.Exec(user.Username, user.Email)
if err != nil {
return err
}
user.ID, err = result.LastInsertId()
return err
}
6.3 Redis Lua脚本
对于复杂操作,使用Lua脚本可以减少网络往返并保证原子性:
const script = `
local current = redis.call('GET', KEYS[1])
if current then
return redis.call('INCRBY', KEYS[1], ARGV[1])
else
return nil
end`
func incrementIfExists(pool *redis.Pool, key string, increment int) (interface{}, error) {
conn := pool.Get()
defer conn.Close()
script := redis.NewScript(1, script)
return script.Do(conn, key, increment)
}
7. 实际项目中的架构设计
在一个真实的微服务架构中,我通常会这样组织数据库访问层:
project/
├── internal/
│ ├── config/ # 配置管理
│ ├── model/ # 数据模型
│ ├── repository/ # 数据访问层
│ │ ├── mysql/ # MySQL实现
│ │ ├── redis/ # Redis实现
│ │ └── interface.go # 接口定义
│ └── service/ # 业务逻辑层
这种分层架构的好处是:
- 业务逻辑与数据访问解耦
- 可以轻松切换数据库实现
- 便于单元测试
例如,定义一个通用的缓存接口:
type CacheRepository interface {
Get(key string) (string, error)
Set(key, value string, expire time.Duration) error
Delete(key string) error
// 其他方法...
}
然后分别实现Redis版本和Mock版本用于测试。
8. 测试策略
8.1 单元测试
对于数据库相关代码,可以使用 sqlmock 进行测试:
go get github.com/DATA-DOG/go-sqlmock
测试示例:
func TestGetUser(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected", err)
}
defer db.Close()
rows := sqlmock.NewRows([]string{"id", "username", "email", "created_at"}).
AddRow(1, "testuser", "test@example.com", time.Now())
mock.ExpectQuery("^SELECT (.+) FROM users WHERE id = ?$").
WithArgs(1).
WillReturnRows(rows)
user, err := getUser(db, 1)
if err != nil {
t.Errorf("error was not expected: %s", err)
}
if user.Username != "testuser" {
t.Errorf("expected username testuser, got %s", user.Username)
}
}
8.2 集成测试
对于需要真实数据库的测试,可以使用Docker启动临时实例:
func TestMySQLIntegration(t *testing.T) {
pool, err := dockertest.NewPool("")
if err != nil {
t.Fatalf("Could not connect to docker: %s", err)
}
resource, err := pool.Run("mysql", "5.7", []string{"MYSQL_ROOT_PASSWORD=secret"})
if err != nil {
t.Fatalf("Could not start resource: %s", err)
}
defer func() {
if err := pool.Purge(resource); err != nil {
t.Fatalf("Could not purge resource: %s", err)
}
}()
// 等待数据库就绪
if err := pool.Retry(func() error {
db, err := sql.Open("mysql",
fmt.Sprintf("root:secret@(localhost:%s)/mysql", resource.GetPort("3306/tcp")))
if err != nil {
return err
}
return db.Ping()
}); err != nil {
t.Fatalf("Could not connect to docker: %s", err)
}
// 执行测试...
}
9. 监控与维护
9.1 连接池监控
定期监控连接池状态可以提前发现问题:
func printDBStats(db *sql.DB) {
stats := db.Stats()
log.Printf("DB Stats: InUse=%d Idle=%d WaitCount=%d WaitDuration=%s",
stats.InUse, stats.Idle, stats.WaitCount, stats.WaitDuration)
}
// 定时执行
go func() {
for range time.Tick(5 * time.Minute) {
printDBStats(db)
}
}()
9.2 Redis慢查询监控
Redis的慢查询日志可以帮助发现性能问题:
func getSlowLogs(pool *redis.Pool) ([]interface{}, error) {
conn := pool.Get()
defer conn.Close()
return redis.Values(conn.Do("SLOWLOG", "GET", 10))
}
9.3 错误处理策略
对于数据库错误,应该区分临时性错误和永久性错误:
func isRetryableError(err error) bool {
if err == nil {
return false
}
// MySQL错误码参考 https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
switch mysqlErr.Number {
case 1213: // Deadlock found
case 1205: // Lock wait timeout
return true
default:
return false
}
}
// 网络错误等通常可以重试
return true
}
10. 高级应用场景
10.1 分布式锁实现
使用Redis实现分布式锁:
func acquireLock(pool *redis.Pool, lockKey string, timeout time.Duration) (string, error) {
conn := pool.Get()
defer conn.Close()
token := uuid.New().String()
end := time.Now().Add(timeout)
for {
result, err := redis.String(conn.Do("SET", lockKey, token, "NX", "PX", int(timeout/time.Millisecond)))
if err == nil && result == "OK" {
return token, nil
}
if time.Now().After(end) {
return "", fmt.Errorf("acquire lock timeout")
}
time.Sleep(10 * time.Millisecond)
}
}
func releaseLock(pool *redis.Pool, lockKey, token string) error {
conn := pool.Get()
defer conn.Close()
script := `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end`
_, err := redis.NewScript(1, script).Do(conn, lockKey, token)
return err
}
10.2 缓存一致性策略
常见的缓存模式有:
- Cache Aside:先更新数据库,再删除缓存
- Read/Write Through:缓存层负责读写数据库
- Write Behind:先更新缓存,异步批量写入数据库
以下是Cache Aside模式的实现:
func GetUserWithCache(userID int64) (*User, error) {
// 先查缓存
user, err := getUserFromCache(userID)
if err == nil && user != nil {
return user, nil
}
// 缓存未命中,查数据库
user, err = getUserFromDB(userID)
if err != nil {
return nil, err
}
// 回填缓存
if err := setUserToCache(user); err != nil {
log.Printf("failed to set cache: %v", err)
}
return user, nil
}
func UpdateUser(user *User) error {
// 先更新数据库
if err := updateUserInDB(user); err != nil {
return err
}
// 删除缓存
if err := deleteUserFromCache(user.ID); err != nil {
log.Printf("failed to delete cache: %v", err)
}
return nil
}
10.3 分片策略
对于大规模数据,需要考虑分片策略:
// 基于用户ID的分片示例
func getShardDB(userID int64) (*sql.DB, error) {
shardNum := userID % 4 // 假设有4个分片
shardConfig := config.GetShardConfig(shardNum)
db, err := sql.Open("mysql",
fmt.Sprintf("%s:%s@tcp(%s:%d)/%s",
shardConfig.User,
shardConfig.Password,
shardConfig.Host,
shardConfig.Port,
shardConfig.DBName))
if err != nil {
return nil, err
}
return db, nil
}
11. 项目实战:用户会话管理系统
让我们通过一个完整的示例来整合所学内容。这个系统需要:
- 在MySQL中存储用户基本信息
- 使用Redis管理用户会话
- 实现注册、登录、会话验证功能
11.1 数据库表设计
MySQL用户表:
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
11.2 核心代码实现
用户服务层:
type UserService struct {
userRepo repository.UserRepository
sessionRepo repository.SessionRepository
}
func (s *UserService) Register(username, email, password string) (*model.User, error) {
// 密码哈希
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("failed to hash password: %w", err)
}
user := &model.User{
Username: username,
Email: email,
PasswordHash: string(hashedPassword),
}
if err := s.userRepo.Create(user); err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return user, nil
}
func (s *UserService) Login(username, password string) (string, error) {
user, err := s.userRepo.FindByUsername(username)
if err != nil {
return "", fmt.Errorf("user not found: %w", err)
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return "", fmt.Errorf("invalid password: %w", err)
}
// 创建会话
sessionToken := uuid.New().String()
if err := s.sessionRepo.Set(sessionToken, user.ID, 24*time.Hour); err != nil {
return "", fmt.Errorf("failed to create session: %w", err)
}
return sessionToken, nil
}
func (s *UserService) ValidateSession(token string) (*model.User, error) {
userID, err := s.sessionRepo.Get(token)
if err != nil {
return nil, fmt.Errorf("invalid session: %w", err)
}
user, err := s.userRepo.FindByID(userID)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
return user, nil
}
11.3 性能优化
对于高频访问的会话验证,可以使用本地缓存+Redis的多级缓存:
type cachedSessionRepo struct {
redisRepo repository.SessionRepository
localCache *lru.Cache
cacheTTL time.Duration
}
func (r *cachedSessionRepo) Get(token string) (int64, error) {
// 先查本地缓存
if userID, ok := r.localCache.Get(token); ok {
return userID.(int64), nil
}
// 本地缓存未命中,查Redis
userID, err := r.redisRepo.Get(token)
if err != nil {
return 0, err
}
// 回填本地缓存
r.localCache.Add(token, userID)
return userID, nil
}
12. 部署与扩展
12.1 配置管理
使用Viper管理配置:
go get github.com/spf13/viper
配置文件示例(config.yaml):
mysql:
host: "localhost"
port: 3306
username: "appuser"
password: "secret"
database: "appdb"
redis:
host: "localhost"
port: 6379
pool_size: 100
加载配置:
func initConfig() error {
viper.SetConfigName("config")
viper.AddConfigPath(".")
viper.SetConfigType("yaml")
if err := viper.ReadInConfig(); err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
return nil
}
12.2 连接故障处理
实现自动重连机制:
type resilientDB struct {
db *sql.DB
dsn string
maxRetries int
retryDelay time.Duration
}
func (r *resilientDB) Exec(query string, args ...interface{}) (sql.Result, error) {
var result sql.Result
var err error
for i := 0; i < r.maxRetries; i++ {
result, err = r.db.Exec(query, args...)
if err == nil {
return result, nil
}
if !isRetryableError(err) {
return nil, err
}
time.Sleep(r.retryDelay)
// 尝试重新连接
if err := r.reconnect(); err != nil {
log.Printf("reconnect failed: %v", err)
continue
}
}
return nil, fmt.Errorf("max retries exceeded: %w", err)
}
func (r *resilientDB) reconnect() error {
if err := r.db.Close(); err != nil {
log.Printf("failed to close db: %v", err)
}
db, err := sql.Open("mysql", r.dsn)
if err != nil {
return err
}
r.db = db
return nil
}
12.3 读写分离
对于高负载系统,可以考虑实现读写分离:
type readWriteDB struct {
writer *sql.DB
reader *sql.DB
}
func (db *readWriteDB) Query(query string, args ...interface{}) (*sql.Rows, error) {
return db.reader.Query(query, args...)
}
func (db *readWriteDB) Exec(query string, args ...interface{}) (sql.Result, error) {
return db.writer.Exec(query, args...)
}
13. 安全最佳实践
13.1 SQL注入防护
始终使用参数化查询:
// 错误示范 - 容易导致SQL注入
db.Query("SELECT * FROM users WHERE username = '" + username + "'")
// 正确做法
db.Query("SELECT * FROM users WHERE username = ?", username)
13.2 Redis安全配置
确保Redis配置了密码保护:
func initRedisPoolWithAuth() *redis.Pool {
return &redis.Pool{
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", "localhost:6379",
redis.DialPassword("yourpassword"))
if err != nil {
return nil, err
}
return c, nil
},
}
}
13.3 敏感数据处理
密码等敏感信息应该:
- 使用bcrypt等算法哈希存储
- 不在日志中记录
- 使用环境变量而非代码硬编码
func hashPassword(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
func verifyPassword(hashedPassword, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
14. 常见问题排查
14.1 MySQL连接超时
错误现象: driver: bad connection 或 connection timed out
解决方案:
- 检查连接池设置是否合理
- 增加
wait_timeout和interactive_timeout参数 - 实现连接健康检查:
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)
14.2 Redis内存不足
错误现象: OOM command not allowed when used memory > 'maxmemory'
解决方案:
- 增加Redis内存限制
- 设置合理的淘汰策略:
_, err := conn.Do("CONFIG", "SET", "maxmemory-policy", "allkeys-lru") - 对大Key进行拆分
14.3 慢查询优化
对于MySQL慢查询:
- 使用
EXPLAIN分析查询计划 - 添加适当的索引
- 优化复杂查询
对于Redis慢查询:
- 使用
SLOWLOG命令查看慢查询 - 避免使用
KEYS等阻塞命令 - 对大Value进行拆分
15. 未来扩展方向
15.1 使用ORM框架
对于复杂项目,可以考虑使用GORM等ORM框架:
go get gorm.io/gorm
go get gorm.io/driver/mysql
示例:
type User struct {
gorm.Model
Username string `gorm:"uniqueIndex"`
Email string `gorm:"uniqueIndex"`
}
func main() {
db, err := gorm.Open(mysql.Open("user:pass@tcp(127.0.0.1:3306)/dbname?parseTime=true"), &gorm.Config{})
if err != nil {
panic(err)
}
db.AutoMigrate(&User{})
user := User{Username: "test", Email: "test@example.com"}
db.Create(&user)
}
15.2 引入分布式事务
对于跨数据库操作,可以考虑使用Saga模式或分布式事务框架:
// Saga模式示例
func placeOrder() error {
// 开始Saga
saga := saga.NewSaga()
// 添加补偿操作
saga.AddStep(
func() error {
return deductInventory()
},
func() error {
return restoreInventory()
})
saga.AddStep(
func() error {
return createOrder()
},
func() error {
return cancelOrder()
})
return saga.Run()
}
15.3 使用Redis模块
Redis支持通过模块扩展功能,例如:
- RediSearch:全文搜索
- RedisJSON:JSON文档存储
- RedisGraph:图数据库
// 使用RedisJSON示例
_, err := conn.Do("JSON.SET", "user:1", ".", `{"name":"Alice","age":30}`)
if err != nil {
return err
}
reply, err := redis.String(conn.Do("JSON.GET", "user:1", ".name"))
if err != nil {
return err
}
fmt.Println(reply) // "Alice"
16. 个人经验分享
在实际项目中同时使用Redis和MySQL时,我总结了以下几点经验:
-
连接管理 :数据库连接是宝贵资源,一定要妥善管理。我曾经遇到过一个连接泄露的Bug,导致服务在高峰期崩溃。现在我会在服务启动时打印连接池统计信息,并定期监控。
-
超时设置 :网络操作必须设置合理的超时。我曾经因为没有设置查询超时,导致一个慢查询阻塞了整个服务。现在我会为所有数据库操作设置上下文超时:
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM large_table")
-
批量操作 :对于大量数据操作,批量处理可以提升几个数量级的性能。我曾经优化过一个用户导入功能,从单条插入改为批量插入后,处理时间从10分钟缩短到10秒。
-
缓存策略 :缓存失效是个难题。我现在的原则是:宁可多查一次数据库,也不要提供过期的数据。对于关键数据,会采用更保守的缓存策略。
-
测试覆盖 :数据库相关代码一定要有良好的测试覆盖。我会为每个存储库方法编写单元测试,并使用Docker Compose在CI中运行集成测试。
-
监控指标 :完善的监控可以快速定位问题。我会收集以下指标:
- 查询延迟
- 错误率
- 连接池使用情况
- 缓存命中率
-
文档注释 :数据库模式变更要有完善的文档记录。我会在迁移文件中添加详细的注释,说明变更原因和影响范围。
-
备份策略 :无论数据存储在Redis还是MySQL,都要有可靠的备份方案。我会定期测试备份恢复流程,确保在灾难发生时能快速恢复。

448

被折叠的 条评论
为什么被折叠?



