在非线程安全的数据库中使用多线程
有些数据库管理系统的设计为了尽量的简化而避开了线程安全的话题,这样在处理WEB请求等等并发情况的时候,可能造成一些线程同步的问题,后果很严重。所以这些数据库中的一些使用了一点小技巧来避免这种情况的发生,比如抛出一个异常来提示程序员必须要在线程内部创建数据库连接。这一类数据库包括MySQL和Sqlite,都是我很喜欢的数据库。
好了,谈谈今天的话题,在CherryPy中使用非线程安全的数据库。也就是说,让每一个线程保留一个数据库连接对象,因为这些线程都是在线程池中,一旦启动就反复的处理请求,而在请求处理结束之后并不会关闭。(by gashero)所以将数据库连接在请求之间共享可以减少建立数据库连接的时间,对提高性能有很大的作用。CherryPy早就想到了可能出现这种情况,所以提供了一点相关的支持。就是可以在每次线程启动的时候建立数据库连接,然后在程序中可以使用这些数据库连接。下面是建立这种数据库连接所必须的过程:
·使用××××告知CherryPy要在启动线程时调用一个函数来设置数据库连接
·在待调用函数中创建数据库连接
·将此数据库连接存入cherrypy.threadData的子元素中,这些元素是线程相关的
·在需要调用数据库的方法中从cherrypy.threadData中取回自己的数据库连接
·使用这个数据库连接进行操作,但是注意,操作完成之后可以关闭游标(cursor)但是不要关闭数据库连接。因为CherryPy是基于线程池操作的,这个线程处理完成请求之后并不会销毁,而是等待处理下一个请求,这样才是在请求之间的同一个线程里共享数据库连接。
下面是一个例子:
# -*- coding: gbk -*-
# File: cpsqlite.py
# Date: 2006-9-21
# Author: gashero
# Copyright@1999-2006, Harry Gashero Liu.
# Comment: 在线程环境中使用SQLObject连接SQLite
import os
import cherrypy
from sqlobject import *
from sqlobject.sqlite.sqliteconnection import SQLiteConnection
localDir=os.path.dirname(__file__)
curpath=os.path.normpath(os.path.join(os.getcwd(),localDir))
dbpath=curpath.replace(':','|').replace('//','/')
sqlitedbfilename='dddccc.db'
dbfullpath=os.path.join(curpath,sqlitedbfilename)
connection_string=("sqlite:/"+dbpath+"/"+sqlitedbfilename)
#无奈,要编码成UTF-8才可以被识别
class Root:
@cherrypy.expose
def index(self,pid="1",name=None):
"""http://localhost:8080/?pid=7"""
p=Person.get(int(pid))
if name!=None:
p=Person(name=name)
return ""+p.name.encode('gbk')
class Person(SQLObject):
_connection=None
name=UnicodeCol(length=30,varchar=False)
def initial():
global p
Person.createTable(ifNotExists=True)
if Person.select().count()<5:
p=[]
p.append(Person(name="a1"))
p.append(Person(name="a2"))
p.append(Person(name="a3"))
p.append(Person(name="a4"))
p.append(Person(name="a5"))
p.append(Person(name="a6"))
p.append(Person(name=u"哈哈"))
return
def test1():
"""测试不支持线程的情况下,提示的错误"""
global connection_string
conn=connectionForURI(connection_string.decode('gbk').encode('utf8'))
Person._connection=conn
initial()
return
def test2():
"""测试支持线程的情况下,完好的情况"""
global connection_hub
connection_hub=dbconnection.ConnectionHub()
Person._connection=connection_hub
set_conn_hub(12345) #建立一个临时的DB连接,用于initial()方法
initial()
cherrypy.server.on_start_thread_list=list()
cherrypy.server.on_start_thread_list.append(set_conn_hub)
return
def set_conn_hub(threaID):
"""用于test2()的,用在线程启动时的回调函数"""
global dbfullpath
global connection_hub
global connection_string
aconn=SQLiteConnection(os.path.join(dbfullpath.decode('gbk').encode('utf8')))
connection_hub.threadConnection=aconn
return
test2()
cherrypy.root=Root()
cherrypy.server.start()
HTML="""/
<html>
<body>
<form method="POST" action="/">
<input type="text" name="name" value="test">
<input type="submit" name="submit" value="Submit!">
</form>
</body>
</html>
"""
代码开始处使用了一些技巧来生成可供SQLObject使用的连接字符串,其中dbfullpath是用于SQLiteConnection使用的连接字符串。connection_string是用于connectionForURI使用的URI格式字符串。
之后定义的Root类是用于CherryPy显示的类,主要就是暴露(expose)主页,并接受pid参数,并返回pid对应的数据库索引的记录。Person是SQLObject的数据库对象。用于定义表格和这个表格的相关操作。注意这里使用的UnicodeCol()类,想要存储非ASCII字符必须使用这个类,且输入之前必须转换成Unicode类对象,否则还是出错。(by gashero)实践证明狭隘的西方人在StringCol()类中只能存储ASCII字符。这里,这个表格对应的数据库连接"_connection"暂时设为空,因为后面还要通过不同的方式来设置。
函数initial()初始化了数据库,建立了表格,并输入了一些测试数据,其中还还有中文。
函数test1()使用了常规的方式来连接数据库,并且可以看到在CherryPy接受请求时出错。ProgrammingError,大体意思是说,数据库连接并非是在这个线程中定义的。这就是非线程安全的数据库的聪明方式。在对不支持的特性上给抛出个异常,而没有默默的去出错。
函数test2()定义了在CherryPy线程池中的线程启动时调用一个 回调函数set_conn_hub()来设置线程相关的数据库连接。这个数据库连接表面上存储在了connection_hub.threadConnection中,实际上是线程相关的。在test2()中为了初始化数据库,还是需要调用一次initial(),而这时还没有任何初始化过的线程,所以只好自己在主线程中初始化了一个数据库连接。
test2()函数的设置最终解决了CherryPy并发条件下的SQLObject方法SQLite数据库问题。大家可以参考这个例子来细化自己的程序。