iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0
Modern Web

Let's Go! 解剖Go server開發到部署的過程系列 第 16

day 16 - 開啟git worktree 進行redis lua-script 測試比較

今天要來換個redis寫法, 看看能不能縮短執行時間。

首先我會用git worktree再開一個專案資料夾, 新資料夾就留在調整前, 稱為v1版, 再修改版本就是v2版。

git worktree 可以讓本機同時有兩個分支相互比較或進行開發, 有時候A分支功能寫到一半突然收到需求要改B分支的時候, 就需要先把分支commit或是做git stash, 後來改用worktree就可以直接再開一個資料夾對分支進行修改, 不會影響原本的開發進度; 另外像是這次要比較v1, v2 版本差異的時候, 就可以啟動兩個不同分支的版本進行比較。

  • 使用 git worktree 開啟另一個專案

    git worktree add -b ba_master ../coconut_2
    
  • 修改 redis/limit.go
    第二版採用 redis支援的lua-script方式修改, 寫好的整串lua-script是採原子式的方式排隊執行的, 要等到前一句script執行完才會執行下一個, 它可以保證script裡面的語法都執行完了才開始新的一組指令, 這在我們專案裡面很常使用到。

    type LimitSetting struct {
        Level1 int
        Level2 int
        Level3 int
    }
    
    // set point
    func PointSetBatch(conn *redis.Client, keys []string, point int, limitSetting map[string]int, expired int) (err error) {
        // LimitSetting , lua-script json decode 使用  
        tmp := &LimitSetting{
            Level1: limitSetting["0"],
            Level2: limitSetting["1"],
            Level3: limitSetting["2"],
        }
        // script
        luaScript := `
        local point = tonumber(ARGV[1])
        local limit = cjson.decode(ARGV[2])
        local expired = tonumber(ARGV[3])
    
        -- 先GET一次KEY, 沒有KEY的要SET, SET 同時要EXPIRE
        if( redis.call('GET', KEYS[1]) == nil or redis.call('GET', KEYS[1]) == false) then
            redis.call('SETEX', KEYS[1], expired, 0)
        end
    
        if( redis.call('GET', KEYS[2]) == nil or redis.call('GET', KEYS[2]) == false) then
            redis.call('SETEX', KEYS[2], expired, 0)
        end
    
        if( redis.call('GET', KEYS[3]) == nil or redis.call('GET', KEYS[3]) == false) then
            redis.call('SETEX', KEYS[3], expired, 0)
        end
    
         -- level 1
        if(redis.call('GET', KEYS[1]) + point <= tonumber(limit.Level1)) then
            redis.call('INCRBY',KEYS[1], point)
        else
             -- 定義不同的回傳值來區分踩到的限額是哪一個
            return tostring(-99)
        end
    
        -- level 2
        if(redis.call('GET', KEYS[2]) + point <= tonumber(limit.Level2)) then
            redis.call('INCRBY',KEYS[2], point)
        else
            -- 定義不同的回傳值來區分踩到的限額是哪一個
            return tostring(-98)
        end
    
        -- level 3
        if(redis.call('GET', KEYS[3]) + point <= tonumber(limit.Level3)) then
            redis.call('INCRBY',KEYS[3], point)
        else
            -- 定義不同的回傳值來區分踩到的限額是哪一個
            return tostring(-97)
        end
    
        return 'ok'
        `
        script, err := conn.ScriptLoad(luaScript).Result()
        if err != nil {
            return err
        }
    
        reply, err := conn.EvalSha(script, keys, point, tmp.MarshalBinary(), expired).Result()
        if err != nil {
            return err
        }
        fmt.Println("reply:", reply)
        // TODO: 處理 reply 回傳值對應資訊
        return
    }
    
    // lua-script json decode 使用 
    func (s *LimitSetting) MarshalBinary() (ret string) {
        data, _ := json.Marshal(s)
        ret = string(data)
        return ret
    }
    
    // lua-script json decode 使用 
    func (s *LimitSetting) UnmarshalBinary(data []byte) error {
        return json.Unmarshal(data, s)
    }
    
  • rpc.go 改打新的 function PointSetBatch

    err = coconut_redis.PointSetBatch(s.RedisClient, keys, int(in.Point), limitSettings, 30)
        if err != nil {
            Logger.WithFields(map[string]interface{}{
                "test": 111,
                "time": time.Now().UnixNano(),
                "err:": err.Error(),
            }).Errorf("redis.PointSetBatch")
            return nil, coconutError.ParseError(coconutError.ErrRedis, err)
        }
    
  • 測試v1, v2 版本差異
    在不同的port啟動兩個不同分支的服務&不同台redis, 同時進行測試。

    func main() {
        // 連線到遠端 gRPC 伺服器。
        conn1, err := grpc.Dial("localhost:3100", grpc.WithInsecure())
        if err != nil {
            log.Fatalf("conn 連線失敗:%v", err)
        }
    
        defer conn1.Close()
    
        coco1 := coconut.NewCoconutClient(conn1)
    
        // 連線到遠端 gRPC 伺服器。
        conn2, err := grpc.Dial("localhost:3200", grpc.WithInsecure())
        if err != nil {
            log.Fatalf("conn 連線失敗:%v", err)
        }
    
        defer conn2.Close()
    
        coco2 := coconut.NewCoconutClient(conn2)
    
        n, _ := strconv.Atoi(os.Args[1])
    
        var (
            sum1 float64
            sum2 float64
        )
        req := &coconut.PointsRequest{
            Level_1: "aaa",
            Level_2: "bbb",
            Level_3: "ccc",
            Point:   100,
        }
        wait := &sync.WaitGroup{}
    
        for i := 0; i < n; i++ {
            wait.Add(1)
            go func() {
                defer func() {
                    wait.Done()
                }()
                start := time.Now()
                _, _ = coco1.UpdatePoints(context.Background(), req)
                sum1 += time.Since(start).Seconds()
            }()
        }
    
        for i := 0; i < n; i++ {
            wait.Add(1)
            go func() {
                defer func() {
                    wait.Done()
                }()
                start := time.Now()
                _, _ = coco2.UpdatePoints(context.Background(), req)
                sum2 += time.Since(start).Seconds()
            }()
        }
    
        wait.Wait()
    
        fmt.Println("[v1] total count:", n, ", avg execute_time:", (sum1 / float64(n)))
        fmt.Println("[v2] total count:", n, ", avg execute_time:", (sum2 / float64(n)))
    }
    
  • 測試 10次, 100次, 1000次的執行結果
    https://i.imgur.com/yTcE6dP.png

經過多次分別測試10,100,1000的結果可以觀察到, 10次的時候兩個版本處理時間差異不大, 100~1000次就可以看出明顯的差異, 這樣就可以把版本調整為v2版了。


上一篇
day 15 - 從執行時間開始優化
下一篇
day 17 - 利用 interface 來mock外部回應
系列文
Let's Go! 解剖Go server開發到部署的過程30

尚未有邦友留言

立即登入留言