- 浏览: 80207 次
文章分类
Java编程中“为了性能”尽量要做到的一些地方
最近的机器内存又爆满了,除了新增机器内存外,还应该好好review一下我们的代码,有很多代码编写过于随意化,这些不好的习惯或对程序语言的不了解是应该好好打压打压了。
2. 尽量避免随意使用静态变量
1 public class A{ 2 static B b = new B(); 3 }
1 Public void test(){ 2 Object obj = new Object(); 3 …… 4 Obj=null; 5 }
1 Public void test(){ 2 Object obj = new Object(); 3 …… 4 Obj=null; 5 //执行耗时,耗内存操作;或调用耗时,耗内存的方法 6 …… 7 }
Student.java
1 package cn.itcast.bean; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.CascadeType; 7 import javax.persistence.Column; 8 import javax.persistence.Entity; 9 import javax.persistence.GeneratedValue; 10 import javax.persistence.Id; 11 import javax.persistence.JoinColumn; 12 import javax.persistence.JoinTable; 13 import javax.persistence.ManyToMany; 14 15 @Entity 16 public class Student { 17 private Integer id; 18 private String name; 19 private Set<Teacher> teachers = new HashSet<Teacher>(); 20 21 @Id @GeneratedValue //id作为实体标识符 并且采用数据库的id自增长方式生成主键值 22 public Integer getId() { 23 return id; 24 } 25 26 public void setId(Integer id) { 27 this.id = id; 28 } 29 30 @Column(length = 10, nullable = false) 31 public String getName() { 32 return name; 33 } 34 35 public void setName(String name) { 36 this.name = name; 37 } 38 39 @ManyToMany(cascade = CascadeType.REFRESH) 40 @JoinTable(name = "student_teacher", inverseJoinColumns = @JoinColumn(name = "teacher_id"), 41 joinColumns = @JoinColumn(name = "student_id")) 42 public Set<Teacher> getTeachers() { 43 return teachers; 44 } 45 /* 46 假如不对关联表里的字段做任何设定,那么表里面的字段默认由JPA的实现产品来帮我们自动生成。 47 inverseJoinColumns:inverse中文是反转的意思,但是觉得太恶心了,在JPA里,可以理解为被维护端 48 inverseJoinColumns被维护端外键的定义 49 @JoinColumn:外键,中间表哪个外键名称跟teacher表的主键关联 50 joinColumns:关系维护端的定义 51 */ 52 53 public void setTeachers(Set<Teacher> teachers) { 54 this.teachers = teachers; 55 } 56 }
Teacher.java
1 package cn.itcast.bean; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.CascadeType; 7 import javax.persistence.Column; 8 import javax.persistence.Entity; 9 import javax.persistence.GeneratedValue; 10 import javax.persistence.Id; 11 import javax.persistence.ManyToMany; 12 13 @Entity 14 public class Teacher { 15 private Integer id; 16 private String name; 17 private Set<Student> students=new HashSet<Student>(); 18 19 @Id @GeneratedValue //id作为实体标识符 并且采用数据库的id自增长方式生成主键值 20 public Integer getId() { 21 return id; 22 } 23 public void setId(Integer id) { 24 this.id = id; 25 } 26 27 @Column(length = 10, nullable = false) 28 public String getName() { 29 return name; 30 } 31 public void setName(String name) { 32 this.name = name; 33 } 34 35 @ManyToMany(cascade=CascadeType.REFRESH,mappedBy="teachers") 36 public Set<Student> getStudents() { 37 return students; 38 } 39 /* 40 cascade: 41 CascadeType.PERSIST:级联保存不要,学生没来之前,老师就已经在了 42 CascadeType.MERGE:级联更新不要,把学生的信息改了,没必要修改相应的老师的信息,压根就没这业务需求 43 CascadeType.REMOVE:级联删除更不要,如果双方都设了级联删除,加入删除学生,会删除相应的老师,被删除的老师又 44 跟学生发生千丝万缕的关系,又把一批学生删掉.....没完没了...最终的结果可能是数据里面所有的记录都被清掉 45 所以在多对多关系中,级联删除通常是用不上的 46 这里只需设置级联涮新CascadeType.PERSIST就可以了,事实上refresh方法也很少使用 47 mappedBy: 通过这个属性来说明老师是关系被维护端 48 fetch: 加载行为默认是延迟加载(懒加载),凭Many。 这里不需要设置 49 */ 50 51 public void setStudents(Set<Student> students) { 52 this.students = students; 53 } 54 }
ManyToManyTest.java
1 package junit.test; 2 3 import javax.persistence.EntityManager; 4 import javax.persistence.EntityManagerFactory; 5 import javax.persistence.Persistence; 6 7 import org.junit.BeforeClass; 8 import org.junit.Test; 9 10 public class ManyToManyTest { 11 12 @BeforeClass 13 public static void setUpBeforeClass() throws Exception { 14 } 15 @Test 16 public void save() { 17 EntityManagerFactory factory = Persistence.createEntityManagerFactory("itcast"); 18 factory.close(); 19 } 20 21 }
双向多对多关系是一种对等关系,既然是对等关系,也就是说我们可以人为决定谁是关系维护端,谁是关系被维护端,这里选学生做关系维护端。那么以后就只能通过学生来维护老师跟学生的关系。
假设:
老师A id是1
学生B id是1
那通过什么东西把他们的关系确立起来呢?采用什么来存放他们的关联关系呢?是中间表(关联表)。
学生A和老师B建立起关系,首先要找到关系维护端,是学生,就要通过学生这个关系维护端,学生A.getTeachers().add(Teacher);这样就能把老师跟学生的关系确立起来了。确立起来后,反应在中间表里面就是insert into...一条语句
如果学生A要把老师B开掉,那就要解除关系,也是通过关系维护端学生A,反映在面向对象的操作就是 学生A.getTeachers().remove(Teacher);执行这句代码的时候,在底层JDBC它会对中间表做delete from...语句的操作。
我们都是通过关系维护端来进行操作的,以后在双向关系中一定要找准谁是关系维护端,谁是关系被维护端
@JoinTable的注解有:看图,
目录结构,看图:
Student.java
1 package cn.itcast.bean; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.CascadeType; 7 import javax.persistence.Column; 8 import javax.persistence.Entity; 9 import javax.persistence.GeneratedValue; 10 import javax.persistence.Id; 11 import javax.persistence.JoinColumn; 12 import javax.persistence.JoinTable; 13 import javax.persistence.ManyToMany; 14 15 @Entity 16 public class Student { 17 private Integer id; 18 private String name; 19 private Set<Teacher> teachers = new HashSet<Teacher>(); 20 21 public Student() { 22 } 23 24 public Student(String name) { 25 this.name = name; 26 } 27 28 @Id 29 @GeneratedValue 30 // id作为实体标识符 并且采用数据库的id自增长方式生成主键值 31 public Integer getId() { 32 return id; 33 } 34 35 public void setId(Integer id) { 36 this.id = id; 37 } 38 39 @Column(length = 10, nullable = false) 40 public String getName() { 41 return name; 42 } 43 44 public void setName(String name) { 45 this.name = name; 46 } 47 48 @ManyToMany(cascade = CascadeType.REFRESH) 49 @JoinTable(name = "student_teacher", inverseJoinColumns = @JoinColumn(name = "teacher_id"), joinColumns = @JoinColumn(name = "student_id")) 50 public Set<Teacher> getTeachers() { 51 return teachers; 52 } 53 54 /* 55 假如不对关联表里的字段做任何设定,那么表里面的字段默认由JPA的实现产品来帮我们自动生成。 56 inverseJoinColumns:inverse中文是反转的意思,但是觉得太恶心了,在JPA里,可以理解为被维护端 57 inverseJoinColumns被维护端外键的定义 @JoinColumn:外键,中间表哪个外键名称跟teacher表的主键关联 58 joinColumns:关系维护端的定义 59 */ 60 61 public void setTeachers(Set<Teacher> teachers) { 62 this.teachers = teachers; 63 } 64 65 public void addTeacher(Teacher teacher) { 66 this.teachers.add(teacher); 67 } 68 69 public void removeTeacher(Teacher teacher) { 70 if(this.teachers.contains(teacher)){ //凭什么判断teacher在集合teachers中呢?是根据teacher的id 71 //这就有必要重写Teacher.java的hasCode和equals方法,通过这两个方法来判断对象是否相等 72 this.teachers.remove(teacher); 73 } 74 } 75 }
Teacher.java
1 package cn.itcast.bean; 2 3 import java.util.HashSet; 4 import java.util.Set; 5 6 import javax.persistence.CascadeType; 7 import javax.persistence.Column; 8 import javax.persistence.Entity; 9 import javax.persistence.GeneratedValue; 10 import javax.persistence.Id; 11 import javax.persistence.ManyToMany; 12 13 @Entity 14 public class Teacher { 15 private Integer id; 16 private String name; 17 private Set<Student> students = new HashSet<Student>(); 18 19 public Teacher() { 20 } 21 22 public Teacher(String name) { 23 this.name = name; 24 } 25 26 @Id 27 @GeneratedValue 28 // id作为实体标识符 并且采用数据库的id自增长方式生成主键值 29 public Integer getId() { 30 return id; 31 } 32 33 public void setId(Integer id) { 34 this.id = id; 35 } 36 37 @Column(length = 10, nullable = false) 38 public String getName() { 39 return name; 40 } 41 42 public void setName(String name) { 43 this.name = name; 44 } 45 46 @ManyToMany(cascade = CascadeType.REFRESH, mappedBy = "teachers") 47 public Set<Student> getStudents() { 48 return students; 49 } 50 51 /* 52 cascade: CascadeType.PERSIST:级联保存不要,学生没来之前,老师就已经在了 53 CascadeType.MERGE:级联更新不要,把学生的信息改了,没必要修改相应的老师的信息,压根就没这业务需求 54 CascadeType.REMOVE:级联删除更不要,如果双方都设了级联删除,加入删除学生,会删除相应的老师,被删除的老师又 55 跟学生发生千丝万缕的关系,又把一批学生删掉.....没完没了...最终的结果可能是数据里面所有的记录都被清掉 56 所以在多对多关系中,级联删除通常是用不上的 这里只需设置级联涮新CascadeType.PERSIST就可以了,事实上refresh方法也很少使用 57 mappedBy: 通过这个属性来说明老师是关系被维护端 fetch: 加载行为默认是延迟加载(懒加载),凭Many。 这里不需要设置 58 */ 59 60 public void setStudents(Set<Student> students) { 61 this.students = students; 62 } 63 64 @Override 65 public int hashCode() { 66 final int prime = 31; 67 int result = 1; 68 result = prime * result + ((id == null) ? 0 : id.hashCode()); 69 //判断的依据是,如果id不为null的话,就返回id的哈希码 70 return result; 71 } 72 73 @Override 74 public boolean equals(Object obj) { 75 if (this == obj) 76 return true; 77 if (obj == null) 78 return false; 79 if (getClass() != obj.getClass()) 80 return false; 81 final Teacher other = (Teacher) obj; 82 if (id == null) { 83 if (other.id != null) 84 return false; 85 } else if (!id.equals(other.id)) 86 return false; 87 return true; 88 } 89 }
ManyToManyTest.java
1 package junit.test; 2 3 import javax.persistence.EntityManager; 4 import javax.persistence.EntityManagerFactory; 5 import javax.persistence.Persistence; 6 7 import org.junit.BeforeClass; 8 import org.junit.Test; 9 10 import cn.itcast.bean.Student; 11 import cn.itcast.bean.Teacher; 12 13 public class ManyToManyTest { 14 15 @BeforeClass 16 public static void setUpBeforeClass() throws Exception { 17 } 18 19 @Test 20 public void save() { 21 EntityManagerFactory factory = Persistence.createEntityManagerFactory("itcast"); 22 EntityManager em = factory.createEntityManager(); 23 em.getTransaction().begin(); 24 25 em.persist(new Student("小张同学")); 26 em.persist(new Teacher("李勇老师")); 27 28 em.getTransaction().commit(); 29 em.close(); 30 factory.close(); 31 } 32 33 /* 34 * 建立学生跟老师的关系 35 */ 36 @Test 37 public void buildTS() { 38 EntityManagerFactory factory = Persistence.createEntityManagerFactory("itcast"); 39 EntityManager em = factory.createEntityManager(); 40 em.getTransaction().begin(); 41 42 Student student = em.find(Student.class, 1);// 首先要得到学生,因为学生是关系维护端,通过关系维护端来建立关系 43 student.addTeacher(em.getReference(Teacher.class, 1));//这方法在业务意义上,就代表建立跟老师的关系 44 //所谓建立跟老师的关系,无非就是把老师加进集合里面去。 45 //建立关系,体现在JDBC上面,就是添加一条记录进中间表 46 47 em.getTransaction().commit(); 48 em.close(); 49 factory.close(); 50 } 51 52 /* 53 * 解除学生跟老师的关系 54 */ 55 @Test 56 public void deleteTS() { 57 EntityManagerFactory factory = Persistence.createEntityManagerFactory("itcast"); 58 EntityManager em = factory.createEntityManager(); 59 em.getTransaction().begin(); 60 61 Student student = em.find(Student.class, 1);// 首先要得到学生,因为学生是关系维护端,通过关系维护端来建立关系 62 student.removeTeacher(em.getReference(Teacher.class, 1));//这方法在业务意义上,就代表解除跟老师的关系 63 //所谓解除跟老师的关系,无非就是把老师从集合里面删去 64 //解除关系,体现在JDBC上面,就是在中间表删除一条记录 65 66 em.getTransaction().commit(); 67 em.close(); 68 factory.close(); 69 } 70 71 /* 72 * 删除老师,老师已经跟学生建立起了关系(错误写法) 73 */ 74 @Test 75 public void deleteTeacher1() { 76 EntityManagerFactory factory = Persistence.createEntityManagerFactory("itcast"); 77 EntityManager em = factory.createEntityManager(); 78 em.getTransaction().begin(); 79 80 81 em.remove(em.getReference(Teacher.class, 1)); 82 //并不需要发生数据装载行为,只需要一个托管状态的实体,所以用getReference可以提供性能 83 84 em.getTransaction().commit(); 85 em.close(); 86 factory.close(); 87 } 88 /* 89 该方法会出错,因为中间表中已经有记录了,会抛出以下错误 90 Caused by: java.sql.BatchUpdateException: 91 Cannot delete or update a parent row: a foreign key constraint fails 92 (`itcast/student_teacher`, CONSTRAINT `FKD4E389DE1D49449D` FOREIGN KEY (`teacher_id`) 93 REFERENCES `teacher` (`id`)) 94 关系被维护端没有权力更新外键,所以不会删除中间表的记录 95 */ 96 97 /* 98 * 删除老师,老师已经跟学生建立起了关系(正确写法) 99 */ 100 @Test 101 public void deleteTeacher2() { 102 EntityManagerFactory factory = Persistence.createEntityManagerFactory("itcast"); 103 EntityManager em = factory.createEntityManager(); 104 em.getTransaction().begin(); 105 106 Student student = em.find(Student.class, 1); 107 Teacher teacher = em.getReference(Teacher.class, 1); 108 //并不需要发生数据装载行为,只需要一个托管状态的实体,所以用getReference可以提供性能 109 student.removeTeacher(teacher); 110 em.remove(teacher); 111 112 em.getTransaction().commit(); 113 em.close(); 114 factory.close(); 115 } 116 117 /* 118 * 删除学生,老师已经跟学生建立起了关系 119 */ 120 @Test 121 public void deleteStudent() { 122 EntityManagerFactory factory = Persistence.createEntityManagerFactory("itcast"); 123 EntityManager em = factory.createEntityManager(); 124 em.getTransaction().begin(); 125 126 Student student = em.getReference(Student.class, 1); 127 em.remove(student);//这样是可以删除学生的,尽管目前是有关系,中间表有关联记录,有外键约束 128 //但是我们现在要删除的是关系维护端,关系维护端是有权力对外键进行增删改查操作的 129 //删除的时候,Hibernate会判断student是关系维护端,然后去中间表把关联记录先删除掉 130 //再删掉student对象 131 132 em.getTransaction().commit(); 133 em.close(); 134 factory.close(); 135 } 136 }
1. Hibernate优化-引言
Hibernate是最流行的对象关系映射(ORM)引擎之一,它提供了数据持久化和查询服务。
在你的项目中引入Hibernate并让它跑起来是很容易的。但是,要让它跑得好却是需要很多时间和经验的。
通过我们的使用Hibernate 3.3.1和Oracle 9i的能源项目中的一些例子,本文涵盖了很多Hibernate调优技术。其中还提供了一些掌握Hibernate调优技术所必需的数据库知识。
我们假设读者对Hibernate有一个基本的了解。如果一个调优方法在Hibernate 参考文档(下文简称HRD)或其他调优文章中有详细描述,我们仅提供一个对该文档的引用并从不同角度对其做简单说明。我们关注于那些行之有效,但又缺乏文档的调优方法。
2. Hibernate优化-Hibernate性能调优
调优是一个迭代的、持续进行的过程,涉及软件开发生命周期(SDLC)的所有阶段。在一个典型的使用Hibernate进行持久化的Java EE应用程序中,调优会涉及以下几个方面:
业务规则调优
设计调优
Hibernate调优
Java GC调优
应用程序容器调优
底层系统调优,包括数据库和OS。
没有一套精心设计的方案就去进行以上调优是非常耗时的,而且很可能收效甚微。好的调优方法的重要部分是为调优内容划分优先级。可以用Pareto定律(又称“80/20法则”)来解释这一点,即通常80%的应用程序性能改善源自头20%的性能问题[5]。
相比基于磁盘和网络的访问,基于内存和CPU的访问能提供更低的延迟和更高的吞吐量。这种基于IO的Hibernate调优与底层系统IO部分的调优应该优先于基于CPU和内存的底层系统GC、CPU和内存部分的调优。
范例1
我们调优了一个选择电流的HQL查询,把它从30秒降到了1秒以内。如果我们在垃圾回收方面下功夫,可能收效甚微——也许只有几毫秒或者最多几秒,相比HQL的改进,GC方面的改善可以忽略不计。
好的调优方法的另一个重要部分是决定何时优化[4]。
积极优化的提倡者主张开始时就进行调优,例如在业务规则和设计阶段,在整个SDLC都持续进行优化,因为他们认为后期改变业务规则和重新设计代价太大。
另一派人提倡在SDLC末期进行调优,因为他们抱怨前期调优经常会让设计和编码变得复杂。他们经常引用Donald Knuth的名言“过早优化是万恶之源” [6]。
为了平衡调优和编码需要一些权衡。根据笔者的经验,适当的前期调优能带来更明智的设计和细致的编码。很多项目就失败在应用程序调优上,因为上面提到的“过早优化”阶段在被引用时脱离了上下文,而且相应的调优不是被推迟得太晚就是投入资源过少。
但是,要做很多前期调优也不太可能,因为没有经过剖析,你并不能确定应用程序的瓶颈究竟在何处,应用程序一般都是这样演化的。
对我们的多线程企业级应用程序的剖析也表现出大多数应用程序平均只有20-50%的CPU使用率。剩余的CPU开销只是在等待数据库和网络相关的IO。
基于上述分析,我们得出这样一个结论,结合业务规则和设计的Hibernate调优在Pareto定律中20%的那个部分,相应的它们的优先级更高。
一种比较实际的做法是:
识别出主要瓶颈,可以预见其中多数是Hibernate、业务规则和设计方面的(其数量视你的调优目标而定;但三到五个是不错的开端)。
修改应用程序以便消除这些瓶颈。
测试应用程序,然后重复步骤1,直到达到你的调优目标为止。
你能在Jack Shirazi的《Java Performance Tuning》 [7]一书中找到更多关于性能调优阶段的常见建议。
下面的章节中,我们会按照调优的大致顺序(列在前面的通常影响最大)去解释一些特定的调优技术。
3. Hibernate优化-监控和剖析
没有对Hibernate应用程序的有效监控和剖析,你无法得知性能瓶颈以及何处需要调优。
3.1.1 监控SQL生成
尽管使用Hibernate的主要目的是将你从直接使用SQL的痛苦中解救出来,为了对应用程序进行调优,你必须知道Hibernate生成了哪些 SQL。JoeSplosky在他的《The Law of Leaky Abstractions》一文中详细描述了这个问题。
你可以在log4j中将org.hibernate.SQL包的日志级别设为DEBUG,这样便能看到生成的所有SQL。你还可以将其他包的日志级别设为DEBUG,甚至TRACE来定位一些性能问题。
3.1.2 查看Hibernate统计
如果开启hibernate.generate.statistics,Hibernate会导出实体、集合、会话、二级缓存、查询和会话工厂的统计信息,这对通过SessionFactory.getStatistics()进行的调优很有帮助。为了简单起见,Hibernate还可以使用MBean“org.hibernate.jmx.StatisticsService”通过JMX来导出统计信息。你可以在这个网站找到配置范例 。
3.1.3 剖析
一个好的剖析工具不仅有利于Hibernate调优,还能为应用程序的其他部分带来好处。然而,大多数商业工具(例如JProbe [10])都很昂贵。幸运的是Sun/Oracle的JDK1.6自带了一个名为“Java VisualVM” [11]的调试接口。虽然比起那些商业竞争对手,它还相当基础,但它提供了很多调试和调优信息。
4. Hibernate优化-调优技术
4.1 业务规则与设计调优
尽管业务规则和设计调优并不属于Hibernate调优的范畴,但此处的决定对后面Hibernate的调优有很大影响。因此我们特意指出一些与Hibernate调优有关的点。
在业务需求收集与调优过程中,你需要知道:
数据获取特性包括引用数据(reference data)、只读数据、读分组(read group)、读取大小、搜索条件以及数据分组和聚合。
数据修改特性包括数据变更、变更组、变更大小、无效修改补偿、数据库(所有变更都在一个数据库中或在多个数据库中)、变更频率和并发性,以及变更响应和吞吐量要求。
数据关系,例如关联(association)、泛化(generalization)、实现(realization)和依赖(dependency)。
基于业务需求,你会得到一个最优设计,其中决定了应用程序类型(是OLTP还是数据仓库,亦或者与其中某一种比较接近)和分层结构(将持久层和服务 层分离还是合并),创建领域对象(通常是POJO),决定数据聚合的地方(在数据库中进行聚合能利用强大的数据库功能,节省网络带宽;但是除了像 COUNT、SUM、AVG、MIN和MAX这样的标准聚合,其他的聚合通常不具有移植性。在应用服务器上进行聚合允许你应用更复杂的业务逻辑;但你需要 先在应用程序中载入详细的数据)。
范例2
分析员需要查看一个取自大数据表的电流ISO(Independent System Operator)聚合列表。最开始他们想要显示大多数字段,尽管数据库能在1分钟内做出响应,应用程序也要花30分钟将1百万行数据加载到前端UI。经 过重新分析,分析员保留了14个字段。因为去掉了很多可选的高聚合度字段,从剩下的字段中进行聚合分组返回的数据要少很多,而且大多数情况下的数据加载时 间也缩小到了可接受的范围内。
范例3
过24个“非标准”(shaped,表示每小时都可以有自己的电量和价格;如果所有24小时的电量和价格相同,我们称之为“标准”)小时会修改小时电流交易,其中包括2个属性:每小时电量和价格。起初我们使用Hibernate的select-before-update特性,就是更新24行数据需要24次选择。因为我们只需要2个属性,而且如果不修改电量或价格的话也没有业务规则禁止无效修改,我们就关闭了select-before-update特性,避免了24次选择。
4.2继承映射调优
尽管继承映射是领域对象的一部分,出于它的重要性我们将它单独出来。HRD [1]中的第9章“继承映射”已经说得很清楚了,所以我们将关注SQL生成和针对每个策略的调优建议。
以下是HRD中范例的类图:
hibernate性能优化
4.2.1 每个类层次一张表
只需要一张表,一条多态查询生成的SQL大概是这样的:
select id, payment_type, amount, currency, rtn, credit_card_type from payment
针对具体子类(例如CashPayment)的查询生成的SQL是这样的:
select id, amount, currency from payment where payment_type=’CASH’
这样做的优点包括只有一张表、查询简单以及容易与其他表进行关联。第二个查询中不需要包含其他子类中的属性。所有这些特性让该策略的性能调优要比其他策略容易得多。这种方法通常比较适合数据仓库系统,因为所有数据都在一张表里,不需要做表连接。
主要的缺点整个类层次中的所有属性都挤在一张大表里,如果有很多子类特有的属性,数据库中就会有太多字段的取值为null,这为当前基于行的数据库 (使用基于列的DBMS的数据仓库处理这个会更好些)的SQL调优增加了难度。除非进行分区,否则唯一的数据表会成为热点,OLTP系统通常在这方面都不 太好。
4.2.2每个子类一张表
需要4张表,多态查询生成的SQL如下:
select id, payment_type, amount, currency, rtn, credit_card type, case when c.payment_id is not null then 1 when ck.payment_id is not null then 2 when cc.payment_id is not null then 3 when p.id is not null then 0 end as clazz from payment p left join cash_payment c on p.id=c.payment_id left join cheque_payment ck on p.id=ck.payment_id left join credit_payment cc on p.id=cc.payment_id; 针对具体子类(例如CashPayment)的查询生成的SQL是这样的: select id, payment_type, amount, currency from payment p left join cash_payment c on p.id=c.payment_id;
针对具体子类(例如CashPayment)的查询生成的SQL是这样的:
select id, payment_type, amount, currency from payment p left join cash_payment c on p.id=c.payment_id;
优点包括数据表比较紧凑(没有不需要的可空字段),数据跨三个子类的表进行分区,容易使用超类的表与其他表进行关联。紧凑的数据表可以针对基于行的 数据库做存储块优化,让SQL执行得更好。数据分区增加了数据修改的并发性(除了超类,没有热点),OLTP系统通常会更好些。
同样的,第二个查询不需要包含其他子类的属性。
缺点是在所有策略中它使用的表和表连接最多,SQL语句稍显复杂(看看Hibernate动态鉴别器的长CASE子句)。相比单张表,数据库要花更多时间调优数据表连接,数据仓库在使用该策略时通常不太理想。
因为不能跨超类和子类的字段来建立复合索引,如果需要按这些列进行查询,性能会受影响。任何子类数据的修改都涉及两张表:超类的表和子类的表。
4.2.3每个具体类一张表
涉及三张或更多的表,多态查询生成的SQL是这样的:
select p.id, p.amount, p.currency, p.rtn, p. credit_card_type, p.clazz from (select id, amount, currency, null as rtn,null as credit_card type, 1 as clazz from cash_payment union all select id, amount, null as currency, rtn,null as credit_card type, 2 as clazz from cheque_payment union all select id, amount, null as currency, null as rtn,credit_card type, 3 as clazz from credit_payment) p;
针对具体子类(例如CashPayment)的查询生成的SQL是这样的:
select id, payment_type, amount, currency from cash_payment;
优点和上面的“每个子类一张表”策略相似。因为超类通常是抽象的,所以具体的三张表是必须的[开头处说的3张或更多的表是必须的],任何子类的数据修改只涉及一张表,运行起来更快。
缺点是SQL(from子句和union all子查询)太复杂。但是大多数数据库对此类SQL的调优都很好。
如果一个类想和Payment超类关联,数据库无法使用引用完整性(referential integrity)来实现它;必须使用触发器来实现它。这对数据库性能有些影响。
4.2.4使用隐式多态实现每个具体类一张表
只需要三张表。对于Payment的多态查询生成三条独立的SQL语句,每个对应一个子类。Hibernate引擎通过Java反射找出Payment的所有三个子类。
具体子类的查询只生成该子类的SQL。这些SQL语句都很简单,这里就不再阐述了。
它的优点和上节类似:紧凑数据表、跨三个具体子类的数据分区以及对子类任意数据的修改都只涉及一张表。
缺点是用三条独立的SQL语句代替了一条联合SQL,这会带来更多网络IO。Java反射也需要时间。假设如果你有一大堆领域对象,你从最上层的Object类进行隐式选择查询,那该需要多长时间啊!
根据你的映射策略制定合理的选择查询并非易事;这需要你仔细调优业务需求,基于特定的数据场景制定合理的设计决策。
以下是一些建议:
设计细粒度的类层次和粗粒度的数据表。细粒度的数据表意味着更多数据表连接,相应的查询也会更复杂。
如非必要,不要使用多态查询。正如上文所示,对具体类的查询只选择需要的数据,没有不必要的表连接和联合。
“每个类层次一张表”对有高并发、简单查询并且没有共享列的OLTP系统来说是个不错的选择。如果你想用数据库的引用完整性来做关联,那它也是个合适的选择。
“每个具体类一张表”对有高并发、复杂查询并且没有共享列的OLTP系统来说是个不错的选择。当然你不得不牺牲超类与其他类之间的关联。
采用混合策略,例如“每个类层次一张表”中嵌入“每个子类一张表”,这样可以利用不同策略的优势。随着你项目的进化,如果你要反复重新映射,那你可能也会采用该策略。
“使用隐式多态实现每个具体类一张表”这种做法并不推荐,因为其配置过于繁缛、使用“any”元素的复杂关联语法和隐式查询的潜在危险性。
范例4
下面是一个交易描述应用程序的部分领域类图:
hibernate性能优化
开始时,项目只有GasDeal和少数用户,它使用“每个类层次一张表”。
OilDeal和ElectricityDeal是后期产生更多业务需求后加入的。没有改变映射策略。但是ElectricityDeal有太多自己的属性,因此有很多电相关的可空字段加入了Deal表。因为用户量也在增长,数据修改变得越来越慢。
重新设计时我们使用了两张单独的表,分别针对气/油和电相关的属性。新的映射混合了“每个类层次一张表”和“每个子类一张表”。我们还重新设计了查询,以便允许针对具体交易子类进行选择,消除不必要的列和表连接。
4.3 领域对象调优
基于4.1节中对业务规则和设计的调优,你得到了一个用POJO来表示的领域对象的类图。我们建议:
4.3.1 POJO调优
从读写数据中将类似引用这样的只读数据和以读为主的数据分离出来。
只读数据的二级缓存是最有效的,其次是以读为主的数据的非严格读写。将只读POJO标识为不可更改的(immutable)也是一个调优点。如果一个服务层方法只处理只读数据,可以将它的事务标为只读,这是优化Hibernate和底层JDBC驱动的一个方法。
细粒度的POJO和粗粒度的数据表。
基于数据的修改并发量和频率等内容来分解大的POJO。尽管你可以定义一个粒度非常细的对象模型,但粒度过细的表会导致大量表连接,这对数据仓库来说是不能接受的。
优先使用非final的类。
Hibernate只会针对非final的类使用CGLIB代理来实现延时关联获取。如果被关联的类是final的,Hibernate会一次加载所有内容,这对性能会有影响。
使用业务键为分离(detached)实例实现equals()和hashCode()方法。
在多层系统中,经常可以在分离对象上使用乐观锁来提升系统并发性,达到更高的性能。
定义一个版本或时间戳属性。
乐观锁需要这个字段来实现长对话(应用程序事务)[译注:session译为会话,conversion译为对话,以示区别]。
优先使用组合POJO。
你的前端UI经常需要来自多个不同POJO的数据。你应该向UI传递一个组合POJO而不是独立的POJO以获得更好的网络性能。
有两种方式在服务层构建组合POJO。一种是在开始时加3.2载所有需要的独立POJO,随后抽取需要的属性放入组合POJO;另一种是使用HQL投影,直接从数据库中选择需要的属性。
如果其他地方也要查找这些独立POJO,可以把它们放进二级缓存以便共享,这时第一种方式更好;其他情况下第二种方式更好。
4.3.2 POJO之间关联的调优
如果可以用one-to-one、one-to-many或many-to-one的关联,就不要使用many-to-many。
many-to-many关联需要额外的映射表。
尽管你的Java代码只需要处理两端的POJO,但查询时,数据库需要额外地关联映射表,修改时需要额外的删除和插入。
单向关联优先于双向关联。
由于many-to-many的特性,在双向关联的一端加载对象会触发另一端的加载,这会进一步触发原始端加载更多的数据,等等。
one-to-many和many-to-one的双向关联也是类似的,当你从多端(子实体)定位到一端(父实体)。
这样的来回加载很耗时,而且可能也不是你所期望的。
不要为了关联而定义关联;只在你需要一起加载它们时才这么做,这应该由你的业务规则和设计来决定(见范例5)。
另外,你要么不定义任何关联,要么在子POJO中定义一个值类型的属性来表示父POJO的ID(另一个方向也是类似的)。
集合调优
如果集合排序逻辑能由底层数据库实现,就使用“order-by”属性来代替“sort”,因为通常数据库在这方面做得比你好。
集合可以是值类型的(元素或组合元素),也可以是实体引用类型的(one-to-many或many-to-many关联)。对引用类型集合的调优主要是调优获取策略。对于值类型集合的调优,HRD [1]中的20.5节“理解集合性能”已经做了很好的阐述。
获取策略调优。请见4.7节的范例5。
范例5
我们有一个名为ElectricityDeals的核心POJO用于描述电的交易。从业务角度来看,它有很多many-to-one关联,例如和 Portfolio、Strategy和Trader等的关联。因为引用数据十分稳定,它们被缓存在前端,能基于其ID属性快速定位到它们。
为了有好的加载性能,ElectricityDeal只映射元数据,即那些引用POJO的值类型ID属性,因为在需要时,可以在前端通过portfolioKey从缓存中快速查找Portfolio:
<property name="portfolioKey" column="PORTFOLIO_ID" type="integer"/>
这种隐式关联避免了数据库表连接和额外的字段选择,降低了数据传输的大小。
4.4 连接池调优
由于创建物理数据库连接非常耗时,你应该始终使用连接池,而且应该始终使用生产级连接池而非Hibernate内置的基本连接池算法。
通常会为Hibernate提供一个有连接池功能的数据源。Apache DBCP的BasicDataSource[13]是一个流行的开源生产级数据源。大多数数据库厂商也实现了自己的兼容JDBC 3.0的连接池。举例来说,你也可以使用Oracle ReaApplication Cluster [15]提供的JDBC连接池[14]以获得连接的负载均衡和失败转移。
不用多说,你在网上能找到很多关于连接池调优的技术,因此我们只讨论那些大多数连接池所共有的通用调优参数:
最小池大小:连接池中可保持的最小连接数。
最大池大小:连接池中可以分配的最大连接数。
如果应用程序有高并发,而最大池大小又太小,连接池就会经常等待。相反,如果最小池大小太大,又会分配不需要的连接。
最大空闲时间:连接池中的连接被物理关闭前能保持空闲的最大时间。
最大等待时间:连接池等待连接返回的最大时间。该参数可以预防失控事务(runaway transaction)。
验证查询:在将连接返回给调用方前用于验证连接的SQL查询。这是因为一些数据库被配置为会杀掉长时间空闲的连接,网络或数据库相关的异常也可能会杀死连接。为了减少此类开销,连接池在空闲时会运行该验证。
4.5事务和并发的调优
短数据库事务对任何高性能、高可扩展性的应用程序来说都是必不可少的。你使用表示对话请求的会话来处理单个工作单元,以此来处理事务。
考虑到工作单元的范围和事务边界的划分,有3中模式:
每次操作一个会话。每次数据库调用需要一个新会话和事务。因为真实的业务事务通常包含多个此类操作和大量小事务,这一般会引起更多数据库活动(主要是数据库每次提交需要将变更刷新到磁盘上),影响应用程序性能。这是一种反模式,不该使用它。
使用分离对象,每次请求一个会话。每次客户端请求有一个新会话和一个事务,使用Hibernate的“当前会话”特性将两者关联起来。
在一个多层系统中,用户通常会发起长对话(或应用程序事务)。大多数时间我们使用Hibernate的自动版本和分离对象来实现乐观并发控制和高性能。
带扩展(或长)会话的每次对话一会话。在一个也许会跨多个事务的长对话中保持会话开启。尽管这能把你从重新关联中解脱出来,但会话可能会内存溢出,在高并发系统中可能会有旧数据。
你还应该注意以下几点。
如果不需要JTA就用本地事务,因为JTA需要更多资源,比本地事务更慢。就算你有多个数据源,除非有跨多个数据库的事务,否则也不需要 JTA。在最后的一个场景下,可以考虑在每个数据源中使用本地事务,使用一种类似“Last Resource Commit Optimization”[16]的技术(见下面的范例6)。
如果不涉及数据变更,将事务标记为只读的,就像4.3.1节提到的那样。
总是设置默认事务超时。保证在没有响应返回给用户时,没有行为不当的事务会完全占有资源。这对本地事务也同样有效。
如果Hibernate不是独占数据库用户,乐观锁会失效,除非创建数据库触发器为其他应用程序对相同数据的变更增加版本字段值。
范例6
我们的应用程序有多个在大多数情况下只和数据库“A”打交道的服务层方法;它们偶尔也会从数据库“B”中获取只读数据。因为数据库“B”只提供只读数据,我们对这些方法在这两个数据库上仍然使用本地事务。
服务层上有一个方法设计在两个数据库上执行数据变更。以下是伪代码:
//Make sure a local transaction on database A exists @Transactional (readOnly=false, propagation=Propagation.REQUIRED) public void saveIsoBids() { //it participates in the above annotated local transaction insertBidsInDatabaseA(); //it runs in its own local transaction on database B insertBidRequestsInDatabaseB(); //must be the last operation
因为insertBidRequestsInDatabaseB()是saveIsoBids ()中的最后一个方法,所以只有下面的场景会造成数据不一致:
在saveIsoBids()执行返回时,数据库“A”的本地事务提交失败。
但是,就算saveIsoBids()使用JTA,在两阶段提交(2PC)的第二个提交阶段失败的时候,你还是会碰到数据不一致。因此如果你能处理好上述的数据不一致性,而且不想为了一个或少数几个方法引入JTA的复杂性,你应该使用本地事务。
关于作者
Yongjun Jiao是SunGard Consulting Services的技术主管。过去10年中他一直是专业软件开发者,他的专长包括Java SE、Java EE、Oracle和应用程序调优。他最近的关注点是高性能计算,包括内存数据网格、并行计算和网格计算。
Stewart Clark是SunGard Consulting Services的负责人。过去15年中他一直是专业软件开发者和项目经理,他的专长包括Java核心编程、Oracle和能源交易。
相关推荐
在Java编程中,性能优化是提升程序运行效率的关键。...以上就是Java编程中为了性能要尽量做到的26点,遵循这些原则,可以帮助编写出更高效的Java代码。在实际开发中,结合具体业务场景灵活运用,才能真正提升程序性能。
在Java编程中,优化性能是每个开发者关注的重要话题。这篇博文主要探讨了为了提升程序运行...以上是Java编程中为了性能提升应尽量做到的一些实践。理解并应用这些原则,可以帮助开发者编写出更加高效、响应更快的程序。
Java编程中的性能优化是开发者需要关注的重要议题,尤其是在大型系统或者高并发环境下,代码的性能表现直接影响到程序的运行效率和资源消耗。以下基于标题和描述中的知识点进行详细阐述: 1. **使用单例模式**:...
本文将深入探讨Java编程中为了提升性能而需关注的关键点,这些知识点基于标题“Java编程中‘为了性能’要做的几点”,描述及部分内容,旨在帮助开发者理解和实践高效编程技巧。 ### 1. 合理运用单例模式 单例模式...
本文将基于“Java编程中‘为了性能’需做的26件事”的标题和描述,深入探讨一系列实用技巧,旨在帮助程序员编写出更加高效、低耗的Java应用。 #### 1. 合理运用单例模式 单例模式通过确保类只有一个实例并提供一个...
Java编程规范在性能方面的重要性不言而喻,良好的编码习惯能够显著提升程序的运行效率,减少不必要的资源消耗。以下是对给定文件中提到的一些关键性能规则和建议的详细解释: 1. **日志规则**: - 在输出Debug或...
在Java编程中,为了提升程序性能,开发人员需要遵循一系列最佳实践。以下是对标题和描述中提到的26个性能优化技巧的详细说明: 1. **单例模式的使用**:单例模式可以减少资源创建,提高效率,适用于控制资源访问、...
### Java程序性能优化知识点 #### 一、避免在循环条件中使用复杂表达式 在Java程序中,尤其是在不做编译优化的情况下,如果在循环条件中使用了复杂的表达式,那么这个表达式会在每次循环时被重新计算。这种重复计算...
在Java编程领域,性能优化是不可或缺的一环,尤其是在构建大规模、高并发的系统时。"高性能java代码优化建议"这一主题涵盖了众多资深开发者积累的实践经验,旨在帮助开发者编写出更高效、性能更强的代码。以下是一些...
《阿里巴巴Java编程规范》是阿里巴巴官方团队为了提升Java开发者代码质量与一致性,降低维护成本而制定的一套详尽的编程指南。这份规范不仅对新手有很好的引导作用,也为经验丰富的开发者提供了良好的编码习惯建议,...
根据提供的文件信息,本文将详细阐述Java程序性能优化的相关知识点。 一、循环条件中避免使用复杂表达式 在循环中,循环条件会被反复计算。如果循环条件复杂,会导致每次迭代都进行大量的计算,从而降低程序运行...
在本文中,我们将讨论如何用Java开发高性能、高并发Web应用,并提供一些Java程序性能优化techniques。 首先,让我们来讨论生成对象时的合理分配空间和大小。Java中的许多类都有它们的默认的空间分配大小,对于一些...
在Java编程中,代码优化是一个关键环节,目的是使程序在有限的资源下更高效地运行。优化主要包括两个方面:减小代码体积和提高代码执行效率。以下是一些关于如何提高Java代码效率的具体策略: 1. 使用`final`修饰符...