GolangHttpSession-2 数据结构l2

本文探讨了Golang中会话管理的优化方法,通过使用slice替代map来提高内存管理和性能,并进行了详细的性能测试。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 版权

本文为原创, 遵循 CC 4.0 BY-SA 版权协议, 转载需注明出处: https://blog.youkuaiyun.com/big_cheng/article/details/120461065.
文中代码属于 public domain (无版权).

2. 概述

上一篇里sesmgr 用m2 map 存放每个session 的过期时间来模拟堆, 如果使用slice 的话扩容copy 有耗时.

但是之后考虑slice 的性能应该高于map, 而且堆本身在概念上就是个数组. 如果m2 改用slice 的话: a) 不能一次申请太多内存; b) 避免copy; c) 删除元素没有空位问题, 因为待删元素总会先移到堆尾.
由此想到可以分块来申请内存. 例如先申请10w 个空位; 用完还需要时 再申请10w 个. 如此内存是缓慢增长的, 并且少copy.
因为内存实际上是有限的. 例如100w 个session, 假设每个需100B, 就是100M. int32/uint32 的最大值是20/40多亿, 相应个数的session 就是100~200G 内存. 所以很多时候sesmgr 可以限制session 的数量.

首先试验slice 的性能.

3. p_slice.go

func main() {
	rand.Seed(time.Now().Unix())

	n := 100_000
	idxs := make([]int, n) // shuffled
	{
		m := map[int]int{}
		for i := 0; i < n; i++ {
			m[i] = 0
		}
		i := 0
		for k, _ := range m {
			idxs[i] = k
			i++
		}
	}

	arr := make([][2]int, n)
	t0 := time.Now()
	for _, idx := range idxs {
		arr[idx] = [2]int{rand.Int(), rand.Int()}
	}
	println("init arr\n\t", len(arr), time.Now().Sub(t0).String())
	
	...
}

上面idxs 是乱序的slice 下标值, 因为map 是无序的.
单线程遍历:

	println("单线程遍历一遍")
	t0 = time.Now()
	for _, idx := range idxs {
		if v := arr[idx]; v[0] >= 0 {
		}
	}
	println("\t", time.Now().Sub(t0).String())

4根线程并发读+改:

	println("4线程查改20s")
	chX := make(chan int)
	chOut := make(chan int)
	mu := new(sync.Mutex)
	for i := 0; i < 4; i++ {
		go func(i0 int) {
			cnt := 0
			for {
				select {
				case <-chX: // 退出
					chOut <- cnt
					return
				default:
					mu.Lock()
					idx := idxs[(i0+cnt*4)%n]
					if v := arr[idx]; v[0] >= 0 {
						v[1] = rand.Int()
						arr[idx] = v
					}
					cnt++
					mu.Unlock()
				}
			}
		}(i)
	}

搜集结果:

	time.Sleep(20 * time.Second)
	close(chX)
	cnts, total := [4]int{}, 0
	for i := 0; i < 4; i++ {
		cnts[i] = <-chOut
		total += cnts[i]
	}
	fmt.Printf("\t %v total %d(%d)\n", cnts, total, len(strconv.Itoa(total)))

手测结果:

// 数据量   init   单线程遍历一遍   4线程查改20s   内存
// 10w      7ms    0              7kw            10M
// 100w     70ms   0              5kw            105M

相比于p_map int: 单线程遍历非常快, 4线程处理数增加1/4~3/4, 内存也稍有减少. 应该是可行的.

4. t_ses_l2.go

ses

package main

import (
	"container/heap"
	"errors"
	"fmt"
	"math/rand"
	"strconv"
	"sync"
	"time"
)

const default_duration = 30 * time.Minute // 默认有效期, 30min

type ses struct {
	key2  int                    // 在l2 的下标
	attrs map[string]interface{} // session 属性
}

func (s ses) String() string {
	return fmt.Sprintf("ses/key2=%d", s.key2)
}

sesmgr

const LEN1 = 100_000

type sesmgr struct {
	// m2 是map 模拟的堆, key2 是下标, 总是从0 开始连续. 节点0 过期时间最小.
	//
	// 堆节点swap 时, 由节点.key1 同步更新m1 ses.key2. 例如:
	//   123 => ses1(key2=0) => 堆节点0 {123, t1}, 456 => ses2(key2=1) => 堆节点1 {456, t2}.
	//   堆节点0,1 swap时, 由123,456 找到ses1,ses2 后更新ses.key2:
	//   123 => ses1(key2=1) => 堆节点1 {123, t1}, 456 => ses2(key2=0) => 堆节点0 {456, t2}.

	mu *sync.Mutex
	m1 map[uint32]*ses    // session map: key1 => ses.
	l2 []*[LEN1][2]uint32 // 二维数组, key2 => {key1, 过期时间 (unix seconds)}.
}

func (psm *sesmgr) Len() int {
	sm := *psm
	return len(sm.m1)
}
func (psm *sesmgr) Less(i, j int) bool {
	sm := *psm
	i1, i2 := i/LEN1, i%LEN1
	j1, j2 := j/LEN1, j%LEN1
	return sm.l2[i1][i2][1] < sm.l2[j1][j2][1]
}
func (psm *sesmgr) Swap(i, j int) {
	sm := *psm
	i1, i2 := i/LEN1, i%LEN1
	j1, j2 := j/LEN1, j%LEN1
	sm.m1[sm.l2[i1][i2][0]].key2 = j
	sm.m1[sm.l2[j1][j2][0]].key2 = i
	sm.l2[i1][i2], sm.l2[j1][j2] = sm.l2[j1][j2], sm.l2[i1][i2]
}

m2 改为l2 二维数组, 每次申请LEN1 个空位. i/LEN1、i%LEN1 就是在一、二维的下标.
l2 的第二维使用指针拷贝更快, 参见补充.

func (psm *sesmgr) Push(x interface{}) {
	sm := *psm
	i := len(sm.m1) - 1 /* 要减1, 因为m1 已先加了. */
	i1, i2 := i/LEN1, i%LEN1
	if i1 >= len(sm.l2) { // 扩容
		psm.l2 = append(psm.l2, &[LEN1][2]uint32{}) /* 这里不能用sm, 因为它是拷贝. */
	}
	psm.l2[i1][i2] = x.([2]uint32)
}
func (psm *sesmgr) Pop() interface{} {
	sm := *psm
	i := len(sm.m1) - 1
	i1, i2 := i/LEN1, i%LEN1
	x := sm.l2[i1][i2]
	sm.l2[i1][i2] = [2]uint32{} // 清除
	return x
}

func newSesmgr() *sesmgr {
	return &sesmgr{
		mu: new(sync.Mutex),
		m1: map[uint32]*ses{},
		l2: []*[LEN1][2]uint32{},
	}
}

Push 方法注意append 要用psm 而非sm, 因为sm 是拷贝. 例如:

type ctT struct {
	l []int
}

func test_slice3() {
	ct := ctT{
		l: []int{0},
	}

	ct2 := ct
	fmt.Printf("%p %p\n", &ct, &ct2)   // 0xc000004078 0xc000004090 - 拷贝, 地址变了
	fmt.Printf("%p %p\n", ct.l, ct2.l) // 0xc000016088 0xc000016088 - l[0]的地址, 相同

	ct2.l = append(ct2.l, 1)
	fmt.Printf("%p %p\n", ct.l, ct2.l) // 0xc000016088 0xc0000160c0
	println(len(ct.l), len(ct2.l))     // 1 2
}

新建session

// 新建session.
func (psm *sesmgr) newSes() (*ses, error) {
	sm := *psm
	sm.mu.Lock()
	defer sm.mu.Unlock()
	var key1 uint32
	for i := 0; i < 11; i++ {
		if i == 10 { // 尝试10次
			return nil, errors.New("sesmgr: new key fail")
		} else {
			j := rand.Uint32()
			if _, ok := sm.m1[j]; !ok {
				key1 = j
				break
			}
		}
	}

	key2 := len(sm.m1)
	v1 := &ses{
		key2:  key2,
		attrs: map[string]interface{}{},
	}
	sm.m1[key1] = v1                     /* 要先加m1, 因为heap.Push 内部会去m1 查找. */
	v2 := [2]uint32{key1, rand.Uint32()} // 过期时间用随机值代替
	heap.Push(psm, v2)
	return v1, nil
}

查找session

// 查找session. 如果找到, 过期时间增加默认有效期.
func (psm *sesmgr) getSes(key1 uint32) *ses {
	sm := *psm
	sm.mu.Lock()
	defer sm.mu.Unlock()
	ses, ok := sm.m1[key1]
	if !ok {
		return nil
	}

	i := ses.key2
	i1, i2 := i/LEN1, i%LEN1
	v2 := sm.l2[i1][i2]
	v2[1] = v2[1] + uint32(default_duration/time.Second)
	sm.l2[i1][i2] = v2
	heap.Fix(psm, i)
	return ses
}

删除session

// 删除session.
func (psm *sesmgr) delSes(key1 uint32) {
	sm := *psm
	sm.mu.Lock()
	defer sm.mu.Unlock()
	ses, ok := sm.m1[key1]
	if !ok {
		return
	}

	heap.Remove(psm, ses.key2)
	delete(sm.m1, key1)
}

测试增删改

func main() {
	rand.Seed(time.Now().Unix())

	sm := newSesmgr()
	ses1, _ := sm.newSes()
	ses2, _ := sm.newSes()
	debug(sm, ses1, ses2)

	fmt.Println("\nses2 过期时间减到0")
	i := ses2.key2
	i1, i2 := i/LEN1, i%LEN1
	ses2V2 := sm.l2[i1][i2]
	ses2V2[1] = 0
	sm.l2[i1][i2] = ses2V2
	heap.Fix(sm, i)
	debug(sm, ses1, ses2)

	fmt.Println("\n删除ses2, 新增ses3(显示为ses2)")
	ses2Key1 := ses2V2[0]
	sm.delSes(ses2Key1)
	ses2, _ = sm.newSes()
	debug(sm, ses1, ses2)

	...
}

func debug(sm *sesmgr, ses1, ses2 *ses) {
	i, j := ses1.key2, ses2.key2
	i1, i2 := i/LEN1, i%LEN1
	j1, j2 := j/LEN1, j%LEN1
	ses1V2 := sm.l2[i1][i2]
	ses2V2 := sm.l2[j1][j2]
	fmt.Printf("ses1\n\tkey1=>ses\t\t%d => %v\n\tkey2=>{key1 expire}\t%d => %v\n",
		ses1V2[0], ses1, ses1.key2, ses1V2)
	fmt.Printf("ses2\n\tkey1=>ses\t\t%d => %v\n\tkey2=>{key1 expire}\t%d => %v\n",
		ses2V2[0], ses2, ses2.key2, ses2V2)
}

实测结果一例:

ses1
        key1=>ses               950647161 => ses/key2=1
        key2=>{key1 expire}     1 => [950647161 706409805]
ses2
        key1=>ses               116464702 => ses/key2=0
        key2=>{key1 expire}     0 => [116464702 175556386]

ses2 过期时间减到0
ses1
        key1=>ses               950647161 => ses/key2=1
        key2=>{key1 expire}     1 => [950647161 706409805]
ses2
        key1=>ses               116464702 => ses/key2=0
        key2=>{key1 expire}     0 => [116464702 0]

删除ses2, 新增ses3(显示为ses2)
ses1
        key1=>ses               950647161 => ses/key2=0
        key2=>{key1 expire}     0 => [950647161 706409805]
ses2
        key1=>ses               3387079671 => ses/key2=1
        key2=>{key1 expire}     1 => [3387079671 3950566886]

测试性能

	fmt.Println("\ninit map")
	n := 100_000
	t0 := time.Now()
	keys1 := make([]uint32, n)
	for i := 0; i < n; i++ {
		if ses, err := sm.newSes(); err != nil {
			panic(err)
		} else {
			j := ses.key2
			j1, j2 := j/LEN1, j%LEN1
			keys1[i] = sm.l2[j1][j2][0]
		}
	}
	fmt.Println("\t", n, time.Now().Sub(t0).String(), "len(l2)", len(sm.l2))

单线程遍历一遍:

	fmt.Println("单线程遍历一遍")
	t0 = time.Now()
	for _, key1 := range keys1 {
		sm.getSes(key1)
	}
	fmt.Println("\t", time.Now().Sub(t0).String())

4根线程并发遍历:

	fmt.Println("4线程遍历20s")
	chX := make(chan int)
	chOut := make(chan int)
	for i := 0; i < 4; i++ {
		go func(i0 int) {
			cnt := 0
			for {
				select {
				case <-chX: // 退出
					chOut <- cnt
					return
				default:
					key1 := keys1[(i0+cnt*4)%n]
					sm.getSes(key1)
					cnt++
				}
			}
		}(i)
	}

搜集结果:

	time.Sleep(20 * time.Second)
	close(chX)
	cnts, total := [4]int{}, 0
	for i := 0; i < 4; i++ {
		cnts[i] = <-chOut
		total += cnts[i]
	}
	fmt.Printf("\t %v total %d(%d)\n", cnts, total, len(strconv.Itoa(total)))

单线程删除1/10:

	fmt.Println("单线程删除1/10")
	t0 = time.Now()
	for i := n / 10; i >= 0; i-- {
		sm.delSes(sm.l2[0][0][0])
	}
	fmt.Println("\t", time.Now().Sub(t0).String(), len(sm.m1))

手测结果对比:

p_map int
	// 数据量   init   单线程遍历一遍   4线程查改20s   内存
	// 10w     20ms    5ms             4kw           10M
	// 100w    200ms   80ms            4kw           120M
t_ses
	// 数据量   init   单线程遍历一遍   4线程遍历20s   内存   单线程删除1/10
	// 10w     120ms   40ms           2.3kw          16M    60ms
	// 100w    1.6s    500ms          1.9kw          197M   780ms
t_ses_l2
	// 数据量   init   单线程遍历一遍   4线程遍历20s   内存   单线程删除1/10
	// 10w      60ms   20ms           4.4kw          15M    20ms
	// 100w     1s     200ms          3.9kw          140M   370ms

效果不错, 速度/处理数都提升了约1倍, 内存也有所减少.

5. 补充

拷贝的性能

const n = 100_000

// 2ms 20ms 0s - 拷贝10个指针不要时间.
func twoDim() {
	println("l 1")
	l := make([][n][2]int, 1)
	arr := &l[0]
	for i := 0; i < n; i++ {
		arr[i] = [2]int{rand.Int(), rand.Int()}
	}
	// println(l[0][0][0], l[0][0][1], l[0][99][0], l[0][99][1])
	//
	t0 := time.Now()
	l = append(l, [n][2]int{})
	println(time.Now().Sub(t0).String())
	// println(l[0][0][0], l[0][0][1], l[0][99][0], l[0][99][1])

	println("\nl 10")
	l = make([][n][2]int, 10)
	// println("len", len(l), "cap", cap(l))
	for i := 0; i < 10; i++ {
		arr := &l[i]
		for j := 0; j < n; j++ {
			arr[j] = [2]int{rand.Int(), rand.Int()}
		}
	}
	// println(l[0][0][0], l[0][0][1], l[9][99][0], l[9][99][1])
	//
	t0 = time.Now()
	l = append(l, [n][2]int{})
	println(time.Now().Sub(t0).String())
	// println("len", len(l), "cap", cap(l))
	// println(l[0][0][0], l[0][0][1], l[9][99][0], l[9][99][1])

	println("\npl 10")
	pl := make([]*[n][2]int, 10)
	for i := 0; i < 10; i++ {
		arr := [n][2]int{}
		for j := 0; j < n; j++ {
			arr[j] = [2]int{rand.Int(), rand.Int()}
		}
		pl[i] = &arr
	}
	// println(pl[0][0][0], pl[0][0][1], pl[9][99][0], pl[9][99][1])
	//
	t0 = time.Now()
	pl = append(pl, &[n][2]int{})
	println(time.Now().Sub(t0).String())
	// println("len", len(pl), "cap", cap(pl))
	// println(pl[0][0][0], pl[0][0][1], pl[9][99][0], pl[9][99][1])
}

拷贝指针几乎不需要时间, 可见第二维要定义成指针类型.

6. 参考

GolangHttpSession-1 数据结构

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值