Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions OPTIMIZATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# 代码优化说明 (Code Optimizations)

本文档记录了对 go-lock 项目所做的优化改进。

## 优化清单

### 1. 修复导入循环问题
**问题**: `main.go` 在根包 `lock` 中,与 `redis` 包中的测试文件形成导入循环。
**解决方案**:
- 将 `main.go` 移至 `examples/` 目录
- 修改包名为 `package main`
- 这样既避免了导入循环,又提供了使用示例

**影响**: 修复了测试无法运行的问题

---

### 2. 修复 time.Tick 内存泄漏
**问题**: `startWatchDog` 函数使用 `time.Tick()` 创建 ticker,该 ticker 无法被垃圾回收,造成内存泄漏。
**解决方案**:
- 使用 `time.NewTicker()` 替代 `time.Tick()`
- 添加 `defer ticker.Stop()` 确保资源释放

```go
// 修改前
tick := time.Tick(expire / 3)

// 修改后
ticker := time.NewTicker(expire / 3)
defer ticker.Stop()
```

**影响**: 防止长时间运行时的内存泄漏

---

### 3. 修复 time.After 内存泄漏
**问题**: `Lock()` 函数中使用 `time.After()` 在循环中可能导致大量 timer 堆积。
**解决方案**:
- 使用 `time.NewTimer()` 替代 `time.After()`
- 在不再需要时调用 `timer.Stop()` 释放资源

```go
// 修改前
case <-time.After(time.Duration(val) * time.Millisecond):

// 修改后
timer := time.NewTimer(time.Duration(val) * time.Millisecond)
select {
case v := <-sub:
timer.Stop()
// ...
case <-timer.C:
case <-ctx.Done():
timer.Stop()
// ...
}
```

**影响**: 防止在高并发场景下的内存泄漏

---

### 4. 添加互斥锁保护 doneC
**问题**: `doneC` 的初始化和关闭存在并发访问,可能导致竞态条件。
**解决方案**:
- 在 `Lock` 结构体中添加 `mu sync.Mutex`
- 使用互斥锁保护 `doneC` 的读写操作

```go
type Lock struct {
// ...
doneC chan struct{}
mu sync.Mutex // 新增
}

// 在访问 doneC 时加锁
l.mu.Lock()
if l.doneC == nil {
l.doneC = make(chan struct{})
}
l.mu.Unlock()
```

**影响**: 消除竞态条件,提高并发安全性

---

### 5. 添加类型断言安全检查
**问题**: `Lock()` 函数中直接进行类型断言 `res.Val.(int64)`,如果类型不匹配会 panic。
**解决方案**:
- 使用安全的类型断言 `val, ok := res.Val.(int64)`
- 检查断言结果,失败时返回错误

```go
// 修改前
val := res.Val.(int64)

// 修改后
val, ok := res.Val.(int64)
if !ok {
return errors.New("unexpected return type from lock script")
}
```

**影响**: 提高代码健壮性,避免运行时 panic

---

### 6. 优化 goroutine 和 channel 资源管理

#### 6.1 Subscribe 方法优化
**问题**: goroutine 可能因为 channel 阻塞而泄漏。
**解决方案**:
- 使用缓冲 channel `make(chan Sub, 1)`
- 添加 `defer close(c)` 确保 channel 关闭
- 在发送时检查 channel 关闭状态
- 在所有发送操作中添加 `ctx.Done()` 检查,防止阻塞

```go
func (l *Lock) Subscribe(ctx context.Context) <-chan Sub {
c := make(chan Sub, 1) // 使用缓冲 channel
go func() {
defer close(c) // 确保 channel 关闭
// ...
select {
case c <- Sub{}:
case <-ctx.Done():
return
}
}()
return c
}
```

#### 6.2 DefaultClient.Subscribe 方法优化
**问题**: goroutine 和 Redis 订阅连接未正确清理。
**解决方案**:
- 添加 `defer sub.Close()` 确保 Redis 订阅关闭
- 添加 `defer close(res)` 确保 channel 关闭
- 在所有发送操作中添加 `ctx.Done()` 检查
- 使用缓冲 channel 防止阻塞

```go
func (d DefaultClient) Subscribe(ctx context.Context, channels ...string) (chan any, error) {
res := make(chan any, 1) // 使用缓冲 channel
sub := d.client.Subscribe(ctx, channels...)

go func() {
defer close(res) // 确保 channel 关闭
defer sub.Close() // 确保订阅关闭
// ...
select {
case res <- msg:
case <-ctx.Done():
return
}
}()
return res, nil
}
```

**影响**: 防止 goroutine 泄漏和资源泄漏

---

## 优化总结

这些优化主要解决了以下问题:
1. **内存泄漏**: 修复了 timer 和 goroutine 泄漏问题
2. **并发安全**: 添加了互斥锁保护共享资源
3. **资源管理**: 确保所有资源(channel、goroutine、订阅)正确清理
4. **健壮性**: 添加了类型检查,避免运行时 panic
5. **可测试性**: 修复了导入循环,使测试可以正常运行

所有优化都经过了以下验证:
- `go build ./...` - 编译通过
- `go vet ./...` - 静态分析通过
- `go test -race` - 竞态检测通过

## 建议

为了进一步提高代码质量,建议:
1. 添加单元测试(不依赖真实 Redis)
2. 添加基准测试以验证性能
3. 考虑添加配置项控制 watchdog 的刷新频率
4. 考虑添加指标收集(metrics)以监控锁的使用情况
9 changes: 5 additions & 4 deletions main.go → examples/main.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package lock
package main

import (
"context"
redisv9 "github.com/redis/go-redis/v9"
"github.com/uzziahlin/go-lock"
"github.com/uzziahlin/go-lock/redis"
)

func main() {
var lock Lock
var l lock.Lock
client := redis.NewDefaultClient(&redisv9.Options{
Addr: "your redis address",
Password: "you redis password",
DB: 0,
})
lock = redis.NewLock("lockName", redis.WithClient(client))
err := lock.Lock(context.TODO())
l = redis.NewLock("lockName", redis.WithClient(client))
err := l.Lock(context.TODO())
if err != nil {
// todo 加锁失败
}
Expand Down
44 changes: 35 additions & 9 deletions redis/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,43 +56,69 @@ func (d DefaultClient) Eval(ctx context.Context, script string, keys []string, v
}

func (d DefaultClient) Subscribe(ctx context.Context, channels ...string) (chan any, error) {
res := make(chan any)
res := make(chan any, 1)

sub := d.client.Subscribe(ctx, channels...)

go func() {
defer close(res)
defer sub.Close()

for {
receive, err := sub.Receive(ctx)
if err != nil {
res <- err
select {
case res <- err:
case <-ctx.Done():
}
return
}
switch v := receive.(type) {
case *redis.Subscription:
res <- &Subscription{
select {
case res <- &Subscription{
Kind: v.Kind,
Channel: v.Channel,
Count: v.Count,
}:
case <-ctx.Done():
return
}
case *redis.Message:
res <- &Message{
select {
case res <- &Message{
Channel: v.Channel,
Pattern: v.Pattern,
Data: []byte(v.Payload),
}:
case <-ctx.Done():
return
}
for _, p := range v.PayloadSlice {
res <- &Message{
select {
case res <- &Message{
Channel: v.Channel,
Pattern: v.Pattern,
Data: []byte(p),
}:
case <-ctx.Done():
return
}
}
case *Pong:
res <- &Pong{
Data: v.Data,
case *redis.Pong:
select {
case res <- &Pong{
Data: v.Payload,
}:
case <-ctx.Done():
return
}
case error:
res <- v
select {
case res <- v:
case <-ctx.Done():
}
return
}
}
}()
Expand Down
Loading