用数据库框架控制开发环境 控制环境是反洗钱内控框架体系中
本文从数据库管理员的角度,讨论了保护数据库的重要性。作者建议通过添加Java数据库框架,在开发人员和数据库之间构建一个经过反复试验的稳固的中间层,以降低风险,并提供跟踪及报告问题的工具。
没有什么比这样一只Java开发小组更影响数据库性能的了:他们有一堆需求,又要使用数据库,但却仅仅了解了一些Java数据库连接(JDBC)的皮毛。连接会打开并闲置好几个小时,一旦连接超时,问题就会扔给数据库管理员(DBA)。未关闭的语句和占用系统资源的结果将是数据库管理员头痛的问题。编程人员使用含糊的JDBC方法和动态SQL导致性能低下。
因此,本文讨论了如何使用Java数据库框架帮助数据库远离随意操作的开发人员。它提供了连接池和管理、跟踪及报告JDBC对象、有选择地删除性能低下的结构和方法。目的在于防止开发人员影响性能、避免沾上常见的不良开发习惯,并且在无法防止这类活动的情况下提供跟踪机制。这样一来,数据库管理员就可以找到问题的根源,在系统进入生产环境之前改正问题。使用Java数据库框架的另一个目的在于,让一切开发工作都保持简单,那样开发人员就可以尽快熟悉情况。
与任何框架一样,Java数据库框架的目的在于隐藏复杂性,并为处理复杂任务提供一套标准操作程序。同样重要的一个方面是让执行任务的方式具有一致性。这可以改进封装、大大提高代码的可维护性。只要设想一下:假如每个人都构建各自的类和方法来建立数据库连接,势必会导致混乱、无序的局面。除了数据库外,框架通常适用的一些方面包括:进程间通信、多线程管理和图形用户界面(GUI)标准。
本文描述的框架旨在供所有中间件开发人员使用,它在由数据库开发商提供的实际的JDBC实现上添加了一层(如图1)。
JDBC问题和陷阱
记得下面这一点很重要:JDBC是数据库开发商提供的实现,但不是所有的实现都是相同的。但是,在数据库框架让,开发人员可以在某种程度上让它们相同。在个别情况下,同一家开发商提供的JDBC驱动程序的各个版本之间会存在差异。不同开发商提供的JDBC驱动程序免不了总是会存在差异。差异通常出现在以下几方面:连接管理;存储过程和返回ResultSets;处理ResultSets;元数据支持;因语句和ResultSets未结束而消耗资源;连接未关闭带来的问题;性能异常及实现缓慢;数据库优化器从一个版本到下一个版本所出现的变化;数据库从一个版本到下一个版本添加了新特性。
笔者曾有幸参于来自Sybase和Oracle的JDBC实现,它们采用的方法形成了鲜明对比。笔者常开玩笑说,Oracle好比是“父亲”,Sybase好比是“母亲”。如果你在冬天没穿衣服就出去,母亲会叫你停下来,穿上衣服,免得感冒;而父亲会一言不发地看着,觉得要是天气寒冷,你会晓得自己添衣服。Sybase驱动程序在连接、语句和结果集管理方面可以为开发人员做大量工作;而Oracle驱动程序只会做开发人员让它做的那些事情。如果开发人员没有结束语句,它恐怕不会自动结束,也肯定不会把会话、进程及打开的游标清理干净。这里不会去研究哪个方向是正确的,我们只是为框架添加了代码,让它们看上去很相似。
图2显示了框架示例,旨在处理上面讨论的JDBC问题。它还在数据库上提供了抽象层,那样开发人员可以更迅速、更安全地访问数据库。CWDatabase类负责管理开发人员的所有访问,它利用CWConnectionPool管理连接、利用CWSqlRepository管理SQL字符串。较低级的Connection和Statement类都进行了封装,以便提供跟踪机制,并保证连接重新签入到连接池后,所有语句和结果集都已关闭。下文讨论了这些类,随后讨论了比较高级的框架特性,用于跟踪执行性能、限制JDBC特性及报告连接池。下面的所有框架类都以代表笔者所在公司CodeWorks Software的“CW”开头,这样它们很容易识别。
重要的类
数据库接口类:CWDatabase和CWParamList
为开发人员添加用于数据库访问的一个简单类。通过创建框架类的实例,他们可以获得运行SQL命令及存储过程的连接及简单方法。异常处理得到了适当的处理及报告;通过使用CWParamList允许用户创建参数列表,数据库管理员就可以牢牢地控制Java数据类型及它们如何绑定到数据库中的基本数据类型。目的在于绝对不允许用户直接控制jdbc.Connection实例。牢牢获得这种控制权的另一个好处是,通常可以获得很高的数据速率,因为数据库管理员可以控制数据库访问。
public class CWDatabase
{
Connection m_conn = null;
public CWDatabase()
{
m_conn = CWConnectionPool.checkOut();
}
public int executeUpdate(String queryName, CWParamList plist)
public int executeUpdate(String queryName)
public ResultSet executeQuery(String queryName, CWParamList plist)
public ResultSet executeQuery(String queryName)
private void processException(String msg, Throwable ex)
}
有了上述这个类,用户可以运行如下的简单查询:
public void updateSensorType()
{
CWDatabase theDB = null;
try
{
// 创建数据库实例和参数列表实例
theDB = new CWDatabase();
CWParamList plist = new CWParamList();
// 添加参数
plist.addParameter(1,"TYPE1");
plist.addParameter(2,1);
plist.addParameter(3,"ACT");
// 执行更新
int numupdate = theDB.executeUpdate("sensor.updateSensorType",plist);
}
catch( Exception exception )
{
CWExceptionReporter.write(this,CWExceptionReporter.FATAL,"Error updating sensor type.");
}
finally
{
theDB.close();
}
return;
} // End方法
连接池:CWConnectionPool
建立连接很费资源,所以通过重复使用连接,就可以避免每次重新建立连接带来的成本。系统启动时,可以为连接池提供可随时使用的几个连接。用户创建数据库接口类的实例后,连接就会签出。用户调用关闭命令后,连接重新签入,供其他用户使用。
虽然重复使用连接是连接池的主要目的,但还有许多其他好处。因为所有连接都在一个地方加以管理及创建,所以数据库管理员就能够严格管理隔离级别和数据库选项。SQL Anywhere在这方面的例子包括:DELAYED_COMMITS、ISOLATION_LEVEL和 COOPERATIVE_COMMITS,以及面向原始设备制造商(OEM)版本的软件的授权代码方面的设置。
不过,笔者在建立连接池时发现了一个问题,它们会留下一些“行李(baggage)”。这样一来,多次重复使用会渐渐减慢连接速度。而且,笔者根本查不出这个问题的根源,不过怀疑它与PreparedStatements、ResultSets或者当时出现的其他某种内部跟踪机制有关。鉴于所有连接都由连接池管理,这样就有可能跟踪连接存在了多久;重新签入后,可以“刷新”连接。通过在过了限定时间后丢弃连接,就可以更好地维持很高的性能比率。
这种严密跟踪机制的另一个好处就是,还可以监控谁把连接签出了、时间有多久。长时间保持的连接,尤其是作为成员变量,可能会导致问题。如果连接好几个小时都处于休眠状态,就会超时,进而导致问题。有了这种额外的跟踪机制,数据库管理员就可以报告谁拥有哪个连接、打开状态保持了多久。如果知道签出问题连接的那一行代码,就能找到相应的开发人员,告诉他如何使用框架。因为跟踪连接需要一定开销,笔者在编写框架时在默认状态下禁用了这项特性,不过测试过程中可以启用它。
public class CWConnectionPool
{
// 维护闲置及签出列表上的连接
private static ArrayList m_freePool = null;
private static HashMap m_outPool = null;
public CWConnectionPool()
{
m_freePool = new ArrayList();
m_outPool = new HashMap();
}
public static void initialize()
public static synchronized Connection checkOut()
public static synchronized void checkIn(Connection conn)
public static void discardConnection(Connection conn)
private static CWConnection createConnection()
public static reportConnectionPool()
}
SQL存储库:CWSqlRepository
SQL语句最好保存在不同文件中,那样不必重新编译代码就可以修改语句。譬如说,如果发现了某个性能问题,经过分析,发现是数据库优化器选错了索引,这时就很容易添加SQL提示。为了管理SQL语句,笔者使用了CWSQLRepository类。该存储库还允许重复使用代码;又因为所有SQL语句都在同一个地方,数据库管理员就更容易找到可能受模式改变影响的所有语句。
public class CWSqlRepository
{
private static CWSqlRepository m_SQLRepository;
private static Properties m_SQLrepositoryTable;
public static void initialize()
{
if (m_SQLRepository == null)
m_SQLRepository = new CWSqlRepository();
return;
}
private static void loadSQLrepository(String sqlfile)
public static String getSQLString(String tag)
}
封装JDBC类
说到简化数据库访问,通过提供上面讨论的那几个简单类,就能得到很大成效。不过说到消除JDBC实现在较低层面上的差异,从事重复工作毫无意义。只要封装JDBC类,就可以处理问题、添加功能。JDBC文档齐全,开发人员很熟悉它,数据库管理员也是一样。另外,很容易教人学会,并提供合理使用的示例。通过创建封装器类,数据库管理员可以添加自己需要的任何跟踪、定时及报告机制,还可以消除差异。只有在极少数情况下,开发人员才真正知道自己在使用Connection.prepareStatement()的框架实现,而不是实际的实现。
CWConnection
要封装的最重要的一个类是java.sql.Connection。数据库管理员可以在这里跟踪某连接签出了多久,并维护所有已创建语句的列表。为了调试,数据库管理员可以维护堆栈跟踪信息(stack trace)。这样在建立连接后,一旦发现“连接滥用”,就能更准确地找到建立该连接的代码,譬如说,连接签出时间超过规定。
public class CWConnection implements Connection
{
// 连接的基本信息
private Connection _conn = null;
//封装的java.sql.Connection
private int _connNum ;
// 跟踪号码
private long _createTime;
// 设定时间
private ArrayList _stmtTracker;
// 跟踪语句
public int getConnectionNum()
public int getElapseTime()
public void closeStatements()
private ArrayList getStatementTracker()
private void clearStatementTracker()
String reportConnnection()
// 封装的JDBC方法
public Statement createStatement() throws SQLException
{
CWStatement stmt = new CWStatement(_conn.createStatement());
_stmtTracker.add(stmt);
return stmt;
}
}
CWStatement、CWPreparedStatement和CWCallableStatement
数据库管理员可以编写这样的框架:很少允许开发人员可以控制Statements、CallableStatements和PreparedStatements。封装这些类的主要原因是可以跟踪时间设定,如果返回结果集的话,还可以跟踪ResultSet实例。虽然在下面讨论了串行化的结果集,但笔者并不建议把结果集隐藏起来,不让开发人员看到,因为接口方面的文档很齐全。主要的滥用现象就是让结果集打开着,不过对此进行跟踪却相当简单。连接签入后,所有语句都被关闭,每个语句保证结果集被关闭。笔者仍封装了ResultSet,但主要目的是消除性能低下的方法,那样开发人员就没法用它们。稍后会讨论这个话题。
public class CWStatement implements Statement
{
// 语句的基本信息
private Statement m_stmt = null;
//封装的java.sql.Statement
private int m_stmtNum;
// 跟踪号码
private long m_createTime;
// 设定时间
protected ResultSet m_rsTracker;
// 跟踪结果集
private String m_sqlTracker;
// 随该语句一起发出的Sql
public String reportStatement()
public CWStatement( Statement stmt, int stmtNum)
{
m_stmt = stmt;
m_stmtNum = stmtNum;
m_createTime = System.currentTimeMillis();
m_sqlTracker = null;
m_rsTracker = null;
}
public ResultSet executeQuery(String sql) throws SQLException
{
CWResultSet rs = new CWResultSet(m_stmt.executeQuery(sql));
m_rsTracker = rs;
m_sqlTracker = sql;
return rs;
}
}
框架的先进思想
上面讨论的话题集中于简化数据库接口、连接管理,并提供防范常见JDBC问题的方法。接下来会介绍框架的附加部分,它们为监控及控制使用数据库的开发人员提供了更有效的机制,包括:解决结果集的问题、限制性能低下的操作、监控SQL性能。
CWResultSetSerialized
许多Java编程人员没有认识到(或者忘了)ResultSets实际上是数据库游标。它们传递引用、把它们存储为成员变量,往往从不关闭,因而占用了数据库资源。为了消除所有风险,可利用JDBC结果集来创建串行化的结果集。这还可以让结果集通过远程方法调用(RMI)在进程之间发送。
在极端情况下,引起阻塞问题的结果集频频传送,以至笔者查不到该在什么地方关闭它。一旦用串行化的结果集取而代之,就能关闭实际的ResultSet,所有问题都立马消失了。串行化结果集的任何实现都需要限制可以创建的行数,因为100万行的串行化结果集会引起性能问题。笔者开始限制在5000行。
下面的代码表明了结果集管理不善,因为它传到了方法外面。现在很难知道它是不是被关闭了,因为方法只有出现了错误才关闭数据库实例。代码可以使用,不过,要是结果集在调用方法里面没有关闭,连接会处于签出状态,游标仍然是打开的。
public ResultSet getAllSensors()
{
CWDatabase theDB = null;
ResultSet rs = null;
try
{
// 创建数据库实例和参数列表实例
theDB = new CWDatabase();
// 执行查询
rs = theDB.executeQuery("sensor.getAllSensors");
}
catch( Exception exception )
{
CWExceptionReporter.write(this,CWExceptionReporter.FATAL,"Error during query: " + " sensor.getAllSensors ");
theDB.close();
}
return rs;
} // End方法
因为有时在开发周期很晚时才发现这类问题,因而无法通过改写方法来解决,可以通过以下办法解决问题:传回串行化的结果集,通过finally块关闭数据库实例,从而保证一切都正常关闭。笔者仍认为,数据库管理员应当给引起问题的工程师出难题,不过系统代码冻结前一天不是改变大量代码的时候。所作的变化用下面的黑体字表明:
public ResultSet getAllSensors()
{
CWDatabase theDB = null;
CWResultSetSerialized rss = null;
try
{
// 创建数据库实例和参数列表实例
theDB = new CWDatabase();
// 执行更新
ResultSet rs = theDB.executeQuery("sensor.getAllSensors");
rss = new ResultSetSerialized(rs);
}
catch( Exception exception )
{
CWExceptionReporter.write(this,CWExceptionReporter.FATAL,"Error during query: " + " sensor.getAllSensors ");
}
finally
{
theDB.close();
}
// 返回串行化结果集
return rss;
} // End方法
性能级别
因性能需求不同,数据库管理员的要求可能大不相同,有的是“只允许速度最快的数据库访问”,有的是“我不在乎访问速度,只要可以使用任何特性”。大部分人介于两者之间。 能够“关闭”已知性能低下的JDBC方法大有帮助。譬如说,使用ResultSet.update()比使用不同的Statement.execute()来执行同样的更新慢得多。允许用户使用rs.last()等方法返回不是“只能向前移动的”结果集也很慢。笔者使用三个基本的性能级别:
● 级别1:开发人员不可以访问连接,也无法发出动态SQL。所有性能低下的方法都被关闭,包括结果集更新和元数据访问。
● 级别2:允许动态查询,但其他所有性能低下的方法仍然受到限制。笔者的架构就使用这种默认值。
● 级别3:全面的JDBC访问,没有任何限制。
关闭JDBC特性后,笔者建议发出异常,这可以解释特性已被关闭,需要联系数据库管理员。在开发期间,数据库管理员可以决定是否真正需要该特性,并确定要不要重新添加到框架上,或者更改该特性的性能级别。
public class CWResultSet implements ResultSet
{
private ResultSet m_rs;
private long m_createTime;
private int m_perf_level;
public CWResultSet( ResultSet rs, perf_level)
{
m_rs = rs;
m_createTime = System.currentTimeMillis();
m_perf_level = perf_level;
}
public void updateRow() throws SQLException
{
if(perf_level
// 执行更新
startQueryTimer();
int numupdate =theDB.executeUpdate queryName,plist);
stopQueryTimer(queryName);
}
如果查询跟踪机制开启,查询时间就会记录到数据库里面。
(沈建苗 编译)
