当前位置: 首页 > 科技观察

高并发服务优化:浅谈数据库连接池

时间:2023-03-19 11:50:25 科技观察

本文转载自微信公众号《码农的技术之路》,作者码农的技术之路。转载本文请联系Coder技术之路公众号。N大学转载的一篇博客引起了我的注意。讲了数据库连接池使用threadlocal的原因。文章中的结论如下图所示。threadlocal的作用和工作原理先不说,只说数据库连接池的知识点,一看就懂;仔细一看,感觉不太对,同学,这是什么词。$实践是检验真理的唯一标准。个人理解。连接池提供的获取连接的能力需要是“任务”唯一的,即只有当一个线程完成数据操作,将连接放回连接池时,其他线程才能再次获取和使用.至于原因,后面会详细说,先自己测试一下。连接池选择一个druid,连接池中只设置一个连接,方便验证多线程处理同一个连接的场景。首先将datasource共享资源传入线程,使用datasource.getConnection()方法获取连接:注意:connection.close的结果故意不在上图的Runnable中执行:只有一个线程可以正常执行,而且因为没有关闭,所以其他线程可以拿到Connectionfailed。说明数据库连接池的作用是对某个线程任务“独占”的。$退一步假设,如开篇所述,使用一个功能不完整的连接池,让多个线程获得同一个连接。那么,threadlocal真的能起到互不影响的作用吗?//验证思路参考:https://blog.csdn.net/sunbo94/article/details/79409298//连接设置autoCommit=falseprivatestaticfinalThreadLocalconnectionThreadLocal=newThreadLocal<>();privatestaticclassInnerRunnerimplementsRunnable{@Overridepublicvoidrun(){//其他代码省略...StringinsertSql="insertintouser(id,name)value("+RunnerIndex+","+RunnerIndex+")";statement=connectionThreadLocal.get().createStatement();statement.executeUpdate(insertSql);System.out.println(RunnerIndex+"isrunning");//让特定线程执行回滚以验证事务间的影响if(RunnerIndex==3){//增加模拟异常的时间Thread.sleep(100);//GettheconnectionobjectfromthreadlocalconnectionThreadLocal.get().rollback();System.out.println("3rollback");}else{//GettheconnectionobjectfromthreadlocalconnectionThreadLocal.get().commit();System.out.println(RunnerIndex+"commit");}}}结果如下:只要线程3的statement.executeUpdate语句先运行,事务回滚语句在一个c中执行ommit之后会出现一个问题,就是如下图提交了需要回滚的数据。3的插入结果确实没有回滚,而是出现在表中:所以,对于知识,不能盲目接受。有必要提出一些怀疑。$话说回来,为什么threadlocal对同一个数据库连接不起作用?什么是连接?Connection可以看作是服务器和数据库之间的一次会话,Statemant用于在会话的上下文中执行SQL并返回结果。一个连接可以包含多个语句;但是,在两者之间,有一个事务(Translation)的概念,用来保证内部的语句要么执行,要么不执行。如果启用了autoCommit,则默认为一个语句,一个事务。简单来说,connection就是一个共享资源,更简单一点,就是一个共享变量。被连接池创建后,内存中的地址是唯一的变量。ThreadLocal可以存储共享变量吗?肯定是可以存的,但是不推荐,因为把Connection设置到ThreadLocalMap中其实只是存了一个内存对象的地址引用。当它被实际使用时,它是唯一有效的对象。ThreadLocal最常用的功能是提供对象保存和获取的方法,以避免层层传递。我在高中学数学的时候,用了一个技巧叫proofisdifficult,这里也适用。我们反过来想,如果可以利用threadlocal的copy副本来实现连接隔离,那不是只有一个连接就够了吗?实时的时候,经常会出现数据库连接不足的情况,结论很明显~$那又回来了,threadLocal想要完成数据库连接隔离的功能,需要做什么呢?如果非要用ThreadLocal来实现这个连接隔离功能,那么只能为每个线程新建一个连接保存在Threadlocal中,这样,每个线程只会在自己的生命周期内使用这个连接,线程隔离可以实现了。$那么话说回来,druid、zadl等数据库连接池是怎么管理连接的呢?最大连接数为1的druid连接池原理概述:druid维护一个数组存储连接,维护多个变量检测连接池的状态,其中poolingCount用于表示连接数游泳池。当有线程获取连接时,需要先加锁,数量减一。当获取连接时发现数字为0,则返回空。当连接关闭时,连接资源会被放回数组中,数量会加一。*以上只是druid连接池的流程描述的简化版。其实还有连接池空等待、满通知、活跃数、异常数等复杂的判断。*有兴趣的同学可以看看源码。zdal的连接池管理列表源码:publicclassInternalManagedConnectionPool{//最大连接数privatefinalintmaxSize;//用于存储连接的链表privatefinalArrayListconnectionListeners;//内部信号量用于控制允许的线程总数acquireresourcesprivatefinalInternalSemaphorepermits;//正在使用的连接数privatevolatileintmaxUsedConnections=0;protectedInternalManagedConnectionPool(...){//在构造函数中,初始化连接池大小和信号量大小connectionListeners=newArrayList(this.maxSize);permits=newInternalSemaphore(this.maxSize);}getConnection()方法://获取连接publicConnectionListenergetConnection(){//信号量尝试获取权限if(permits.tryAcquire(poolParams.blockingTimeout,TimeUnit.MILLISECONDS)){ConnectionListenercl=null;do{//锁资源池synchronized(connectionListeners){if(connectionListeners.size()>0){//获取list的最后一个cl=(ConnectionListener)connectionListeners.remove(connectionListeners.size()-1);//最大连接数减去工作信号量intsize=(maxSize-permits.availablePermits());if(size>maxUsedConnections){maxUsedConnections=size;}}}if(cl!=null){returncl;}}while(connectionListeners.size()>0);//确定,在连接池中找不到工作连接。然后createNewConnection(){...}}else{if(this.maxSize==this.maxUsedConnections){thrownewResourceException("数据源的最大连接数已满,并且在超时时间,poolName="+poolName+"blockingtimeout="+poolParams.blockingTimeout+"(ms)");}}这里简化了内部连接池管理类的关键属性和连接获取方法的流程,而connectionreturn是不会做的,类似的,仔细看,我们看到什么volatile标记的maxUsedConnections是用来完成线程间数据的。可以看出属于AQS系列的Semaphone是用来控制共享资源的并发访问的。$那么话说回来,在druid和zdal中,threadlocal的作用在哪里呢?我们知道,像druid、zdal这样优秀的中间件,并不局限于数据库连接池的作用。阿里巴巴数据库中间件在zdal源码分析一文中也有提到。那么,ThreadLocal在这里能起到什么作用呢?以zdal为例,因为阿里的数据库基本上都很大,但是有一套完整的数据库分表规范。因此,分库键、分表键、Primary键、虚表名等在设计和存储时需要遵循规范,zdal中的解析操作也需要与之匹配。这个解析工作比较复杂和繁重。但是对于同一个用户的操作,库表的路由通常是比较固定的。所以我们在一次解析SQL的时候,通过各个字段和配置规则来计算出库表的路由,那么就可以直接放到线程上下文中,供本次请求后续的数据库操作使用。publicObjectparse(...){SimpleConditionsimpleCondition=newSimpleCondition();simpleCondition.setVirtualTableName("user");simpleCondition.put("age",10);ThreadLocalMap.put(ThreadLocalString.ROUTE_CONDITION,simpleCondition);}publicvoidfollow-upoperation(){RouteConditionrc=(RouteCondition)ThreadLocalMap.get(ThreadLocalString.ROUTE_CONDITION);if(rc!=null){//不解析SQL,ThreadLocal传入的指定对象(RouteCondition)决定库表的去向元数据=sqlDispatcher。getDBAndTables(rc);}else{//通过解析SQL来分库分表也恰好是对之前正确使用ThreadLocal的一个补充。原因是我对一篇文章的描述有疑问。通过简单的验证,确认了自己的想法,然后从几个方面展开数据库连接和threadlocal。如果您发现以上问题,请留言帮助指正。补充。