科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道将ReadWriteLock应用于缓存设计

将ReadWriteLock应用于缓存设计

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

在JavaEEdev站点(http://www.javaeedev.com)的设计中,有几类数据是极少变化的,如ArticleCategory(文档分类),ResourceCategory(资源分类),Board(论坛版面)。在对应的DAO实现中,总是一次性取出所有的数据

来源:dev2dev 2007年10月15日

关键字: 应用 技术 缓存 中间件

  • 评论
  • 分享微博
  • 分享邮件

  在JavaEEdev站点(http://www.javaeedev.com)的设计中,有几类数据是极少变化的,如ArticleCategory(文档分类),ResourceCategory(资源分类),Board(论坛版面)。在对应的DAO实现中,总是一次性取出所有的数据,例如:

  

  List getArticleCategories();

  

  此类数据的特点是:数据量很小,读取非常频繁,变化却极慢(几天甚至几十天才变化一次),如果每次通过DAO从数据库获取数据,则增加了数据库服务器的压力。为了在不影响整个系统结构的情况下透明地缓存这些数据,可以在Facade一层通过Proxy模式配合ReadWriteLock实现缓存,而客户端和后台的DAO数据访问对象都不受影响:

  

  

  首先,现有的中间层是由Facade接口和一个FacadeImpl具体实现构成的。对ArticleCategory的相关操作在FacadeImpl中实现如下:

  

  public class FacadeImpl implements Facade {

  protected CategoryDao categoryDao;

  public void setCategoryDao(CategoryDao categoryDao) {

  this.categoryDao = categoryDao;

  }

  // 读操作:

  public ArticleCategory queryArticleCategory(Serializable id) {

  return categoryDao.queryArticleCategory(id);

  }

  // 读操作:

  public List queryArticleCategories() {

  return categoryDao.queryArticleCategories();

  }

  // 写操作:

  public void createArticleCategory(ArticleCategory category) {

  categoryDao.create(category);

  }

  // 写操作:

  public void deleteArticleCategory(ArticleCategory category) {

  categoryDao.delete(category);

  }

  // 写操作:

  public void updateArticleCategory(ArticleCategory category) {

  categoryDao.update(category);

  }

  // 其他方法省略...

  }

  

  设计代理类FacadeCacheProxy,让其实现缓存ArticleCategory的功能:

  

  public class FacadeCacheProxy implements Facade {

  private Facade target;

  public void setFacadeTarget(Facade target) {

  this.target = target;

  }

  // 定义缓存对象:

  private FullCache cache = new FullCache() {

  // how to get real data when cache is unavailable:

  protected List doGetList() {

  return target.queryArticleCategories();

  }

  };

  // 从缓存返回数据:

  public List queryArticleCategories() {

  return cache.getCachedList();

  }

  // 创建新的ArticleCategory后,让缓存失效:

  public void createArticleCategory(ArticleCategory category) {

  target.createArticleCategory(category);

  cache.clearCache();

  }

  // 更新某个ArticleCategory后,让缓存失效:

  public void updateArticleCategory(ArticleCategory category) {

  target.updateArticleCategory(category);

  cache.clearCache();

  }

  // 删除某个ArticleCategory后,让缓存失效:

  public void deleteArticleCategory(ArticleCategory category) {

  target.deleteArticleCategory(category);

  cache.clearCache();

  }

  }

  

  该代理类的核心是调用读方法getArticleCategories()时,直接从缓存对象FullCache中返回结果,当调用写方法(create,update和delete)时,除了调用target对象的相应方法外,再将缓存对象清空。

  FullCache便是实现缓存的关键类。为了实现强类型的缓存,采用泛型实现FullCache:

  

  public abstract class FullCache {

  ...

  }

  

  AbstractId是所有Domain Object的超类,目的是提供一个String类型的主键,同时便于在Hibernate或其他ORM框架中只需要配置一次JPA注解:

  

  @MappedSuperclass

  public abstract class AbstractId {

  protected String id;

  @Id

  @Column(nullable=false, updatable=false, length=32)

  @GeneratedValue(generator="system-uuid")

  @GenericGenerator(name="system-uuid", strategy="uuid")

  public String getId() { return id; }

  public void setId(String id) { this.id = id; }

  }

  

  FullCache实现以下2个功能:

  List getCachedList():获取整个缓存的List

  clearCache():清除所有缓存

  此外,FullCache在缓存失效的情况下,必须从真正的数据源获得数据,因此,抽象方法:

  protected abstract List doGetList()

  负责获取真正的数据。

  下面,用ReadWriteLock实现该缓存模型。

  Java 5平台新增了java.util.concurrent包,该包包含了许多非常有用的多线程应用类,例如ReadWriteLock,这使得开发人员不必自己封装就可以直接使用这些健壮的多线程类。

  ReadWriteLock是一种常见的多线程设计模式。当多个线程同时访问同一资源时,通常,并行读取是允许的,但是,任一时刻只允许最多一个线程写入,从而保证了读写操作的完整性。下图很好地说明了ReadWriteLock的读写并发模型:

  读 写

  读 允许 不允许

  写 不允许 不允许

  当读线程远多于写线程时,使用ReadWriteLock来取代synchronized同步会显著地提高性能,因为大多数时候,并发的多个读线程不需要等待。

  Java 5的ReadWriteLock接口仅定义了如何获取ReadLock和WriteLock的方法,对于具体的ReadWriteLock的实现模式并没有规定,例如,Read优先还是Write优先,是否允许在等待写锁的时候获取读锁,是否允许将一个写锁降级为读锁,等等。

  Java 5自身提供的一个ReadWriteLock的实现是ReentrantReadWriteLock,该ReadWriteLock实现能满足绝大多数的多线程环境,有如下特点:

  支持两种优先级模式,以时间顺序获取锁和以读、写交替优先获取锁的模式;

  当获得了读锁或写锁后,还可重复获取读锁或写锁,即ReentrantLock;

  获得写锁后还可获得读锁,但获得读锁后不可获得写锁;

  支持将写锁降级为读锁,但反之不行;

  支持在等待锁的过程中中断;

  对写锁支持Condition(用于取代wait,notify和notifyAll);

  支持锁的状态检测,但仅仅用于监控系统状态而并非同步控制;

  FullCache采用ReentrantReadWriteLock实现读写同步:

  

  public abstract class FullCache {

  private final ReadWriteLock lock = new ReentrantReadWriteLock();

  private final Lock readLock = lock.readLock(); // 读锁

  private final Lock writeLock = lock.writeLock(); // 写锁

  private List cachedList = null; // 持有缓存的数据,若为null,表示缓存失效

  }

  

  对于clearCache()方法,由于其是一个写操作,故定义如下:

  

  public void clearCache() {

  writeLock.lock();

  cachedList = null;

  writeLock.unlock();

  }

  

  对于get方法,由于是读操作,同时要考虑在缓存失效的情况下更新数据,其实现就稍微复杂一点:

  

  public List getCachedList() {

  // 获得读锁:

  readLock.lock();

  try {

  if(cachedList==null) {

  // 在获得写锁前,必须先释放读锁:

  readLock.unlock();

  writeLock.lock();

  try {

  cachedList = doGetList(); // 获取真正的数据

  }

  finally {

  // 在释放写锁前,先获得读锁:

  readLock.lock();

  writeLock.unlock();

  }

  }

  return cachedList;

  }

  finally {

  // 确保读锁在方法返回前被释放:

  readLock.unlock();

  }

  }

  

  通过适当的装配(例如在Spring IoC容器中),让客户端持有FacadeCacheProxy的引用,就在中间层完全实现了透明的缓存,客户端代码一行也不用更改。

  考虑到多线程模型远比单线程复杂,为了确保FullCache实现的健壮性,编写一个FullCacheTest来执行单元测试:

  

  public class FullCacheTest {

  // count how many hits:

  class Hit {

  private AtomicInteger total = new AtomicInteger(0);

  private AtomicInteger notHit = new AtomicInteger(0);

  public void total() {

  total.incrementAndGet();

  }

  public void notHit() {

  notHit.incrementAndGet();

  }

  public void debug() {

  System.err.println("Total get: " + total.intValue());

  System.err.println("Not hit: " + notHit.intValue());

  System.err.println("Hits: " + ((total.intValue()-notHit.intValue()) * 100 / total.intValue()) + "%");

  }

  }

  private static final int DATA_OPERATION = 10;

  private static final int MAX = 10;

  private static String[] ids = new String[MAX];

  static {

  for(int i=0; i

  ids[i] = UUID.randomUUID().toString();

  }

  }

  private Hit hit;

  private FullCache cache;

  @Before

  public void setUp() {

  hit = new Hit();

  cache = new FullCache() {

  @Override

  protected List doGetList() {

  hit.notHit();

  List list = new ArrayList();

  for(int i=0; i

  ArticleCategory obj = new ArticleCategory();

  obj.setId(ids[i]);

  list.add(obj);

  }

  doSleep(DATA_OPERATION);

  return list;

  }

  @Override

  public List getCachedList() {

  hit.total();

  return super.getCachedList();

  }

  };

  }

  @Test

  public void testMultiThread() {

  final int THREADS = 100;

  final int LOOP_PER_THREAD = 100000;

  List threads = new ArrayList(THREADS);

  // test FullCache.getCachedList(id):

  for(int i=0; i

  threads.add(

  new Thread() {

  public void run() {

  for(int j=0; j

  List list = cache.getCachedList();

  for(int k=0; k

  assertEquals(ids[k], list.get(k).getId());

  }

  }

  }

  }

  );

  }

  // test FullCache.clearCache():

  Thread clearThread = new Thread() {

  public void run() {

  for(;;) {

  cache.clearCache();

  try {

  Thread.sleep(DATA_OPERATION * 2);

  }

  catch(InterruptedException e) {

  break;

  }

  }

  }

  };

  // start all threads:

  clearThread.start();

  for(Thread t : threads) {

  t.start();

  }

  // wait for all threads:

  for(Thread t : threads) {

  try {

  t.join();

  }

  catch(InterruptedException e) {}

  }

  clearThread.interrupt();

  try {

  clearThread.join();

  }

  catch(InterruptedException e) {}

  // statistics:

  hit.debug();

  }

  private static void doSleep(long n) {

  try {

  Thread.sleep(n);

  }

  catch(InterruptedException e) {}

  }

  }

  

  反复运行JUnit测试,均未报错。统计结果如下:

  Total get: 10000000

  Not hit: 7

  Hits: 99%

  执行时间3.9秒。如果用synchronized取代ReadWriteLock,执行时间为204秒,可见性能差异巨大。

  总结:

  接口和实现的分离是必要的,否则难以实现Proxy模式。

  Facade模式和DAO模式都是必要的,否则,一旦数据访问分散在各个Servlet或JSP中,将难以控制缓存读写。

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章