`

事务策略: 了解事务陷阱

阅读更多

事务策略: 了解事务陷阱

在 Java 平台中实现事务时要注意的常见错误

developerWorks
文档选项
将打印机的版面设置成横向打印模式

打印本页

将此页作为电子邮件发送

将此页作为电子邮件发送

英文原文

英文原文


级别: 中级

Mark Richards, 主管和高级技术架构师, Collaborative Consulting, LLC

2009 年 3 月 06 日

事务处理的目标应该是实现数据的高度完整性和一致性。本文是为 Java 平台开发有效事务策略 系列文章 的第一篇,介绍了一些妨碍您实现此目标的常见事务陷阱。本系列作者 Mark Richards 通过使用 Spring Framework 和企业 JavaBeans(Enterprise JavaBeans,EJB)3.0 规范中的代码示例解释了这些极其常见的错误。

在应用程序中使用事务常常是为了维护高度的数据完整性和一致性。如果不关心数据的质量,就不必使用事务。毕竟,Java 平台中的事务支持会降低性能,引发锁定问题和数据库并发性问题,而且会增加应用程序的复杂性。

关于本系列

事务提高了数据的质量、完整性和一致性,使应用程序更健壮。在 Java 应用程序中实现成功的事务处理不是一件容易的事,设计和编码几乎一样重要。在这份新的 系列文章 中,Mark Richards 将带领您设计一个有效的事务策略,适合从简单应用程序到高性能事务处理等各种用例。

但是不关心事务的开发人员就会遇到麻烦。几乎所有与业务相关的应用程序都需要高度的数据质量。金融投资行业在失败的交易上浪费数百亿美元,不好的数据是导致这种结果的第二大因素(请参阅 参考资料)。尽然缺少事务支持只是导致坏数据的一个因素(但是是主要的因素),但是完全可以这样认为,在金融投资行业浪费掉数十亿美元是由于缺少事务支持或事务支持不充分。

忽略事务支持是导致问题的另一个原因。我常常听到 “我们的应用程序中不需要事务支持,因为这些应用程序从来不会失败” 之类的说法。是的,我知道有些应用程序极少或从来不会抛出异常。这些应用程序基于编写良好的代码、编写良好的验证例程,并经过了充分的测试,有代码覆盖支持,可以避免性能损耗和与事务处理有关的复杂性。这种类型的应用程序只需考虑事务支持的一个特性:原子性。原子性确保所有更新被当作一个单独的单元,要么全部提交,要么回滚。但是回滚或同时更新不是事务支持的惟一方面。另一方面,隔离性 将确保某一工作单元独立于其他工作单元。没有适当的事务隔离性,其他工作单元就可以访问某一活动工作单元所做的更新,即使该工作单元还未完成。这样,就会基于部分数据作出业务决策,而这会导致失败的交易或产生其他负面(或代价昂贵的)结果。

迟做总比不做好

我是在 2000 年年初开始关注事务处理问题的,当时我正在研究一个客户端站点,我发现项目计划中有一项内容优先于系统测试任务。它称为实现事务支持。当然,在某个主要应用程序差不多准备好进行系统测试时,给它添加事务支持是非常简单的。遗憾的是,这种方法实在太普通。至少这个项目(与大多数项目不同)确实 实现了事务支持,尽管是在开发周期快结束时。

因此,考虑到坏数据的高成本和负面影响,以及事务的重要性(和必须性)这些基本常识,您需要使用事务处理并学习如何处理可能出现的问题。您在应用程序中添加事务支持后常常会出现很多问题。事务在 Java 平台中并不总是如预想的那样工作。本文会探讨其中的原因。我将借助代码示例,介绍一些我在该领域中不断看到的和经历的常见事务陷阱,大部分是在生产环境中。

虽然本文中的大多数代码示例使用的是 Spring Framework(version 2.5),但事务概念与 EJB 3.0 规范中的是相同的。在大多数情况下,用 EJB 3.0 规范中的 _cnnew1@TransactionAttribute 注释替换 Spring Framework @Transactional 注释即可。如果这两种框架使用了不同的概念和技术,我将同时给出 Spring Framework 和 EJB 3.0 源代码示例。

本地事务陷阱

最好先从最简单的场景开始,即使用本地事务,一般也称为数据库事务。在数据库持久性的早期(例如 JDBC),我们一般会将事务处理委派给数据库。毕竟这是数据库应该做的。本地事务很适合执行单一插入、更新或删除语句的逻辑工作单元(LUW)。例如,考虑清单 1 中的简单 JDBC 代码,它向 TRADE 表插入一份股票交易订单:


清单 1. 使用 JDBC 的简单数据库插入
				
@Stateless
public class TradingServiceImpl implements TradingService {
   @Resource SessionContext ctx;
   @Resource(mappedName="java:jdbc/tradingDS") DataSource ds;

   public long insertTrade(TradeData trade) throws Exception {
      Connection dbConnection = ds.getConnection();
      try {
         Statement sql = dbConnection.createStatement();
         String stmt =
            "INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE, STATE)"
          + "VALUES ("
          + trade.getAcct() + "','"
          + trade.getAction() + "','"
          + trade.getSymbol() + "',"
          + trade.getShares() + ","
          + trade.getPrice() + ",'"
          + trade.getState() + "')";
         sql.executeUpdate(stmt, Statement.RETURN_GENERATED_KEYS);
         ResultSet rs = sql.getGeneratedKeys();
         if (rs.next()) {
            return rs.getBigDecimal(1).longValue();
         } else {
            throw new Exception("Trade Order Insert Failed");
         }
      } finally {
         if (dbConnection != null) dbConnection.close();
      }
   }
}

清单 1 中的 JDBC 代码没有包含任何事务逻辑,它只是在数据库中保存 TRADE 表中的交易订单。在本例中,数据库处理事务逻辑。

在 LUW 中,这是一个不错的单个数据库维护操作。但是如果需要在向数据库插入交易订单的同时更新帐户余款呢?如清单 2 所示:


清单 2. 在同一方法中执行多次表更新
				
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      throw up;
   }
} 

在本例中,insertTrade() 和 updateAcct() 方法使用不带事务的标准 JDBC 代码。insertTrade() 方法结束后,数据库保存(并提交了)交易订单。如果 updateAcct() 方法由于任意原因失败,交易订单仍然会在 placeTrade()方法结束时保存在 TRADE 表内,这会导致数据库出现不一致的数据。如果 placeTrade() 方法使用了事务,这两个活动都会包含在一个 LUW 中,如果帐户更新失败,交易订单就会回滚。

随着 Java 持久性框架的不断普及,如 Hibernate、TopLink 和 Java 持久性 API(Java Persistence API,JPA),我们很少再会去编写简单的 JDBC 代码。更常见的情况是,我们使用更新的对象关系映射(ORM)框架来减轻工作,即用几个简单的方法调用替换所有麻烦的 JDBC 代码。例如,要插入 清单 1 中 JDBC 代码示例的交易订单,使用带有 JPA 的 Spring Framework,就可以将 TradeData 对象映射到 TRADE 表,并用清单 3 中的 JPA 代码替换所有 JDBC 代码:


清单 3. 使用 JPA 的简单插入
				
public class TradingServiceImpl {
    @PersistenceContext(unitName="trading") EntityManager em;

    public long insertTrade(TradeData trade) throws Exception {
       em.persist(trade);
       return trade.getTradeId();
    }
}

注意,清单 3 在 EntityManager 上调用了 persist() 方法来插入交易订单。很简单,是吧?其实不然。这段代码不会像预期那样向 TRADE 表插入交易订单,也不会抛出异常。它只是返回一个值 0 作为交易订单的键,而不会更改数据库。这是事务处理的主要陷阱之一:基于 ORM 的框架需要一个事务来触发对象缓存与数据库之间的同步。这通过一个事务提交完成,其中会生成 SQL 代码,数据库会执行需要的操作(即插入、更新、删除)。没有事务,就不会触发 ORM 去生成 SQL 代码和保存更改,因此只会终止方法 — 没有异常,没有更新。如果使用基于 ORM 的框架,就必须利用事务。您不再依赖数据库来管理连接和提交工作。

这些简单的示例应该清楚地说明,为了维护数据完整性和一致性,必须使用事务。不过对于在 Java 平台中实现事务的复杂性和陷阱而言,这些示例只是涉及了冰山一角。





回页首


Spring Framework @Transactional 注释陷阱

您将测试 清单 3 中的代码,发现 persist() 方法在没有事务的情况下不能工作。因此,您通过简单的网络搜索查看几个链接,发现如果使用 Spring Framework,就需要使用 @Transactional 注释。于是您在代码中添加该注释,如清单 4 所示:


清单 4. 使用 @Transactional 注释
				
public class TradingServiceImpl {
   @PersistenceContext(unitName="trading") EntityManager em;

   @Transactional
   public long insertTrade(TradeData trade) throws Exception {
      em.persist(trade);
      return trade.getTradeId();
   }
}

现在重新测试代码,您发现上述方法仍然不能工作。问题在于您必须告诉 Spring Framework,您正在对事务管理应用注释。除非您进行充分的单元测试,否则有时候很难发现这个陷阱。这通常只会导致开发人员在 Spring 配置文件中简单地添加事务逻辑,而不会使用注释。

要在 Spring 中使用 @Transactional 注释,必须在 Spring 配置文件中添加以下代码行:

 

<tx:annotation-driven transaction-manager="transactionManager"/>

 

transaction-manager 属性保存一个对在 Spring 配置文件中定义的事务管理器 bean 的引用。这段代码告诉 Spring 在应用事务拦截器时使用 @Transaction 注释。如果没有它,就会忽略 @Transactional 注释,导致代码不会使用任何事务。

让基本的 @Transactional 注释在 清单 4 的代码中工作仅仅是开始。注意,清单 4 使用 @Transactional 注释时没有指定任何额外的注释参数。我发现许多开发人员在使用 @Transactional 注释时并没有花时间理解它的作用。例如,像我一样在清单 4 中单独使用 @Transactional 注释时,事务传播模式被设置成什么呢?只读标志被设置成什么呢?事务隔离级别的设置是怎样的?更重要的是,事务应何时回滚工作?理解如何使用这个注释对于确保在应用程序中获得合适的事务支持级别非常重要。回答我刚才提出的问题:在单独使用不带任何参数的 @Transactional 注释时,传播模式要设置为 REQUIRED,只读标志设置为 false,事务隔离级别设置为 READ_COMMITTED,而且事务不会针对受控异常(checked exception)回滚。





回页首


@Transactional 只读标志陷阱

我在工作中经常碰到的一个常见陷阱是 Spring @Transactional 注释中的只读标志没有得到恰当使用。这里有一个快速测试方法:在使用标准 JDBC 代码获得 Java 持久性时,如果只读标志设置为 true,传播模式设置为SUPPORTS,清单 5 中的 @Transactional 注释的作用是什么呢?


清单 5. 将只读标志与 SUPPORTS 传播模式结合使用 — JDBC
				
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public long insertTrade(TradeData trade) throws Exception {
   //JDBC Code...
}

当执行清单 5 中的 insertTrade() 方法时,猜一猜会得到下面哪一种结果:

  • 抛出一个只读连接异常
  • 正确插入交易订单并提交数据
  • 什么也不做,因为传播级别被设置为 SUPPORTS

是哪一个呢?正确答案是 B。交易订单会被正确地插入到数据库中,即使只读标志被设置为 true,且事务传播模式被设置为 SUPPORTS。但这是如何做到的呢?由于传播模式被设置为 SUPPORTS,所以不会启动任何事物,因此该方法有效地利用了一个本地(数据库)事务。只读标志只在事务启动时应用。在本例中,因为没有启动任何事务,所以只读标志被忽略。

如果是这样的话,清单 6 中的 @Transactional 注释在设置了只读标志且传播模式被设置为 REQUIRED 时,它的作用是什么呢?


清单 6. 将只读标志与 REQUIRED 传播模式结合使用 — JDBC
				
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
   //JDBC code...
}

执行清单 6 中的 insertTrade() 方法会得到下面哪一种结果呢:

  • 抛出一个只读连接异常
  • 正确插入交易订单并提交数据
  • 什么也不做,因为只读标志被设置为 true

根据前面的解释,这个问题应该很好回答。正确的答案是 A。会抛出一个异常,表示您正在试图对一个只读连接执行更新。因为启动了一个事务(REQUIRED),所以连接被设置为只读。毫无疑问,在试图执行 SQL 语句时,您会得到一个异常,告诉您该连接是一个只读连接。

关于只读标志很奇怪的一点是:要使用它,必须启动一个事务。如果只是读取数据,需要事务吗?答案是根本不需要。启动一个事务来执行只读操作会增加处理线程的开销,并会导致数据库发生共享读取锁定(具体取决于使用的数据库类型和设置的隔离级别)。总的来说,在获取基于 JDBC 的 Java 持久性时,使用只读标志有点毫无意义,并会启动不必要的事务而增加额外的开销。

使用基于 ORM 的框架会怎样呢?按照上面的测试,如果在结合使用 JPA 和 Hibernate 时调用 insertTrade() 方法,清单 7 中的 @Transactional 注释会得到什么结果?


清单 7. 将只读标志与 REQUIRED 传播模式结合使用 — JPA
				
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
   em.persist(trade);
   return trade.getTradeId();
}

清单 7 中的 insertTrade() 方法会得到下面哪一种结果:

  • 抛出一个只读连接异常
  • 正确插入交易订单并提交数据
  • 什么也不做,因为 readOnly 标志被设置为 true

正确的答案是 B。交易订单会被准确无误地插入数据库中。请注意,上一示例表明,在使用 REQUIRED 传播模式时,会抛出一个只读连接异常。使用 JDBC 时是这样。使用基于 ORM 的框架时,只读标志只是对数据库的一个提示,并且一条基于 ORM 框架的指令(本例中是 Hibernate)将对象缓存的 flush 模式设置为 NEVER,表示在这个工作单元中,该对象缓存不应与数据库同步。不过,REQUIRED 传播模式会覆盖所有这些内容,允许事务启动并工作,就好像没有设置只读标志一样。

这令我想到了另一个我经常碰到的主要陷阱。阅读了前面的所有内容后,您认为如果只对 @Transactional 注释设置只读标志,清单 8 中的代码会得到什么结果呢?


清单 8. 使用只读标志 — JPA
				
@Transactional(readOnly = true)
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

清单 8 中的 getTrade() 方法会执行以下哪一种操作?

  • 启动一个事务,获取交易订单,然后提交事务
  • 获取交易订单,但不启动事务

正确的答案是 A。一个事务会被启动并提交。不要忘了,@Transactional 注释的默认传播模式是 REQUIRED。这意味着事务会在不必要的情况下启动。根据使用的数据库,这会引起不必要的共享锁,可能会使数据库中出现死锁的情况。此外,启动和停止事务将消耗不必要的处理时间和资源。总的来说,在使用基于 ORM 的框架时,只读标志基本上毫无用处,在大多数情况下会被忽略。但如果您坚持使用它,请记得将传播模式设置为 SUPPORTS(如清单 9 所示),这样就不会启动事务:


清单 9. 使用只读标志和 SUPPORTS 传播模式进行选择操作
				
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
}

另外,在执行读取操作时,避免使用 @Transactional 注释,如清单 10 所示:


清单 10. 删除 @Transactional 注释进行选择操作
				
public TradeData getTrade(long tradeId) throws Exception {
   return em.find(TradeData.class, tradeId);
} 





回页首


REQUIRES_NEW 事务属性陷阱

不管是使用 Spring Framework,还是使用 EJB,使用 REQUIRES_NEW 事务属性都会得到不好的结果并导致数据损坏和不一致。REQUIRES_NEW 事务属性总是会在启动方法时启动一个新的事务。许多开发人员都错误地使用REQUIRES_NEW 属性,认为它是确保事务启动的正确方法。考虑清单 11 中的两个方法:


清单 11. 使用 REQUIRES_NEW 事务属性
				
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {...}

@Transactional(propagation=Propagation.REQUIRES_NEW)
public void updateAcct(TradeData trade) throws Exception {...}

注意,清单 11 中的两个方法都是公共方法,这意味着它们可以单独调用。当使用 REQUIRES_NEW 属性的几个方法通过服务间通信或编排在同一逻辑工作单元内调用时,该属性就会出现问题。例如,假设在清单 11 中,您可以独立于一些用例中的任何其他方法来调用 updateAcct() 方法,但也有在 insertTrade() 方法中调用 updateAcct() 方法的情况。现在如果调用 updateAcct() 方法后抛出异常,交易订单就会回滚,但帐户更新将会提交给数据库,如清单 12 所示:


清单 12. 使用 REQUIRES_NEW 事务属性的多次更新
				
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {
   em.persist(trade);
   updateAcct(trade);
   //exception occurs here! Trade rolled back but account update is not!
   ...
}

之所以会发生这种情况是因为 updateAcct() 方法中启动了一个新事务,所以在 updateAcct() 方法结束后,事务将被提交。使用 REQUIRES_NEW 事务属性时,如果存在现有事务上下文,当前的事务会被挂起并启动一个新事务。方法结束后,新的事务被提交,原来的事务继续执行。

由于这种行为,只有在被调用方法中的数据库操作需要保存到数据库中,而不管覆盖事务的结果如何时,才应该使用 REQUIRES_NEW 事务属性。比如,假设尝试的所有股票交易都必须被记录在一个审计数据库中。出于验证错误、资金不足或其他原因,不管交易是否失败,这条信息都需要被持久化。如果没有对审计方法使用 REQUIRES_NEW 属性,审计记录就会连同尝试执行的交易一起回滚。使用 REQUIRES_NEW 属性可以确保不管初始事务的结果如何,审计数据都会被保存。这里要注意的一点是,要始终使用 MANDATORY 或 REQUIRED 属性,而不是 REQUIRES_NEW,除非您有足够的理由来使用它,类似审计示例中的那些理由。





回页首


事务回滚陷阱

我将最常见的事务陷阱留到最后来讲。遗憾的是,我在生产代码中多次???到这个错误。我首先从 Spring Framework 开始,然后介绍 EJB 3。

到目前为止,您研究的代码类似清单 13 所示:


清单 13. 没有回滚支持
				
@Transactional(propagation=Propagation.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      throw up;
   }
}

假设帐户中没有足够的资金来购买需要的股票,或者还没有准备购买或出售股票,并抛出了一个受控异常(例如 FundsNotAvailableException),那么交易订单会保存在数据库中吗?还是整个逻辑工作单元将执行回滚?答案出乎意料:根据受控异常(不管是在 Spring Framework 中还是在 EJB 中),事务会提交它还未提交的所有工作。使用清单 13,这意味着,如果在执行 updateAcct() 方法期间抛出受控异常,就会保存交易订单,但不会更新帐户来反映交易情况。

这可能是在使用事务时出现的主要数据完整性和一致性问题了。运行时异常(即非受控异常)自动强制执行整个逻辑工作单元的回滚,但受控异常不会。因此,清单 13 中的代码从事务角度来说毫无用处;尽管看上去它使用事务来维护原子性和一致性,但事实上并没有。

尽管这种行为看起来很奇怪,但这样做自有它的道理。首先,不是所有受控异常都是不好的;它们可用于事件通知或根据某些条件重定向处理。但更重要的是,应用程序代码会对某些类型的受控异常采取纠正操作,从而使事务全部完成。例如,考虑下面一种场景:您正在为在线书籍零售商编写代码。要完成图书的订单,您需要将电子邮件形式的确认函作为订单处理的一部分发送。如果电子邮件服务器关闭,您将发送某种形式的 SMTP 受控异常,表示邮件无法发送。如果受控异常引起自动回滚,整个图书订单就会由于电子邮件服务器的关闭全部回滚。通过禁止自动回滚受控异常,您可以捕获该异常并执行某种纠正操作(如向挂起队列发送消息),然后提交剩余的订单。

使用 Declarative 事务模式(本系列的第 2 部分将进行更加详细的描述)时,必须指定容器或框架应该如何处理受控异常。在 Spring Framework 中,通过 @Transactional 注释中的 rollbackFor 参数进行指定,如清单 14 所示:


清单 14. 添加事务回滚支持 — Spring
				
@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      throw up;
   }
}

注意,@Transactional 注释中使用了 rollbackFor 参数。这个参数接受一个单一异常类或一组异常类,您也可以使用 rollbackForClassName 参数将异常的名称指定为 Java String 类型。还可以使用此属性的相反形式(noRollbackFor)指定除某些异常以外的所有异常应该强制回滚。通常大多数开发人员指定 Exception.class 作为值,表示该方法中的所有异常应该强制回滚。

在回滚事务这一点上,EJB 的工作方式与 Spring Framework 稍微有点不同。EJB 3.0 规范中的 @TransactionAttribute 注释不包含指定回滚行为的指令。必须使用 SessionContext.setRollbackOnly() 方法将事务标记为执行回滚,如清单 15 所示:


清单 15. 添加事务回滚支持 — EJB
				
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
   try {
      insertTrade(trade);
      updateAcct(trade);
      return trade;
   } catch (Exception up) {
      //log the error
      sessionCtx.setRollbackOnly();
      throw up;
   }
} 

调用 setRollbackOnly() 方法后,就不能改变主意了;惟一可能的结果是在启动事务的方法完成后回滚事务。本系列后续文章中描述的事务策略将介绍何时、何处使用回滚指令,以及何时使用 REQUIRED 与 MANDATORY 事务属性。





回页首


结束语

用于在 Java 平台中实现事务的代码不是太复杂;但是,如何使用以及如何配置它就有一些复杂了。在 Java 平台中实现事务支持有许多陷阱(包括一些我未在本文中讨论的、不是很常见的陷阱)。大多数陷阱最大的问题是,不会有任何编译器警告或运行时错误告诉您事务实现是不正确的。而且,与本文开头的 “迟做总比不做好” 部分的内容相反,实现事务支持不仅仅是一个编码工作。开发一个完整的事务策略涉及大量的设计工作。事务策略 系列的其余部分将指导您如何设计针对从简单应用程序到高性能事务处理用例的有效事务策略。



参考资料

学习

讨论


关于作者

Mark Richards

Mark Richards 是 Collaborative Consulting, LLC 的主管和高级技术架构师。他是第 2 版 Java Message Service(O'Reilly,2009)和 Java Transaction Design Strategies(C4Media Publishing,2006)的作者。他也是其他几本书的丛集著者,包括 97 Things Every Software Architect Should Know(O'Reilly,2009)、NFJS Anthology Volume 1(Pragmatic Bookshelf,2006)和 NFJS Anthology Volume 2(Pragmatic Bookshelf,2007)。Mark 拥有 IBM、Sun、The Open Group 和 BEA 的架构师和开发人员证书。他是 No Fluff Just Stuff 专题讨论会的定期演讲者,并在世界各地的其他会议和用户组中进行演讲。


分享到:
评论

相关推荐

    java陷阱常见面试题

    理解对象生命周期、引用类型和内存回收策略至关重要。 2. String对象与字符串常量池:String是不可变的,创建多个相同的String对象会占用大量内存。使用`String.intern()`方法可以避免这个问题。 3. 数组与集合:...

    Java事物设计策略

    然而,这种方式容易引入编码陷阱,如事务环境问题,即在事务边界内调用其他事务边界内的方法,可能会导致事务嵌套或事务泄漏等问题。 ### 声明式事务模型 相比之下,声明式事务管理通过配置元数据(如注解或XML...

    RF-FW-ZD-01 法律事务管理制度.zip

    合同审查确保所有签订的合同都符合法律标准,避免合同陷阱;知识产权保护则关乎企业的创新成果不被侵犯;纠纷处理机制用于解决企业内部或与外界的法律纠纷;合规审计则定期检查企业各项活动的合法性。 3. **法律...

    MySQL面试题:从基础到进阶全面解析

    2. 表结构:了解如何创建表格,包括定义字段、数据类型(如INT、VARCHAR、DATE等)和主键。 3. 数据类型:熟悉每种数据类型的特点,例如数值类型、字符串类型、日期时间类型等。 4. SQL语言:掌握SQL的基本语法,如...

    律师事务所如何规划市场营销?.pdf

    高云在探讨法律市场营销策略时,指出了一系列错误做法,这些错误可能导致事务所在竞争中失去优势。 首先,过于依赖过去的经验是错误之一。法律服务不同于其他行业,它需要结合科学的数据分析来进行市场营销。随着...

    北京XX律师事务所尽职调查报告.pdf

    《北京XX律师事务所尽职调查报告》是一份深入剖析XX有限公司法律状况的专业文档,旨在为教育领域的相关决策提供详实的法律依据。报告分为多个部分,涵盖了公司的法律事实与法律评价,以确保全面理解公司的运营环境和...

    Expert One-on-One Oracle

    - **如何(不)开发数据库应用程序**:提供了一套实用指南,指导开发人员在开发过程中避免常见的陷阱,并推荐最佳实践。 - **理解Oracle架构**:深入解析Oracle数据库的基本架构,包括其组件、工作方式及相互之间的...

    开源项目-lukechampine-stm.zip

    - **重试策略(Retry Policy)**:定义了在事务冲突时如何决定是否重试以及重试的次数和方式。 - **示例和文档**:为了帮助用户理解和使用STM库,项目可能包含了丰富的示例代码和详细文档。 STM相比于传统的并发...

    Java程序员面试陷阱共48页.pdf.zip

    这份48页的PDF文档“Java程序员面试陷阱”将详细分析上述知识点,并提供实用的解题策略和避坑指南,帮助Java开发者在面试中展现出扎实的技术功底和解决问题的能力。对于准备面试或希望提升自身技能的Java程序员来说...

    某公司税务管理知识筹划.docx

    - **法规速递**:了解最新的税法变动,及时调整筹划策略,确保企业的税务活动始终与法律法规保持一致。 - **税务稽查与风险管理**:企业应建立完善的税务内控机制,定期进行自我审计,以应对税务机关的稽查,减少...

    案例分析解题策略.pdf

    - 注意细节和陷阱:文字中的细节往往隐藏着关键信息,有时也会设置误导性的信息,考生需谨慎对待,避免被误导。 - 分析问题:先理解问题要求,与材料内容进行对比,确定解题方向。 - 精准答题:回答问题时,应确保...

    AspectJ in Action: Enterprise AOP with Spring Applications

    综上所述,《AspectJ in Action: Enterprise AOP with Spring Applications》这本书不仅是一本关于面向切面编程的权威指南,还提供了大量实用案例和最佳实践建议,对于希望深入了解Spring-AspectJ集成的企业级开发者...

    MySQL最佳优化完美攻略

    - **索引策略**: 了解MySQL在哪些情况下会选择使用索引。 #### 22. MySQL何时不使用索引 - **索引限制**: 理解MySQL不使用索引的情况及其原因。 #### 23. 学会使用EXPLAIN - **查询分析**: 使用`EXPLAIN`分析...

    常遇见的大前端面试题内容,包括但不限于:react,vue,html,node,数据库等.zip

    - **State和生命周期方法**:了解如何管理和更新组件状态,以及生命周期方法如`componentDidMount`、`shouldComponentUpdate`。 - **Redux或Context API**:用于管理全局状态,解决组件间通信问题。 3. **HTML**...

    时间管理培训

    **了解个人的最佳工作时段:** - 观察并记录自己一天中的状态变化,识别高效时间段,并在此期间安排重要任务。 **PDCA循环的应用:** - **Plan (计划)**:制定清晰的目标和计划。 - **Do (执行)**:根据计划行动。 ...

    castor-1.2-doc.zip

    1. **对象关系映射(ORM)基础**:了解ORM的基本概念,它是如何通过将数据库表映射到Java类来消除SQL代码,使数据访问更直观、更易于维护。 2. **Castor配置**:学习如何配置Castor XML绑定文件,定义类与数据库表...

    招行台湾顾问的培训教材《时间管理》.ppt

    3. 跳出时间陷阱:识别并避免过多的电话、不必要的会议、不速之客、无意义的文件、无能的下属以及刁蛮的上司等浪费时间的因素。 4. 区分可控与不可控时间:明确哪些时间可以自我掌控(如起居、饮食、睡眠),哪些受...

    ssh框架项目教程-教案

    10. **最佳实践**:了解SSH框架的优化技巧,如何提高应用性能,避免常见的陷阱和误区。 总的来说,这份教程涵盖了SSH框架的全方位学习,无论是对于初次接触Java Web开发的新人,还是希望提升SSH框架应用能力的...

    《Hibernate快速入门手册》

    1. **Hibernate概述**:了解Hibernate的基本概念,包括它的起源、目标以及在软件开发中的作用。学习ORM的概念,理解为何需要Hibernate来处理数据库操作。 2. **环境配置**:设置Hibernate开发环境,包括JDK、...

Global site tag (gtag.js) - Google Analytics