2011年9月25日星期日

企业应用程序中的并发控制


  在企业应用程序中,如何考虑并发是一件极为重要的事情:企业应用一般都要求严格的数据完整性和一致性。然而,在实际的开发过程中,很多企业应用程序都不考虑并发,造成了脏数据随处可见,需要浪费大量人力物力来维护的境地。
  一般而言,企业应用中一共有两类场景需要考虑并发加锁:
    1. 多个事务并发时,比如同时有很多个事务修改同一个对象(数据库表),典型的表现为多个线程对应的多个事务同时修改一个对象。
    2. 一个用例要经过一系列的事务操作才能完成的,比如典型的WEB应用中修改订单的行为:用户发起一个请求,然后服务端程序响应,读取订单,返回给前端表现层,事务完成;用户修改订单后提交,此时系统会再启动一个事务,修改订单后返回。我们把这种场景成为离线模式。
  对应“在线”模式场景和离线模式场景,都有两个典型的解决方式:乐观锁和悲观锁。乐观锁是先尝试,失败了再说的处理方式,而悲观锁则是估计会失败,所以我先把对象都锁住。两种锁模式要视实际业务需求来选择使用。
一、在线乐观锁实现原理:
  在线乐观锁一般会有三种实现方式:基于版本号、基于时间戳、基于所有字段。基于版本号和基于时间戳类似,都是在更新数据的时候比较该值和数据库中是否相同,如果相同则表示该对象从读取到写回的这段时间内并没有别的线程(事务)修改过,此时可以认为修改是有效的。过程一般如下:开始-->读取数据-->执行业务逻辑-->保存数据-->结束。对应的SQL语句如下:
  update my_table set version=version+1,business='new_value' where version=V1 and other_condition
此时可以根据更新的数据行数来确认是否执行成功,如果更新的数据行数为0,则可以根据实际业务的需要选择抛出乐观锁失效或者简单的选择忽略。
  基于所有字段的乐观锁和基于版本号的基本类似,只是将条件改成了所有字段的旧值。语句如下:
  update my_table set name='new_name',address='new_address' where name='old_name' and address = 'old_address' and email='old_email' and phone='old_phone'
  一般基于所有字段的在线乐观锁只在不能修改现有表结构的情况下使用,否则不推荐使用。因为该方法存在一些缺陷,比如比较浮点数的时候不能正确比较等。好处是不需要修改scheme。
  Hibernate内置了对于在线乐观锁的支持,使用起来很简单,只需要在其映射文件(*.hbm.xml)中配置声明version字段即可。这样Hibernate在保存数据的时候会校验version和读取时一致,不一致则抛出StaleObjectStateException的异常。配置片段如下所示:

  "version" column="version" />

二、在线悲观锁实现原理:
  在线悲观锁一般都使用数据库的机制来实现,比如Oracle的 for update语句。使用for update语句时,如果没有其他事务锁定执行的记录行,则可以直接获取锁并返回;如果锁已经被其他事务所获取了,则当前事务就会等待,
直到获取到锁的事务释放该锁以后才能继续运行。
  Hibernate使用锁的方式也较为简单,比如执行load方法的时候指定锁模式为LockMode.UPGRADE即为悲观锁,此外还能在session和Query中指定悲观锁。
  示例代码如下:
    调用 Session.load() 的时候指定锁定模式(LockMode)
    调用 Session.lock()
    调用 Query.setLockMode()。

三、离线乐观锁实现原理:
  离线锁都跨越了多个事务,所以在线锁的场景并不适用离线锁的场景。设想如下修改订单的场景:
  1. A用户读取订单Order-->编辑订单-->提交更改-->结束
  2. B用户读取订单Order-->编辑订单-->提交更改-->结束
  其中的读取订单是一次HTTP请求,也对应一个事务;编辑订单是在浏览器中完成;提交更改是另外一次HTTP请求,对应另外一个事务。即修改订单这个用例横跨了两个事务。而两个不用的用户同时修改了一个订单,这会导致后提交
更改的用户把前面用户的修改结果覆盖,造成“写丢失”的情况,这在一些场景中是不允许发生的。
  类似前面的在线乐观锁模式,系统也使用类似的控制策略。所不同的是,我们把verion保存在session中(对于没有session的集群应用来说,可以采用另外的方法)。在每个用户读取到订单的时候,都将version的值保存在session中,在第二次提交更改的时候比较该version的值,如果发现不同,则抛出“乐观锁失效”的异常给用户提示。其实现的基本原理就是在一个能够横跨多次事务的地方保存最初的版本号,然后进行比较判断。
  还可以使用脱管对象来进行离线乐观锁。具体的做法是,把业务层返回的持久对象脱钩后放入session,然后等第二次事务(这里是提交更改)开始的时候,先进行持久对象的挂钩操作,此时如果持久对象的version已经被修改过了,则持久层框架会抛出“StaleObjectStateException”异常。这种做法的好处是简单易行,坏处就是把脱管对象放入HTTP Session将会占用大量的内存。
  对于没有session的应用程序(很多企业级应用程序为了简化集群而不使用http session),可以采取如下措施:在一个用例的多次事务中传递version。比如上述场景在第一次读取订单的时候就返回持久对象的version到客户端保存。在用户编辑完订单后,提交更改的时候,也同时提交version信息到服务端。服务端使用该version与从数据库中load的持久化对象的version进行比较。该措施的缺点是增加了系统的复杂性,也增加了网络的开销(尽管网络开销很小)。

四、离线悲观锁实现原理:
  离线悲观锁一般都采取应用程序控制的方式。典型的做法是在数据库中维持一张系统锁表,可以如下设计表结构:
  sys_lock(object_code,object_id,locker,create_date)
  其中object_code为对象编码,可以是表名或者持久化对象类名;object_id是锁定的记录主键ID;locker是悲观锁的持有人;创建时间可以用在自动释放锁上面(比如设定一个机制把锁定时间超过24小时的锁释放掉)。
  在要对对象进行更新以前,先获取悲观锁。获取到锁以后再更新数据,然后释放锁。
  对应上面的场景,这里变为:
      用户尝试获取对象锁,成功后-->编辑订单-->提交更改-->释放锁-->结束
  这里的获取对象锁和返回对象在同一个事务中,提交更改和释放锁在另外一个事务中。
  使用悲观离线锁时需要注意两点:
  1. 由于悲观离线锁是应用程序级别的锁,所有需要控制的用例都必须遵循 获取锁-->更改-->释放锁 的模式,如果有人不遵守,则会不受悲观锁的控制。
  2. 注意锁的释放。可以采取定时释放和后来者抢锁的策略。具体视需求而定。

2011年2月20日星期日

关于对AOP的理解

  在AOP(面向切面的编程)里面,其核心在于对某一类特定的关注点附加特定的处理。从这句话我们可以分析出来,AOP应该有下面的东西:
1. Advice (通知):处理部分。定义处理的具体手段,比如在调用方法前统计方法调用次数,比如在调用方法后记录调用日志等,都是具体是增强手段。很常见的Advice就是事务控制,调用方法后,视情况决定是提交事务还是回滚事务。
2. Pointcut(关注点):定义哪一些方法符合我们要处理的条件,其实就是一个条件定义,符合这个条件定义的,就是我们要关注的对象。比如要为所有以save开头的方法附加事务。那么这个关注点的定义就是 “save*”
3. 对二者的组合。 定义哪些被关注的对象应该如何处理。
设想一下情况:
Advice:事务控制、调用次数记录、异常次数记录
Pointcut: 以save开头的方法、以query开头的方法
我们可以组合出6中AOP的使用方法:
1. 给save开头的方法增加事务控制
2. 给save开头的方法记录调用次数
3. 给save开头的方法记录异常次数
4. 给query开头的方法增加事务控制
5. 给query开头的方法记录调用次数
6. 给query开头的方法记录异常次数
具体到SpringAOP中,我们看看是如何进行配置的。

2011年1月25日星期二

设计模式学习笔记之三工厂方法模式

  工厂方法模式的含义是,为对象的创建定义一个接口,并将由子类来决定如何创建该对象。工厂方法在创建具有平行类结构层次的对象时比较合适。比如工厂有很多种,产品也有很多种,彼此平行。A工厂创建A对象,B工厂创建B对象。其中,A、B工厂的创建方式是一致的,所以AB工厂类会继承自同一个抽象工厂类。A、B产品也是类似的,但是又具体不同,所以A、B产品也实现相同的接口。这种A-B工厂、A-B产品的类层次就称之为平行类结构层次。

2011年1月23日星期日

设计模式学习笔记之二修饰器模式

  修饰器模式:能够动态透明的给一个类增加行为。修饰器模式中存在两大阵营,一边是被修饰的对象,一边是修饰器本身,正如房子和粉刷匠的关系。粉刷匠(修饰器)给房子增加了不同外观,看上去像是变了一个样一样。在修饰器模式中,修饰器和修饰对象都继承或实现同一个接口,但区别在于,修饰器持有一个对修饰对象的引用。
  java中修饰器最典型的例子莫过于IO流。以输入流InputStream为例,修饰对象如FileInputStream、ByteArrayInputStream,StringBufferInputStream,修饰器为FilterInputStream的子类。FilterInputStream持有一个InputStream,以便能够在其基础上改变其行为。

2011年1月22日星期六

设计模式学习笔记之一观察者模式

观察者模式的定义:当被观察的对象状态发生改变时,被观察者将逐一通知观察者,告知观察者其状态发生了改变。生活中常见的例子诸如订阅报纸杂志、在Twitter上关注一个人等。以在Twitter上关注某人为例,点击Twitter的Follow按钮时,表示将自己注册为某人的一个观察者,此时,被观察的就是被关注的那个人,相关类图如下:
观察者接口                                            主题接口
          |                                                             |
          |                                                             |
          |                                                             |
          |                                                             |
用户自身-------------------关注---------->被Follow的用户
java内置的API中已经有对观察者模式的实现,即Observable对象和Observer接口。要使用Java API实现观察者模式只需:
1. 被观察的对象继承Obervable对象,并实现自己的业务方法。
2. 观察者实现Observer接口即可。 观察者通过调用被观察者的方来来注册自己到观察者列表中去,从而让被观察者持有观察者的引用,当被观察者发生改变时,能够通过该引用通知观察者。
Java里的观察者模式随处可见,典型的例子比如swing里面的事件。其中,事件源比如按钮、窗体等就是被观察者,而实现监听接口的类即为观察者。调用按钮或者窗体的addListener方法即为注册观察者。

2011年1月7日星期五

gVim使用技巧:有用操作

一些有用的gVim使用小技巧:
1. 分割窗口 Ctrl+w, 然后再按s
2. 退出窗口分割: Ctrl+w,然后按q
3. 自动补全: Ctrl+n
4. 文件比较:打开一个文件后,输入命令: vertical diffsplit 要比较的文件名 回车。此时,vim会打开两个垂直分割的窗口,然后就可以进行文件的比较与合并了。
5. 浏览源码文件(java,c,python等),安装taglist插件,然后输入命令:Tlist 即可
6. 对于常写技术博客的人一个非常有用的插件tohtml,可以把任何的文本格式转为html,便于在博客发布,一般gVim默认自带了该插件,使用方式如下:
TOhtml 即可,命名执行完后会打开一个垂直分割的窗口,里面是转好的内容。
7. 统计符合条件的字符串出现的次数,采用替换命令实现:%s/搜索内容/&/gn 即可。解释如下:% 为全文搜索、s为替换命令、&表示替换内容本身,即统计后内容不变、gn表示全行搜索

2010年12月29日星期三

gVim使用技巧:排版命令

1. 在Normal模式下,输入“>>”表示右移一个tab位,相似的,按“<<”左移。
2. 命令模式下,输入 le 表示该行左对齐,相似的,输入ri表示右对齐。居中对齐就是ce。这三个命令分别是left和right,center的头两个字母,比较利于记忆
3. gqq 命令,在本行进行段落重排。
4. gqQ命令,在全文段落重拍