一个与 Ehcache 相关的死锁案例

最近朋友分享一个与 Ehcache 相关的死锁案例,记录下。

0. 一个思考题

缓存击穿是指高并发的请求访问同一个 key,当这个 key 失效后,所有的请求都会传递到数据库,数据库因而崩溃。

为了防止数据库崩溃,我们需要在应用层面拦截这些请求,比如控制一个应用加载一个 key 时只有一个请求到数据库。

如果用加锁的方法,每个 key 一个锁,那么 100 万个 key 就会有 100 万个锁实例在 JVM 里。如果 key 过期了,锁实例可能还得过期,特麻烦。

如果所有的 key 共享同一个锁,那么这个锁就会成为瓶颈。

该如何控制是好?

1. 背景说明

一个面向海量用户的 api 服务,需要提供用户信息查询的功能。

缓存有两层,第一层是 JVM 内基于 Ehcache 的,第二层是 memcached ,查找缓存时首先从本机 Ehcache 查询,找不到再去 memcached 查找,还找不到则从 Loader 接口加载数据并存入相应的缓存。

用户信息由 会员信息、个人信息、家庭信息 等组成,这些信息位于不同的模块,存储在不同的表或库里。

在 Ehcache 里,用户信息有一层缓存,会员信息、个人信息、家庭信息 也有对应的缓存。

以 ID 为 “007” 的用户举例,用户信息缓存对应的 key 为 “User:007″,会员信息对应的缓存 key 为 “member:007″,个人信息对应的缓存 key 为 “personal:007″,家庭信息对应的缓存 key 为 “family:007″。
其中用户信息对应的缓存的过期时间是比较短的。

当用户信息在缓存里都查找不到时,它的 Loader 实现会调用 MemerbService 加载会员信息、调用 PersonalService 加载个人信息,FamilyService 加载家庭信息,然后合并成用户信息保存在缓存里。MemerbService/PersonalService/FamilyService加载信息时也是先从缓存查找,两级缓存都查找不到时也调用对应的 Loader 从表里加载数据。

使用的 Ehcache 的版本是 ehcache-core-2.6.9 。

继续阅读

LMAX 架构–笔记

原文 The LMAX Architecture

原文介绍了 LMAX 支持高性能、低延迟的架构,还介绍了 Disruptor 这个框架的设计缘由。

LMAX 用 3Ghz 的 CPU 单线程处理达到 600w TPS,意味着要在 500 个时钟周期内处理完一个事务。(dual-socket quad-core,32GB RAM)

LMAX 整体架构:

Business Logic Processor,BLP

只是简单的 Java 代码,不依赖于任何框架。单线程执行,全内存操作, 顺序地获取输入的消息。

要操作的数据全在内存里。好处有两点:快 和 简化了编程(没有对象/关系映射)。

用 Event Sourcing 事件溯源机制来保证 BLP 的状态是可以重建的,输入事件由 input disruptor 来进行持久化。事件溯源机制可以采用快照的方式来缩短重建需要的时间。

LMAX 采用多个 BLP 同时处理同样的事件(两个在同一个数据中心,第三个在灾备中心),但只有一个 BLP 的输出是有效的。当存活的 processor 失败时,系统切换到另一个。

事件溯源的另一个好处是诊断方便,可以把事件拷贝到开发环境进行重放。

继续阅读

HikariCP 连接池–高性能数据结构

HikariCP,日语的含义是“光”,号称目前最快的数据库连接池。

它的高性能来自两个方面:

  1. 利用 Javassist 在字节码层面优化代理对象的创建,提升代理对象的调用性能。
  2. 在数据结构上采用定制的 FastListConcurrentBag 来提升性能。

本文主要关注这两种数据结构的实现。

1. FastList

我们用 JDBC 编程的时候,首先是获取 Connection、创建 Statement、执行查询得到 ResultSet,执行完成后依次关闭:ResultSetStatementConnection,特别是一个逻辑里创建了多个 PreparedStatement 时,一般用完就关闭。

为了防止用户忘了关闭 StatementResultSet,连接池需要跟踪创建的 StatementResultSet,在连接返回到连接池时关闭这两类资源。

public abstract class ProxyConnection implements Connection
{
    // 跟踪本连接创建的语句
    private final FastList<Statement> openStatements;

   private synchronized <T extends Statement> T trackStatement(final T statement) {
      openStatements.add(statement);
      return statement;
   }

   // Statement 关闭时回调此方法
   final synchronized void untrackStatement(final Statement statement) {
      openStatements.remove(statement);
   }

   // 创建语句时加入跟踪列表
   public Statement createStatement() throws SQLException {
      return ProxyFactory.getProxyStatement(this, trackStatement(delegate.createStatement()));
   }

    // Connection.close 方法回调此方法关闭打开的语句
    private synchronized void closeStatements() {
      final int size = openStatements.size();
      if (size > 0) {
         for (int i = 0; i < size && delegate != ClosedConnection.CLOSED_CONNECTION; i++) {
            // 利用 try-with-resources 机制进行关闭
            try (Statement ignored = openStatements.get(i)) {
               // automatic resource cleanup
            } catch (SQLException e) {
               LOGGER.warn("{} - Connection {} marked as broken because of an exception closing open statements during Connection.close()",
                           poolEntry.getPoolName(), delegate);
               leakTask.cancel();
               poolEntry.evict("(exception closing Statements during Connection.close())");
               // 包装的代理对象不再持有底层的连接
               delegate = ClosedConnection.CLOSED_CONNECTION;
            }
         }

         openStatements.clear();
      }
}

关闭的顺序跟创建的顺序是相反的,要关闭并移除的对象一般在列表的末尾。而 ArrayList 的移除对象是从列表头部开始的,在这种场景下不高效。FastList 的实现是逆序查找要删除对象;对于根据下标进行的操作,移除了对下标合法性的检查,由连接池来保证。

继续阅读