应用开发者应该了解的架构知识

当前我们大量使用框架或中间件,从底层的API到软件框架、web服务器、集群软件,在享受各种便利的同时也使得我们对这些外部软件的理解更模糊,也剥夺了我们了解这些轮子设计的权利。当我们遇到新需求时也很难知道应该如何选择这些轮子,哪些特性才是应用真正要关心的,哪些特性是不需要的。(正是因为需求场景不同,才导致世界上每天都有新的轮子产生)

下面我站在应用开发者的角度介绍下我们应该了解的架构知识:

1.单台应用服务器内部介绍:

一个应用服务器(Servlet容器、也叫Web容器)本身是一个java虚拟机,其中运行这我们整个系统(或是多套系统),对外Servlet提供的功能是根据输入信息进行计算和处理,最终返回客户需要的结果,即对http请求进行处理,并返回响应。Servlet服务器天生就是多线程的,对于每个http请求都会由单独的线程处理。由于线程的创建和销毁开销很大,一般会采用线程池来处理,每个线程处理结束会重新回到线程池。java官方提供的线程池很强大,可以支持各种配置,具体可以参考java自带线程池和队列详细讲解

我们应用的代码也是由这些servlet线程调起的,由于servlet服务器天生是多线程的,我们的代码当然也是多线程的,即使你没有显式地声明线程。

[注]:不同的应用服务器的实现方式不一样,例如Ruby的Thin是基于事件驱动的,单线程的;Passenger普通版也是单线程的(可以多进程),但是Puma是多线程的。所以作为应用开发者需要了解这些基础的运行环境知识,了解这些环境下可能会遇到的并发或执行问题是什么。

2.Spring框架的作用:

现在普遍使用Spring框架进行后台应用的开发。Spring会负责整个Java应用的启动、加载和销毁的全生命周期,对于可以重用资源(Bean)会保证整个服务器只会加载一次,对于某些资源比如数据库连接等全局资源会保证其线程安全。如果一个Servlet容器只启动一个Spring容器,则Spring框架在整个虚拟机只会装载一份。

刚刚提到Spring的Bean一般是单例的(Spring允许一个类创建两个实例,每个实例有不同的名称,每个实例是独立的),这些单例在web环境下肯定会被多线程访问,也就是说它们一定要支持多线程,且线程安全。为了很简单地支持多线程,一些公司(包括我们公司)会要求在类中不要包含类变量,包括私有的和公有的,这是为了防止大家写错而导致并发问题。这些单例类如果必须有类变量,这就需要应用开发者自己来保证它的线程安全性。如何设计线程安全的类,可以参考“线程安全简介

3.我们的代码如何被调起来处理web请求:

常见的Web服务器有Apache、Nginx,这些服务器擅长处理http解析,静态资源,如html、图片、js、css等文件;在处理具体的应用逻辑时会委托给相应的应用服务器(Java中也称为Servlet容器)来处理,常见的应用服务器有Tomcat、Jetty、Weblogic、Jboss。Web服务器和应用服务器一般部署在一起,内部通信一般通过http或是其他自有协议。现在应用服务器集成的功能越来越多,很多应用服务器也具有web服务器的功能,但还是不建议直接使用应用服务器来代替web服务器。

  1. Web服务器在接收到客户的http请求后,根据特定的路由策略,如果是静态资源则直接返回,对于动态请求,会转发给应用服务器来处理。
  2. 应用服务器接收到请求后会进行消息解析,会为每个请求生成一个线程来处理此请求,此线程会调用对应的Servlet来进行实际业务处理。
  3. Servlet在处理过程中可能会访问数据库、创建自动任务,或是访问外部其他服务器(使用特定的接口规范)。
  4. Servlet处理完成后返回相应的Response对象给应用服务器。
  5. 应用服务器将此响应转化为http响应返回给web服务器。
  6. Web服务器将http响应返回给客户。(在特殊场景下web服务器可能会根据应用服务器的响应来做一些额外处理,比如返回静态文件)

对于Web服务器就不多做介绍了,因为它的功能比较简单。对于应用服务器澄清几件事:

  1. 应用服务器(Java环境)其实是一个Java程序,它运行在一个JVM虚拟机中。
  2. 应用服务器可以部署多个应用,每个应用之间彼此隔离,包括类加载路径。这是通过类加载器实现的。应用服务器会对每个应用使用单独的类加载器来加载。(热部署也与类加载器有关,有兴趣可以参考《深入理解Java虚拟机》)
  3. 应用服务器本身有一个线程池,用来处理http请求。
  4. 线程是JVM的概念,所有的线程都是归JVM统一调度的,包括应用服务器的应用自身启动的线程、Spring的线程池、JMS的线程池、Quarz的线程池等。
  5. 每个应用的每个请求是在1个线程内处理的。
  6. 同一个应用的不同线程共享一份类中的变量,不同应用之间相互隔离。
  7. ThreadLocal的全局变量比较特殊,是线程级别的,不是所有线程共享的

4.应用的生命周期知识

Web应用都会涉及到服务器的启停,这就会涉及到应用的生命周期,Spring框架也提供了生命周期的接口以及框架加载和销毁的接口。我们应该对这些生命周期有一定的了解,比如在启动时Spring框架会加载所有的Bean和其他资源。在停止时会对所有的Bean进行销毁,尝试关闭线程池。这里需要注意几点:

  1. Bean销毁本质上是删除Bean在Spring中注册的引用,如果某个类正在使用某个Bean还是可以正常使用的,但是如果你尝试通过全局方法根据Bean的Name来查找某个Bean,这时会报错。
  2. Web应用在关闭时会保证应用的联机交易能够执行完它的处理逻辑。
  3. 线程池关闭不是简单的kill,这和具体的线程池策略有关。为了防止由于某个任务执行时间过长而影响关闭问题,应该保证任务对中断异常进行处理,从而正常终止任务。(参考Java多线程发展简史了解为何应该使用interrupt、wait、notify)线程池关闭是一个复杂的问题,需要框架予以支持,后续我会专门进行介绍。
  4. 服务器可能在任意时刻停止,比如out of memory这时finally中的逻辑未必能被执行。只有数据库能够保证异常停止后状态的一致性。应用应该对服务器的异常停止有检测机制,至少能检查核心数据状态有无异常。在一致性要求高的场合最靠谱的就是使用事务保证一致性。
  5. 处理数据量较大、运行时间较长的任务在编写时应当对生命周期进行支持,当关闭时进行特殊的处理,例如实现spring的DisposableBean或是在通过注解、xml的方式进行配置;如果采用了线程池,线程池调起的任务要支持对interrupt做出响应,以便线程正常结束它自己。

上面提到的是关闭时应该注意的事,启动时也有生命周期的概念,对于启动时需要加载某些资源,或是某些Bean需要按照特定顺序启动或是销毁,这时可以借助框架提供的功能,Spring提供了对于生命周期的很好的支持,可以阅读Spring的Reference文档

5.其他一些常用组件:

1. Quartz:

它提供对于定时定频任务的支持,支持特定时间间隔调起某个任务。由于任务可能很多,为了防止过多消耗系统资源,它一般使用线程池的方式,由线程池来处理具体的任务。

2. Spring Batch:

它对于大批数据的处理提供的统一的框架,它对于失败重跑、重试以及事务控制等提供支持。另外它还可以控制多个任务的执行时序控制,并提供了并行处理功能(可以支持单机器线程池的并发、或是跨机器的并发处理)。在并发处理时,sping batch可以指定对应的线程池进行处理。

3.JMS:

JMS是java的消息处理机制,其本质是完成一个消息传输,JMS一般包括如下角色:

  1. 接收队列:用于接收具体的JMS消息,队列可以设置队列大小、存活时间等其他一些参数。
  2. 响应队列:其实本质也是一个接收队列。如果不需要响应就不需要此队列。
  3. JMS容器:负责管理这些队列并对接收的消息进行分发处理。其内部会有一个线程池,以支持多多个消息并发处理。
  4. JMS消息监听器:可以配置不同的监听规则,只监听某个队列或是某个消息。监听器会根据具体的消息来进行实际的业务处理。

JMS队列在集群中可以由单独的服务器担任,也可以和应用服务器部署在一起。从角色上看可以分为:(1)本地队列:即每个服务器都有一个对应的队列。这种队列只用于服务器各个线程之间通讯使用;(2)全局队列:整个集群只有1个队列,可以作为唯一点来做一些事项的处理。

6.全局变量、单例bean、线程:

  • 全局变量是应用级的,每个应用有一份,更严格的讲是classloader级别的。
  • 单例bean是框架级的,是由框架来保证唯一性的,它本质也是以一个普通的对象,只是整个应用只有1份。
  • 线程:线程是JVM级别的。应用的代码可以运行在不同的线程上。可以看出上面的全局变量、单例bean都需要保证是线程安全的,因为他们可能同时运行在多个线程上。
  • ThreadLocal变量:每个线程对于某个对象只有1份,对于整个应用来讲是多份。上面这些变量都是应用隔离的。

有了上面的概念再理解一些并发问题、事务处理机制、分布式事务就会轻松很多了。在进行不同软件、不同框架的选择上也可以从容很多,不会离了spring干不了活了,哈哈。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

线程安全简介

线程和进程都可以被操作系统直接调度,所不同的是多个线程之间会共享内存空间,但是多个进程之间的内存空间是相互隔离的。随着单核CPU的处理速度达到上限,多核时代正式到来,如何更好地利用多核资源成为了一个必须考虑的话题。在Java中倾向与使用多线程模型,Java针对多线程做了很多优化,包括提供了成熟的线程池1、线程安全工具类以及对锁的优化等。Java Web服务器也都是多线程的。但多线程编程一直以来都很困难且容易出错,虽然有很好的工具支撑,但编写代码时也需要格外注意,稍不小心就会掉进并发的陷阱。另一方面有些语言如Ruby则倾向于多进程的方式,由操作系统来实现并发且保证程序正确性,借助Linux的copy on write属性来节省内存。两种方式哪种更好已经超出了本文的范畴,这也是仁者见仁智者见智。值得一提的是Ruby on Rails 4开始正式支持多线程,Rails 5 更是使用puma作为默认的开发服务器,这也从另一角度预示着多线程的到来。

哪些情况是线程安全的?

下面主要讨论下多线程环境下的线程安全问题,线程安全主要是由于并发导致的,1.操作系统可能在任何时刻挂起某个线程;2.多核情况下两个线程可能处于并向执行的情况。其中第一点可以看出在单核情况下也会有线程安全问题。下面介绍哪些情况是线程安全的:

  1. 单线程环境中,包括多进程单线程、基于事件的程序(服务器)中,不存在多个线程同时修改或访问同一个变量的情况,这种情况下肯定线程安全的。为什么把这个放在第一位呢,因为如果你可以选择一个多进程单线程的Web服务器或是基于事件的服务器你就从根本上解决了线程安全的问题(当然可能有其他问题,比如内存消耗大、状态信息如session不一致等);
  2. 局部变量:即方法中声明的局部变量,这些是线程安全的,它们会随着方法调用结束而被回收,且只会对本线程可见。
  3. ThreadLocal全局变量:这些变量是线程级的,线程安全的,java会保证每个线程只有一份,每个线程只能访问属于自己的ThreadLocal变量。
  4. 线程安全的类:java中很多类会明确声明是线程安全的,例如concurrent包下类。这些类在访问是自身会进行加锁、原子化等操作来保证线程安全。
  5. 线程安全的静态类方法:这些类是单例的,其内部会保证线程安全,spring中很多类都属于此类。
  6. 无状态的bean。如没有状态的控制器bean。spring中的所有pojo类都可以是bean,且是单例的,这种类如果包括实例变量,不管共有私有都不一定是线程安全的。
  7. 不可变对象。例如java中的string类,以及scala中很多不可变的类。不可变类有很多优点,对于并发,可重复执行很有用。 上面提到的都是多个线程可以同时访问的变量,比如全局变量、类的静态变量、单例或是bean中的类实例变量(包括共有、私有)。

线程安全的优化方法

如果程序中确实需要包括这些共享信息,如何保证线程安全呢?可以采用如下措施:

  1. 将公共信息保存在数据库中,或是其他公共的位置。这样除了保证线程安全还可以保证一致性,但会牺牲效率。
  2. 对变量进行ThreadLocal包装,作为线程局部变量,做到线程间独立。
  3. 使用线程安全的类保存和处理这些共享信息(变量),如使用java的concurrent包的常用工具类
  4. 对共享信息的存取方法进行封装,并进行同步处理(加锁)。这里要注意不能只对set方法加锁,一方面是因为get方法不加锁不能保证取得的数据是最新的数据(参考java的happen before2),另一方面是除非你认真考虑了源码的各个分支情况,包括其底层调用的各个细节,否则读取方法也可能很危险,因为读取方法并没有规定不能进行其他处理,只是很多情况下它只是简单返回而已。SimpleDateFormat这个类在java中就不是线程安全的,我们有次想当然的把它作为静态变量,结果在日期格式化的时候出了问题,返回日期出现了错乱。
    [注]:Ruby中没有happen before的概念,取而代之的是GIL解释器锁3,这个锁会保证所有的单个native c方法的原子性。这样就导致在同一个进程内同时只有1个线程在使用CPU,如果是单进程单线程模式下甚至都不能充分使用1个CPU。如果要真正使用多核只能采用多进程。GIL的存在并不是必须的,只是对多线程实现的一个妥协。
  5. 如果多个状态不是独立的,这时即使每个状态是线程安全的,但整个对象也不是线程安全的,仍然需要同步处理(这样来看的话其实内部状态是不是线程安全就没什么意义了)。这时也可以使用下面要提的不变对象来处理。
  6. 对变量进行不变处理,使用不变对象保存状态。当更新状态时新建一个对象。在赋值时需要注意进行同步处理或是使用java的volatile标识。不变对象特别适合与同时保存多个关联的状态的情况,这样可以把多个状态当做1个来处理。
  7. 除非一个第三方类声明为线程安全,或是你认真读过它的代码,否则都可能有线程问题。这时需要进行同步处理或是进行充分测试。

如何保证整个程序的线程安全?4

  1. 框架自身是线程安全的
  2. 我们自己的应用代码是线程安全的
  3. 我们依赖的第三方代码是线程安全的

线程安全编程是很困难的,也是bug事故的多发区,除非性能要求否则尽量不要贸然使用多线程。现在很多web服务本身就是多线程的,进而我们的应用本身就是多线程的,在开发时也要小心,尽量不使用有状态的类。

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

Rails datetime类型在mysql数据库中的精度问题

在我的博客网站开发过程中,我在开发环境使用sqlite3,而在生产环境则使用性能更好的mysql。一直以来我也觉得没什么,因为rails会屏蔽所有的数据库差异。但这样也带来一些问题,其中一个就是schema.rb的版本控制问题。Rails建议对此文件进行版本控制,这样可以更清楚的看到数据库版本的变动。但由于开发和生产环境使用的数据库不一致,导致每次rake db:migrate后schema.rb都会变化,开发环境和生产环境的这个文件版本不一致(由于数据库的不同),将测试环境的schema.rb传到版本控制显然不好,但是不传的话总是提示有文件未提交感觉很不好。

经过权衡决定在测试环境也使用mysql,并且将测试数据也导入到新的mysql数据库。数据库不一致的另一个问题是我在迁移时发现的,不同的数据库还是有差异的,而且还影响到测试案例的成功与否,其中一个问题就是这次要重点介绍的datetime类型的精度问题。

datetime类型是rails在generate model时自动生成的,如果执行

$ bin/rails generate model Product name:string description:text 

会生成一个迁移文件如下:

#file name: db/migrate/20160501090706_create_products.rb
class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.text :description
 
      t.timestamps null: false
    end
  end
end 

其中 t.timestamps null: false会生成2个字段created_at,updated_at,其schema.rb中的

#file name : schema.rb
#...
    t.datetime "created_at"
    t.datetime "updated_at"
#...

这种datetime类型的字段是作为时间戳使用的,在sqlite3中为6位精度,如2016-05-01 11:28:38.860909,这没有问题。但是换为使用mysql就有问题了,datetime 类型在mysql中默认精度只到秒,这显然不能满足要求,秒级的时间戳是没有意义的。在网上找了很多资料,有很多比较陈旧,下面把我测试通过的解决方法记录一下,方便大家参考1 2

在Rails 4.2的Release Notes有下面一句话:

Added support for fractional seconds for MySQL 5.6 and above

可以看出Rails 4.2已经支持日期精度功能,关于mysql的fractional seconds功能可以参考 mysql reference.

我们在Rails 4.2+应用中应该如何使用此功能呢?可以通过添加migrate文件来解决,具体如下:

 bin/rails g migration ChangeDatetimeLimitForMysql 

在生成的文件中修改如下:(下面只以修改一个表为例,多个表修改方法类似)

# file name: db/migrate/20160501090706_change_datetime_limit_for_mysql.rb
# mysql 5.6.4以上的版本支持分数精度(fractional seconds),默认mysql精度只到秒
# limit 置为 6 精确到微妙;如将limit 修改为 3 则精确到毫秒

class ChangeDatetimeLimitForMysql < ActiveRecord::Migration
  def up
    change_column :comments, :created_at, :datetime, limit: 6
    change_column :comments, :updated_at, :datetime, limit: 6

     #...
  end
  
  def down
    change_column :comments, :created_at, :datetime
    change_column :comments, :updated_at, :datetime

    #...
  end
end 

然后执行rake db:migrate即可,这样之后新增修改的记录的时间戳都会精确到微秒。注意测试环境也需要执行rake db:test:prepare使其生效。

[注]:此方案适用于Rails 4.2+,mysql 5.6.4+.

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

文件字符编码概述

作为一个程序员,不可避免要经常与字符编码打交道。代码乱码、数据乱码、报文乱码也是时常遇到。近期生产上也出现了乱码、非法字符问题,趁着这个机会对字符编码进行下总结。

1.文件内容格式概述

首先大家应该理解:文件其实是一系列的二进制流,界面展示(或程序执行)相当于对这些二进制流的一种转义。理解这一点再考虑后面的问题就简单多了。

对于文件来说里面可以存放任何内容,例如文件可以存放文本、图片、音乐、视频甚至是程序二进制可执行文件。不同的文件需要不同的软件才能进行查看,例如一般的文本可以用记事本、UltraEdit、vim等来查看;图片可以用系统自带的看图工具打开,但编辑则需要Photoshop等专业工具;音乐和视频文件也类似。为了进一步标识哪些文件可以用什么软件打开或编辑,就有了文件拓展名(后缀),例如.txt、.pdf、.mp3等等,文件拓展名只是一个标识,并不影响具体的文件内容和展示。

下面再说下二进制与文本文件的区别:文本文件是指你能够直接用文本编辑器打开,能直接读得懂。二进制文件则需要特定的软件来读取。

2.文本文件编码问题

文本文件是我们最常遇到的文件格式,它们的后缀有很多种比如txt,log,rb,java,h,cpp等等,当然也不局限于这些后缀。刚也说过了后缀并不影响文件的内容和展示,一个没有后缀的文本文件也可以正常打开和查看。文本文件其实也是一系列的二进制流,但它是遵循某个编码标准的(另外还有BOM等特殊规定),常见的编码标准有ASCII、GBK、UTF-8等等。这些编码定义了二级制和字符之间的映射关系,例如汉字“中”的UTF-8编码为E4 B8 AD,它的GBK编码为D6 D0。编码可以参考这些博文1 2

假设文件是用GBK编码写的,但是当成UTF-8格式来读,得到的结果肯定是不对的,可能会有乱码或者天书文字。当然了,现在编辑器还是挺智能的,它们一般能识别出文件使用的编码,所以看上去没什么问题。遇到乱码问题需要做两件事:1.确定文件使用的正确编码格式;2.利用iconv等编码转换工具将文件转换为其他你指定的编码;或者指定让编辑器采用和文件一致的格式来读取。

大家可能还遇到过一种事情,我的编码选对了,大部分字符都能正确显示,但仍然有部分是?、[]或是其他乱码,这种一种可能是文件本身有问题,另一种是字符集的问题。比如Unicode定义了U+00000-U+10FFFF所有的字符集(其中U+D800-U+DFFF为UTF-16的代理对字段),但是一般的字体、软件仅支持U+0000-U+FFFF中的常用字符。对于这之外的即使是合法的字符,也不支持显示。

3.不同程序对于字符编码的支持

不同程序对于字符编码的支持各异,下面分别以java和ruby为例进行简单说明:

Java编码

在Java内部支持unicode编码,严格地说它采用的是UTF-16编码,对于BMP字符严格支持,但对于增补字符U+100000-U+10FFFF支持不佳,在调用函数时需小心。具体参见3 4

在java中常见的中文string,length方法和substring方法都能得到正确的结果,但是对于增补字符会有问题,可能出现半个字的问题。Java中的length对于U+0000-U+FFFF的字符会认为是长度为1,但对于增补字符会认为是2。

String str = "今天天气很好" ;
Assert. assertEquals(6, str.length());
Assert. assertEquals("天气", str.substring(2, 4));
Assert. assertEquals(2, "\uDD1E\uD834".length());//增补特殊字符
Assert. assertEquals("\uD834", "\uDD1E\uD834".substring(1, 2));

Ruby编码

在ruby中1.9+采用原始文件的编码,可以在文件、io等进行指定,字符串长度等方法对于字符集可以很好的支持,对于增补字符也能得到正确的结果。具体参见5

"今天天气很好".length   =>  6
"\u{1DD1E}".length  => 1

4.其他的编码规范

URL编码:

在URI规范中存在一些保留字符,如:/?&=@%等,这些字符如果要作为参数使用而不是作为特殊字符使用,必须在%字符后以十六进制数值表示,例如%3A表示:;另外由于URL只能使用ASCII编码,所以对于中文等需要按照网页的编码格式进行编码,例如“中”应表示为%E4%B8%AD;另外URL 不能包含空格,通常使用 + 来替换空格。

XML编码:

在xml中对于特殊字符如 &<>’" 也需要进行转义。例如使用&lt;来表示<。在报文或是文件中需要对于这些特殊字符进行处理,如转义或是加上<![CDATE[ ]]>。否则在读取或保存是会出错。

注:严格地讲,在 XML 中仅有字符 “<”和”&” 是非法的。省略号、引号和大于号是合法的,但是把它们替换为实体引用是个好的习惯。 在XML的头部可以指定文件的编码方式,包括gbk,utf-8等,这些字符并不需要特殊转义,只要和声明的编码一致即可。

HTML编码:

和xml一样,对于&<>’”也需要进行转义处理。另外html对于其他一些不易输入的特殊字符可以使用特殊编码6 7。对于文件中的字符只要和文件头中声明的字符集一致即可。

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

Oracle不同事务隔离级别分析

在具体介绍隔离级别之前,谈谈为什么要有隔离级别。数据库作为应用的核心组件,除了保证数据一致性之外,另一个很重要的指标就是并发性(也是很多数据库对外宣传的口号)。但中所周知,数据库高的一致性比如避免脏数据,保证数据可重复读,或者可串行化,都需要进行一定的数据隔离,这也就意味着加锁,加锁肯定会丧失并发性。所以为了保证并发性,提出了不同的隔离级别,或者叫做弱化的一致性。这些隔离级别在不同的数据库中的表现方式各异,对性能的影响也各有差异,远没有表面看上去那么简单。

Oracle的隔离级别分为Read Committed,Serializable,Read Only,其中前两种是ANSI/ISO SQL92标准定义的隔离级别。它不支持Read Uncommited,它在Read Committed级别支持一个很好的特性,读一致性(这个一般在IOS级别的Repeatable Read级别支持)。不过Oracle的Read Committed不能完全代替Repeatable Read,因为它不能避免“第二类丢失更新”(这种在Repeatable Read中会报错的)。我们可以在应用层通过乐观锁来避免”第二类丢失更新“,这也就是说当你选择一款数据库的时候也在一定程度上决定了程序的设计。

下面分别介绍各个隔离级别的特点:

Read Committed 读已提交:

  1. 不会出现脏读、第一类丢失更新(回滚其他事务已提交的数据);
  2. 支持语句级的读一致性;例如在统计一个表的数据时及时这些数据发生了变更也能得到正确的结果(查询发起时刻的结果)。这个是通过多版本技术实现的。
  3. 读不会阻塞写。在读数据时不会对数据加锁。
  4. 在Insert/Update时(1)如果有未提交的有冲突的项,则阻塞。(2)如果有已提交的有冲突的则报错。
  5. 可以实现语句级别的查询一致性:同一个语句得到的结果肯定是数据库中某个时刻的一致的结果。
  6. 不保证多个语句查询的一致性:例如分别查明细和汇总表,得到的结果可能是不一致的。
  7. 如何保证查询结果后续未被更新(避免第二类丢失更新):(1)采用乐观锁(vno等)当更新失败时报错;(2)采用Select for update 查询,以保证记录级的锁定。此语句的隔离效果和直接更新但未提交一样。
  8. 如何实现数据库级最大并发:更新时采用“字段调用”或其改进方法1

Serailizable 可串行化:

  1. Oracle利用多版本化技术可以做到在你问之前(执行sql之前)已经知道答案了,从而保证可串行化。而不是采用锁的技术,采用锁的技术性能会很差。
  2. 支持可重复读、不允许幻想读(是指多次统计或范围读取仍能得到一致的结果)、第二类丢失更新(提交时覆盖别人已提交的数据,天然支持,无需使用乐观锁等技术)。
  3. 读不会阻塞写。
  4. 如果读取时某个数据已经被更新了,数据库会利用UNDO日志恢复到事务开始时刻,并返回那时的结果。如果根据UNDO日志无法恢复,则报错ORA-0155:snapshot too old。
  5. 如果更新时发现某个数据已经被更新,则报错ORA-08177:can’t Serailize access for this transaction。可以看出如果要使用此级别,(1) 一般要保证没有其他人修改相同的数据; (2) 需要事务级的读一致性;(3) 事务都很短(有利于保证第1点)。
  6. 此隔离级别下无法看到其他事务提交的所有更新,包括已经提交的。如果应用需要保证数据完整性可能会有问题,因为其他事务可能已经修改了事务,但你却看不到。
  7. 此隔离级别不意味着所有事务就是像一个接一个串行执行一样。例子参见2

Read Only 只读级别:

和Serailizable级别一致,只是不允许进行数据库更新操作。

其他一些注意事项:

1.热表的超出预期的IO:

由于Oracle利用多版本技术来保证读取数据的一致性,在对热表进行读操作时,在Serailizable隔离级别(或是需要执行很长时间的sql时)下可能会导致读取大量的UNDO块来恢复数据,从而导致大量的IO。

2.update重启动:

如果在更新时部分数据已经被其他事务更新(Read Committed级别),此时Oracle会回退update,然后尝试使用Select for update模式进行重启动更新。一般情况下这对应用是透明的,但是有些数据如触发器等是不能回滚的,另外如果我们的更新记录数过大重启动更新还是很影响效率的。

参考资料:

  1. 事务处理:概念与技术,7.12节

  2. Oracle Database 9i/10g/11g编程艺术深入数据库体系结构, 7.2.4节

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》