java实现singleflight

目录

应用场景

核心思想

代码演示和讲解

要点

代码

流程示意图

测试结果 

结束语

参考文档


应用场景

缓存热点key失效,缓存击穿,大量请求到数据库。

核心思想

如果有多个并发请求同时请求同一个资源,那么只需要执行一次实际的请求,然后将结果返回给所有的请求者。

代码演示和讲解

要点

 此功能实现主要在以下两点

1)由哪个线程去获取资源。

2)获取资源的过程中其他线程如何阻塞,如何获取到结果。

代码

1)Call类

import java.util.concurrent.CountDownLatch;
public class Call {
    private Object val;//使用Object,可以转换成java对象,更具有通用性
    private CountDownLatch cld;//做阻塞使用,阻塞其他线程

    public Object getVal() {
        return val;
    }

    public void setVal(Object val) {
        this.val = val;
    }

    public void await() {
        try {
            this.cld.await();//阻塞
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void lock() {
        this.cld = new CountDownLatch(1);
    }

    public void done() {
        this.cld.countDown();
    }
}

2)CallManage类 

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;

public class CallManage {
    private final Lock lock = new ReentrantLock();//锁,保证callMap的并发安全
    private Map<String, Call> callMap = new HashMap<>();//存请求key和对应的调用信息,包括返回结果等

    public Object run(String key, Supplier<Object> func)  {
        this.lock.lock();//尝试获取锁,获取不到阻塞等待,这个地方也可以改成this.lock.tryLock(time,unit),防止查库的线程阻塞住引发其他的都阻塞
        Call call = this.callMap.get(key);

        if (call != null) {//call != null流程
            //已经存在处理中的请求
            this.lock.unlock();//释放锁,下面的call.await()维持阻塞
            call.await();//第一个线程还没调用call.done()之前回在此阻塞,调用call.done()之后call已经有了val,可以直接获取
            return call.getVal();
        }
        //call=null,是当前key接收到的第一个请求
        call = new Call();
        call.lock();//初始化CountDownLatch
        this.callMap.put(key, call);//call放入map,相同key的其他线程也是获取同一个call
        this.lock.unlock();//释放锁后,相同key的其他线程获取锁之后会进入call != null流程,此流程等待读取

        call.setVal(func.get());//查询数据库,获取缓存,放入到call.val
        call.done();//调用call.done之前,其他线程处于call.await阻塞等待状态

        this.lock.lock();//也可以换成只锁自己当前key
        this.callMap.remove(key);//删除key,这一轮的并发结束
        this.lock.unlock();

        return call.getVal();//返回value,(是查询数据库之后的value)
    }
}

3)TestClass类 

public class TestClass {

    public static void main(String[] args) throws InterruptedException {
        //缓存失效后,走的流程
        CallManage callManage = new CallManage();//全局bean
        int count = 1000;
        String key = "key_asdf";
        for (int i = 0; i < count; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Object value =  callManage.run(key,()->{
                        System.out.println("查询数据库,更新缓存");
                        try {
                            Thread.sleep(1000);//休眠,模拟查库的时间
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        return "新的缓存:"+System.currentTimeMillis();//这个地方也可以是返回java bean,更通用
                    });
                        System.out.println("获取到的值为"+((String)value).toString());//如果返回的是java bean,这个地方可以强转bean

                }
            }).start();

        }
    }
}

流程示意图

测试结果 

1000个线程,数据库查询流程只走了一次

结束语

如有任何问题可以留言,我会找时间回复,如有异议或者建议也欢迎提出。

参考文档

1.https://www.cnblogs.com/xiaozhe97/p/13702010.html

2.Golang中如何使用Singleflight库进行并发请求合并_Jack_InfoQ写作社区

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值