查看原文
其他

【项目实记】使用内存做关键信息的缓存来提升 QPS 和 减少开销

码匠笔记 码匠笔记 2020-08-21

背景

当前业务需要使用 Oauth 授权的 accessToken 调用第三方系统获取用户资料。这里简化说明完成和第三方系统的对接需要两个接口。

  1. 通过 appkey 获取 accessToken。 accessToken 2 小时过期,接口有 quota 限制。

  2. 使用 accessToken 获取用户信息,这个 token 是通用的,只要是当前 appkey 下面的用户都可以使用。

设计

  1. 考虑到有 quota 限制问题,所以一定要有缓存机制,不然获取用户信息的接口请求量大的时候 quota 就不够用。

  2. accessToken 有两个小时过期问题,所有缓存要有失效机制。

  3. 一个 appkey 有且只有一个 accessToken,并且不常变,可以考虑直接使用内存缓存。如果使用 Redis等分布式缓存系统,也会因为频繁网络请求。

下面的表格来源于 Jeff Dean的一个PPT,里面罗列了不同级别的IO时间,这正是我们评估如何设计系统的必要因素。




L1 cache reference0.5 ns
Branch mispredict5 ns
L2 cache reference7 ns
Mutex lock/unlock100 ns
Main memory reference100 ns
Compress 1K bytes with Zippy10,000 ns0.01 ms
Send 1K bytes over 1 Gbps network10,000 ns0.01 ms
Read 1 MB sequentially from memory250,000 ns0.25 ms
Round trip within same datacenter500,000 ns0.5 ms
Disk seek10,000,000 ns10 ms
Read 1 MB sequentially from network10,000,000 ns10 ms
Read 1 MB sequentially from disk30,000,000 ns30 ms
Send packet CA->Netherlands->CA150,000,000 ns150 ms

由上面表格,我们可以清楚的看出从网络上面获取1M数据和从内存中读取1M数据的差别。为什么说到这里呢,因为随着我们的用户的增加,集群的扩展,很少的情况下是把缓存数据库或者其他缓存中间件和应用程序放在一台服务器上,大部分情况都是分布式的应用系统和缓存系统,所以避免不了的我们需要考虑网络而的开销。
回到当前话题,对于一个 accessToken 占用内存足够小,即便是分布式系统每一个系统中都存储一个也不为过,只是能解决过期更新问题就好了。

实现

正好 Guava 就提供了这个功能, GuavaCache 缓存类似于 ConcurrentMap,但不完全相同。 最根本的区别是, ConcurrentMap 会持续添加到其中的所有元素,如果你不手动删除它们会一直存在。然而 GuavaCache 可以通过缓存的大小,过期时间,或者其他策略自动地移除元素,来限制其内存占用。

引入依赖

  1. <dependency>

  2.    <groupId>com.google.guava</groupId>

  3.    <artifactId>guava</artifactId>

  4.    <version>19.0</version>

  5. </dependency>

编写实现类

  1. public class UserInfoProvider {

  2.    public static void main(String[] args) {

  3.        for (int i =0;i<100;i++) {

  4.            String accessToken = new AccessTokenProvider().getAccessToken();

  5.            System.out.println(accessToken);

  6.            try {

  7.                Thread.sleep(1000L);

  8.            } catch (InterruptedException e) {

  9.                e.printStackTrace();

  10.            }

  11.        }

  12.    }

  13. }

  14. class AccessTokenProvider {

  15.    private final static String KEY = "key";

  16.    static Cache<String, Optional<String>> cache = CacheBuilder.newBuilder()

  17.            .expireAfterWrite(3, TimeUnit.SECONDS)

  18.            .removalListener(new RemovalListener<String, Optional<String>>() {

  19.                @Override

  20.                public void onRemoval(RemovalNotification<String, Optional<String>> notification) {

  21.                    System.out.println("cache expired, remove key : " + notification.getKey());

  22.                }

  23.            }).build();

  24.    public String getAccessToken() {

  25.        try {

  26.            Optional<String> stringOptional = cache.get(KEY, new Callable<Optional<String>>() {

  27.                @Override

  28.                public Optional<String> call() throws Exception {

  29.                    return Optional.of(getRemoteAccessToken());

  30.                }

  31.            });

  32.            return stringOptional.or("");

  33.        } catch (ExecutionException e) {

  34.            return null;

  35.        }

  36.    }

  37.    private String getRemoteAccessToken() {

  38.        return "accessToken:" + new Random().nextInt(1000);

  39.    }

  40. }

简单对上面逻辑进行讲解

  1. getRemoteAccessToken 是模拟远程获取 token

  2. getAccessToken 是先从 cache 中获取,如果没有获取到,从远程获取。

  3. expireAfterWrite(3,TimeUnit.SECONDS) 是3秒过期,这是为了测试。

  4. removalListener 监听过期。

  5. UserInfoProvider,sleep 3秒看一下更新情况。

由下面日志可见执行情况

  1. accessToken:651

  2. accessToken:651

  3. accessToken:651

  4. cache expired, remove key : key

  5. accessToken:639

  6. accessToken:639

  7. accessToken:639

  8. cache expired, remove key : key

  9. accessToken:850

  10. accessToken:850

  11. accessToken:850

我们使用的还只是 GuavaCache 的冰山一角,它可以支容量和时间多种策略配置回收策略,同时它是良好的 LRU 实现。如上场景比较简单,如果你考虑缓存其他需求,需要考虑 refreshAfterWritemaximumSize 等配合使用,避免缓存击穿和性能问题。

原理

GuavaCache 的设计和 ConcurrentHashMap 非常类似,使用多个 segments 方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求。看过 ConcurrentHashMap 实现原理的朋友跟进代码一看了然。当然它的设计更复杂一些,跟进代码的话也比较简单,在 get 的时候他会根据不同的策略进行比对是否过期,如果过期再进行相应的通知操作。同时他巧妙的使用的 WeakReference,这样可以利用 GC 来做删除数据的通知。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存