本文试图对数据库异步操作进行一个入门式的介绍,并且对在应用中常遇到的简单的问题给出方案建议.
文档结构:
1.数据库的异步和同步
通常我们使用DB的方式都是同步的:连接上DB后,我们发起一个sql操作请求,等待操作结束和结果返回后,继续下一步操作。这个等待的过程会导致:
1.之后的操作在结果返回前不会被执行;
2.等待导致被动阻塞,被动阻塞甚至不会释放相关资源,从而进一步造成资源闲置。因此,同步方式的缺点在于并发能力随着sql操作请求耗时的增加而急剧降低,尤其当某一步sql操作耗时较大时
我们也可以选择使用异步的方式来操作DB:连接上DB后,我们发起一个sql操作请求,同时定义一个该sql操作完成后被触发执行的方法,然后程序不进行任何等待,直接做下一步的操作。真正的sql操作在另一个时刻被单独执行,完成后调用我们定义好的方法。由于DB操作通常是最耗费时间的,因此在异步操作中,常常是程序的主流程很快的被执行完成,而大部分的DB操作甚至还没有开始,他们会在随后开始运行,但此时已不再有被堵塞的线程了——就像一个蓄水池,把过量的河水拦住,避免洪水泛滥。
异步操作常常依赖于回调的方式,从而把原本顺序的连贯的逻辑拆开来放在不同的地方,复杂情况下就有点四分五裂的感觉了.考虑到容错和异常处理的话,在代码组织和维护上就需要大大的费神了。对于异步操作的简化需要语言级别的支持,而对于常见的js和java而言,缺乏更加强大的机制,因此回调是唯一选择
当然,对于DB本身而言,都是同步的。不存在一个叫‘异步数据库’的产品。
2.WebSQL
在.net平台上,提供了这种异步操作DB的方式,并且支持的相当优雅。而Java用户可以参考ADBCJ。本文则要谈的是另一个平台:浏览器上的类似产品,相对前者而言有些简陋的WebSQL。
WebSQL包括一个sqlite3数据库和一个同时支持同步和异步方式的API,支持这个WebSQL的浏览器有chrome,safari和opera。原本WebSQL是被当做HTML5的一部分被推广的,但是最近'有关部门'停止了维护,在将来会由‘indexedDB’所取代,同样的,‘indexedDB’也支持异步的方式。
WebSQL主要用于在客户端存储一些较为复杂的关系型数据,这些数据需要能够按照多种条件组合被查询出来;如果仅仅是通过key/id来查询数据的,可以适用简单的支持更广泛的localStorage。
对于支持WebSQL的浏览器,可以通过db = openDatabase(‘WebDB’, '1.0', 'db on client side', 1 * 1024 * 1024) 获取数据库的连接,如果指定的数据库不存在,在浏览器端创建一个名为WebDB,版本为1.0, 大小1M的数据库。
----WebSQL的基本使用
上文中,获取到的db是一个js对象,里面封装了异步使用数据库的方法。
假设DB中有表blogs{id, title, gmtCreate}, 页面依赖了jQuery
var lastId = 10; db.transaction() { function(t) { t.executeSql('select * from blogs where id > ? limit 5 order by id asc', [lastId], function(t, result){ $('.blogs').append('' + result.title + ''); }) } }
这一段代码做的事情是:把id>lastId的5条记录按照先后顺序展示在blogs列表中。
这段代码背后的执行过程是这样的:
1. executeSql执行的时候,仅仅是把要执行的sql语句和对应的回调方法推送到一个全局队列S中,然后马上返回
2. 接下来的某个时候,全局队列S中的sql语句按序被调用
3. 当sql成功返回时,对应的回调方法(连同sql执行结果)会被推送到另一个全局队列C中,执行sql的程序马上返回
4. 接下来的某个时候,全局队列C中的回调方法按序被调用,从而完成整个执行过程
在这里可以找到关于WebSQL的详细规范和API文档
3.异步下的一些状况
3.1 数据过时问题
在js中,定义的回调函数是可以使用定义外的一些数据的,假设全局有个counter的变量,负责sql执行次数的统计,我们需要在sql执行完后把’自己是第几个执行的sql‘打印出来。在同步方式下,我们会把这个counter++放在sql执行的语句之后,然后输出counter;但是在异步方式下,我们必须把这个步骤放进回调函数中,而不是t.executeSQL()之后,因为sql真正的执行常常是在这个函数调用完成之后做的,这意味着,当回调函数开始执行的时候,counter的值已经被累加过了,如果executeSQL()多调用了几次,那么counter的数值就会被多次累加并且所有的回调函数中输出的counter都是一个值
这也要求我们,对于引用外部的值需要保持刻意的谨慎,一方面,它随时可能被改动;另一方面,对它的改动很可能不是按照你预期的方式进行的
最理想的方式所有需要用的数据都在’推送回调函数至全局队列C‘这个步骤时都一起传给回调函数,保证数据之间的隔离。但是对于WebSQL,我们看到它的API并不支持这么自由的应用方式,并且它现在已经停止维护了。
所以退而求其次的,我们尽量将会造成影响的改动推迟到回调函数中去完成。
3.2 最后的输出问题
在饭店,无论多么的和服务员异步,她最后总得说一句:“菜已经齐了,先生”,这句话就是’最后的输出‘。操作数据库的时候,很少是一个表就能完成的,碰到需要同时去多个表拿数据组装的场合,我们就得知道那个所有数据都查询到并组装好的’菜齐了的‘时刻。
同步场合下,这根本不是个问题,大不了就一直死等下去,但只要返回了,数据就一定有了。
异步场合下,这几个sql各干各的,回调函数彼此平行,互不了解,就不知道是不是全部数据都有了。为了解决这个问题,通常需要引入一个对所有相关回调函数都知道并可以操作的变量,从而对全局有整体的了解。
这个变量可以是,比如状态表,当状态表中的所有状态都是’OVER‘的时候,我们就知道全部’OVER‘了;又比如计数器,要求计数到指定阀值。根据“数据过时问题”中的描述,对这个变量的更改操作需要放进回调函数中去。
参考Java中的Barrier,其原理是,要求指定数量的线程完成某个操作后,程序才继续往下走, 我们可以设置一个Barrier形式的变量b,在executeSQL()调用前b.counter+1,在对应的回调函数执行后,b.counter-1,当b.counter再度回到0的时候,表示’全部执行完毕‘;如果预先知道sql执行的次数,则设置b.counter,并且省略'调用前b.counter+1'的步骤。这种方式主要解决,状态表复杂或者计数阀值预先不可知的情况
在异步场合下,感知’菜齐了‘的方式当然也是异步的,在变量中封装几个方法吧。
3.3 应用设计
无论是同步还是异步,都只有针对应用场景作最优设计才能最大限度的发掘系统潜力,神奇的应用设计一样具有把异步处理’化神奇为腐朽‘的能力。
比如,我们知道DB操作时非常消耗时间的,如果你把一次调用做成调用三次,那么你就过度给力了。在WebSQL中,由于executeSQL()没有提供对where id in(某个数组)操作的原生支持,需要我们自己去做些额外的操作,像自己拼装sql或者根据该数组大小来拼装带对应数目的’?‘的sql。如果不注意直接在数组循环里逐个调用sql,耗时就会成倍增加。
又比如,上文所讲,不可能全部异步,总会有一些环节是要严格按照先后顺序的,比如这么一个场景:user(id, name) box(id, uid), thing(id, bid, what),必须要知道user,才可以查到box,继而才能查到thing,响应时间是比较长的,因为查到user后,查询box的sql会推入到全局队列等待调度,而在同步场合下是没有这个等待时间的,它会被立刻执行。这意味着这种环节数越多,一个请求的响应时间就越长,因此必须减少环节数,增加单个环节并行执行的sql数。对于例子中的解决办法,如果这种获取是常用的,则可能的方式是在thing中冗余一个uid,从而在user获取到后,并发执行两个查询:box和thing。