上一篇 基于redis开发一个时间复杂度为O(1)的优先级队列-优快云博客提到过,现在的获取前面排队次数的时间复杂度为O(M),M就是优先级的个数,如果优先级非常多,那么需要等待时间是比较久的。
本次我将采用一个前缀和算法进行优化。🔗github地址
这是上次提到的例子:
获取普通用户的长度B的前方排队的数量=len(svip)+len(vip)+countMap[B]-countMap[A]
如果存储了前缀和:
获取普通用户的长度B的前方排队的数量=前缀和[B的优先级-1]+countMap[B]-countMap[A]。
采用此方法不仅能够降低时间复杂度,还能显著减少对Redis队列长度的查询频率。例如,在有200个用户且每个用户每秒均发起一次查询以了解其前方排队人数的情况下,系统每秒将产生200次针对Redis队列长度(LEN
)的操作请求。然而,通过引入前缀和机制并将其更新周期设定为每秒一次,则可以将此类操作压缩至每秒仅需执行一次。不过,这一方案也存在一定的局限性,即数据的实时准确性会有所下降,因为信息仅每隔一秒才进行刷新。
一、获取队列长度
-- lua/len.lua
-- KEYS:
-- KEYS[1] - 所有list的key
--
-- ARGV:
-- ARGV[1] - 所有list的len
local result = {}
for i, key in ipairs(KEYS) do
result[i] = redis.call("LLEN", key)
end
return result
二、构建前缀和
// CalculatePrefixSum 传入 counts 数组,返回累积和数组
func CalculatePrefixSum(counts []int64) []int64 {
n := len(counts)
prefix := make([]int64, n)
if n == 0 {
return prefix
}
prefix[0] = counts[0]
for i := 1; i < n; i++ {
prefix[i] = prefix[i-1] + counts[i]
}
return prefix
}
三、定时器定时刷新
package main
import (
"fmt"
"time"
)
type Updater struct {
regularTime time.Duration //timer 的间隔时间
everyTime time.Duration //在定时里多长时间检测一次是否超过阈值
timer *time.Timer //定时器
triggerUpdate chan struct{} // 触发立即更新的通道
stopChan chan struct{} // 停止信号
}
func NewUpdater(regular, every int64, trigger func() bool, update func() error) *Updater {
regularTime := time.Duration(regular)
u := &Updater{
regularTime: regularTime,
everyTime: time.Duration(every),
timer: time.NewTimer(regularTime),
triggerUpdate: make(chan struct{}, 1), // 缓冲大小为1,避免阻塞
stopChan: make(chan struct{}),
}
go u.run(update) // 启动事件循环
go u.CheckTrigger(trigger) //启动指定方法方法触发更新
return u
}
// CheckTrigger 启动一个goroutine,检查触发更新条件,并立即触发更新
func (u *Updater) CheckTrigger(trigger func() bool) {
for {
if trigger() {
// 非阻塞发送信号,避免重复触发
select {
case u.triggerUpdate <- struct{}{}:
case <-u.stopChan: // 停止信号
return
default:
}
}
time.Sleep(u.everyTime)
}
}
// run 处理定时器和立即触发的事件
func (u *Updater) run(update func() error) {
for {
select {
case <-u.timer.C: // 定时器触发
u.update(update)
u.resetTimer()
case <-u.triggerUpdate: // 立即触发信号
u.update(update)
u.resetTimer()
case <-u.stopChan: // 停止信号
return
}
}
}
// resetTimer 安全重置定时器
func (u *Updater) resetTimer() {
// 停止并排空定时器通道
if !u.timer.Stop() {
select {
case <-u.timer.C:
default:
}
}
u.timer.Reset(u.regularTime)
}
// update 执行更新操作
func (u *Updater) update(update func() error) {
if err := update(); err != nil {
fmt.Printf("Update failed %e, current time:%s \n", err, time.Now().Format("15:04:05"))
} else {
fmt.Printf("Update successful, current time:%s \n", time.Now().Format("15:04:05"))
}
}
// Stop 停止Updater
func (u *Updater) Stop() {
close(u.stopChan)
}
注意:触发更新有两个条件,都是下文要实现的:
- 定时时间到
- trigger方法返回true
四、其它
1.计算返回的长度
func (pq *PriorityQueue) CountBefore(elemID string) (int64, error) {
//...
countVal := res[0]
levelVal := res[1]
if levelVal <= 0 {
return countVal, nil
}
return countVal + pq.levelsCount[levelVal-1], nil
返回的长度本队列的+前缀和的
2.定时器触发更新的方法
// RefreshLevelsCount 刷新各层级计数器
func (pq *PriorityQueue) RefreshLevelsCount() error {
ctx := context.Background()
res, err := pq.client.Eval(ctx, lenScript, pq.levels).Int64Slice()
if err != nil {
return fmt.Errorf("eval error: %v", err)
}
if res == nil {
return nil
}
//刷新完之后清空值
pq.levelsCount = CalculatePrefixSum(res)
pq.pushCount = 0
pq.popCount = 0
return nil
}
也就是update,到时间之后调用前缀和定期更新队列长度
3.其它触发条件
func (pq *PriorityQueue) CheckRefresh() bool {
return float64(atomic.LoadInt64(&pq.thresholdCount)) < math.Abs(float64(atomic.LoadInt64(&pq.pushCount)-atomic.LoadInt64(&pq.popCount)))
}
也就是trigger方法,我这里是当pop和push短时间内操作相差比较大的时候,去更新前缀和数组。也就是说短时间内突然push/pop很多,队列需要变化。