• .NET/JAVA/GO 固定时间窗口算法实现(无锁线程安全)


    一.前言

    最近有一个生成 APM TraceId 的需求,公司的APM系统的 TraceId 的格式为:APM AgentId+毫秒级时间戳+自增数字,根据此规则生成的 Id 可以保证全局唯一(有 NTP 时间同步),前两个字段好说,最后一个字段也不复杂,我的想法是按秒来进行自增。比如说1秒的时候,自增计数为100,在2秒的时候会重置为0,然后进行自增。其实这个思想就是固定时间窗口算法,这个算法一般常用在限流、Id生成器等场景。

    二. .NET 代码实现

    long _currentTime;
    long _current;
    public long FixedWindow()
    {
        var now = DateTimeOffset.Now.ToUnixTimeSeconds();
        var ct = Interlocked.Read(ref _currentTime);
        if (now > ct)
        {
            if (Interlocked.CompareExchange(ref _currentTime, now, ct)==ct)
            {
                Interlocked.Exchange(ref _current, 0);
            }
        }
        
        return Interlocked.Increment(ref _current);
    }
    

    代码没多少,每调用一次就返回计数,采用的 C# CAS API Interlocked ,保证每个计数操作都是原子操作,从而达到无锁。

    测试代码,使用10个线程并发调用,每个线程调用 1w次,最终期望计数应该是10w。

    private static long _currentTime;
    private static long _current;
    private static Semaphore _semaphore = new Semaphore(0, 10);
    static void Main(string[] args)
    {
        _currentTime = DateTimeOffset.Now.ToUnixTimeSeconds();
        _current = 0;
        for (int i = 0; i < 10; i++)
        {
            Task.Factory.StartNew(() =>
            {
                for (int j = 0; j < 10000; j++)
                {
                    FixedWindow();
                }
    
                _semaphore.Release(1);
            });
        }
    
        for (int i = 0; i < 10; i++)
        {
            _semaphore.WaitOne();
        }
        
        Console.WriteLine(_current);
        Console.WriteLine("sleep 2s");
        Thread.Sleep(2000);
        Console.WriteLine(FixedWindow());
    }
    

    执行结果:

    image-20220217141347106

    符合预期,10线程的计数在 1s 内能执行完毕,所以最终计数是10w,然后sleep 2s,重置计数,再次调用就返回了 1

    三.JAVA代码实现

    static AtomicLong currentTime = new AtomicLong();
    static AtomicLong currentNumber = new AtomicLong();
    
    public static long fixedWindow() {
        long now = currentTimeSeconds();
        long ct = currentTime.get();
        if (now > ct) {
            if (currentTime.compareAndSet(ct, now)) {
                currentNumber.set(0);
            }
        }
    
        return currentNumber.incrementAndGet();
    }
    
    public static long currentTimeSeconds(){
        return System.currentTimeMillis() / 1000;
    }
    

    测试代码:

    public static void main(String[] args) throws InterruptedException {
        currentTime.set(currentTimeSeconds());
        currentNumber.set(0);
    
        long num = 0;
        for (int i = 0; i < 1000; i++) {
            num = fixedWindow();
        }
        System.out.println(num);
        Thread.sleep(2000);
        System.out.println(fixedWindow());
    }
    

    执行结果:

    符合预期,但是以上代码用在生产环境,需要自定替换 currentTimeSeconds 方法的实现,不能每次都调用 System.currentTimeMillis(),在多线程同时调用下,会有性能问题,可以自己实现一个定时器来返回当前时间

    四.GO代码实现

    var currentTime atomic.Int64
    var currentNumber atomic.Int64
    
    func fixedWindow() int64 {
    	now := time.Now().Unix()
    	ct := currentTime.Load()
    	if now > ct {
    		if currentTime.CAS(ct, now) {
    			currentNumber.Store(0)
    		}
    	}
    
    	return currentNumber.Inc()
    
    }
    

    测试代码:

    func main() {
    	wg := sync.WaitGroup{}
    	for i := 0; i < 10; i++ {
    		wg.Add(1)
    		go func() {
    			for j := 0; j < 10000; j++ {
    				fixedWindow()
    			}
    			wg.Done()
    		}()
    	}
    	wg.Wait()
    	fmt.Println(currentNumber.Load())
    	time.Sleep(2 * time.Second)
    	fmt.Println(fixedWindow())
    }
    

    执行结果:

    符合预期,10个协程的计数在 1s 内能执行完毕,所以最终计数是10w,然后sleep 2s,重置计数,再次调用就返回了 1

    五.资料

    本文 Demo 代码:github

  • 相关阅读:
    Milvus+Attu
    网络编程与黏包问题
    【单片机毕业设计】【mcuclub-hj-005-3】基于单片机的空气质量(天然气或煤气)检测的设计
    币圈是什么意思?币圈开发
    javaSE -运算符,注释,关键字(复习)
    采用普罗米修斯(Prometheus )监控各个指标的含义,类型,以及格式
    用友U8 crm客户关系管理存在任意文件上传漏洞2 附POC
    nextJs13:如何全局配置antd的日期组件datepicker等都显示中文(2步)
    qlib智能量化里的“因子分析“,“多空分析”
    SpringBoot 整合多数据源的事务问题
  • 原文地址:https://www.cnblogs.com/stulzq/p/15904454.html