在数据访问层中使用同步

时间:2009-10-18 20:09:31

标签: java performance caching synchronization data-access-layer

假设我们正在开发实现简单CRUD操作的类来处理DB。此类还维护缓存以提高性能。

public class FooTableGateway {
   Map<Integer, Foo> id2foo = new HashMap<Integer, Foo> ();
   public void getFoo (int id) {
      if (id2foo.containsKey (id) {
          return id2foo.get (id);
      }
      String query = "select ...";
      Connection cn = null;
      Statement st = null;
      ResultSet rs = null;
      try {
          cn = DBUtils.getConnection ();
          st = cn.createStatement ();
          rs = st.executeQuery (query);

          if (!rs.next ()) {
              return null;
          }
          Foo foo = new Foo (rs.getString (1)...);
          id2foo.put (id, foo);
          return foo;
      } catch (SQLException e) {
          ..
      } finally {
          ..
      }
   }

   public boolean addFoo (Foo foo) {
      if (id2foo.values ().contains (foo) {
           return false;
      }
      String query = "insert into ...";
      Connection cn = null;
      Statement st = null;
      ResultSet rs = null;
      try {
          cn = DBUtils.getConnection ();
          st = cn.createStatement ();
          int num = st.executeUpdate (query.toString (),
                  Statement.RETURN_GENERATED_KEYS);
          rs = st.getGeneratedKeys ();
          rs.next ();  
          foo.setId (rs.getInt (1);
          id2foo.put (foo.getId (), foo);
          return true;
      } catch (SQLException e) {
          ..
          return false;
      } finally {
          ..
      }    
   }

   public void updateFoo (Foo foo) {
      //something similar
      ..
   }

   public boolean deleteFoo (int id) {
      //something similar
      ..
   }

}

问题是:应该同步哪部分代码? (当然,我们正在开发网络应用程序。)

如果我将所有调用同步到缓存集合,那么我甚至不确定使用缓存会提高性能。

5 个答案:

答案 0 :(得分:3)

  

问题是:应该同步哪部分代码?

与往常一样,您需要同步对数据的访问,这些数据由一个线程修改并同时被修改,甚至只是被另一个线程读取。

在此示例中,共享数据是您的id2foo字典。因此,请锁定以下声明:

  • 一个人在这里:

      if (id2foo.containsKey (id) {
          return id2foo.get (id);
      }
    
  • 另一个在这里:

      id2foo.put (id, foo);
    

为了最大化并发性(即最小化锁争用),您应该使这些锁的生命周期尽可能短:即仅围绕我上面列出的几个语句,而不是围绕整个getFoo和{{ 1}}方法。

请注意,使用缓存执行此操作可能会使您获得过时的数据;无论如何,这可能发生在数据库中(取决于“事务隔离级别”),但要小心。

  

如果我将所有调用同步到缓存集合,那么我甚至不确定使用缓存会提高性能。

在我看来, 可能会提高性能:假设您没有在缓存中存储太多数据,从缓存中读取数据所花费的时间要比从数据库中读取要少得多,即使您需要等待缓存锁定,特别是如果缓存上的锁定是短暂的,正如我所建议的那样。

如果你想要花哨,可以使用多阅读器/单一写入器锁,这样当多个线程从高速缓存读取而没有人写入高速缓存时,没有争用。

答案 1 :(得分:1)

哇......这是单个方法的很多代码。我真的建议将它分解为一次做事物的方法和对象。

在上面给出的代码中,您应该在读取,写入和删除时同步缓存集合;这将锁定缓存,因此无法进行并行读取。

编写高性能的线程安全缓存并不容易(特别是如果您现在或将来需要将其集群化)。你应该真正看看现有的,如EHCache(http://ehcache.org/)或JBoss Cache(http://jboss.org/jbosscache/)。

答案 2 :(得分:1)

ChrisW使用answer对其进行了攻击 - 您需要保护共享状态不被多个线程访问+修改。此示例中的共享状态是实例级别Map

Map<Integer, Foo> id2foo = new HashMap<Integer, Foo> ();

您正在用作缓存。同步访问和修改它将使其线程安全。

  

你可以采取的另一种方法是   使用一些更高级别   非阻塞实用程序   Java Concurrent Utils api。

具体来说,看看ConcurrentHashMap允许并发读取而不阻塞和可调整的并发更新。

在您的情况下,这将是HashMap的替代品。 ConcurrentMap定义了用于添加到缓存的原子非阻塞V putiFAbsent(T key, V value)方法,您可以安全地从多个线程中读取它而不会锁定。

答案 3 :(得分:0)

此代码存在太多问题。

我认为DAO不应该与获取数据库连接有任何关系;它应该传入或注入课堂。 DAO无法知道它是否在更大的事务环境中使用。一个单独的服务层,其方法对应于了解工作单元的用例,应该是负责获取连接,设置事务和隔离,编组DAO和业务实体以完成用例,提交或回滚事务的服务层,并清理资源。

你在这里有很多事情:持久性,缓存等等。如果你能开始剥离其中的一些责任并把它们放到其他地方,你的生活会更好。我认为你的网关做得太多了。

更新:

你扔进课堂的地图告诉我这是一个很大的错误。我没有看到任何SoftReferences来帮助垃圾收集器。我没有看到任何限制缓存大小或更新时刷新值的努力。这是一种乞求麻烦的方法。编写缓存是一项重大努力。如果您不相信我,请下载EhCache的源代码并将其与您的Map进行比较。这不是微不足道的。

声明性交易没有逻辑 - 另一个巨大的错误。

在充分尊重的情况下,我会重新考虑这种实施。

更好的建议是学习Spring和/或Hibernate。

答案 4 :(得分:0)

你也可以看看JDK 5 + RWLs。引用Wikipedia

  

在此模式中,多个读取器可以并行读取数据,但在写入数据时需要独占锁定。当作者正在编写数据时,读者将被阻止,直到作者完成写作。

请务必查看使用R / W锁定的潜在陷阱,例如this Java专家通讯。