1.分库:
是将在一个用户下的表分到多个用户下(物理上可能是同一个库,也可以是不同的库,用户下的表不重复)。例如:把库分为配置库和实例库。或者把库拆为:产品库 订单库等。
2.分表:
在表的后面添加分表标志。例如A_110 , A_111 ,A_112等
3.分中心:
将一套表部署到多个库中。(物理上可能是同一个库,也可以是不同的库,用户下的表重复)
1.在分库的条件下,要保持事务一致性。要么所有用户全部提交,要么所有用户全部回滚。
2.在保存数据时,要确定插入的数据要在哪个分表,哪个中心。
(1)分库事务一致性的解决:
正常情况下,事务的封装,都是定义一个事务对象,事务对象中有一个数据库连接。
分库的条件下,可以在事务对象中维护一个容器,存放多个数据库连接。要么一起提交,要么一起回滚。保证事务的一致性。
事物封装的简单实现,在分库的情况下,保证事物的一致性。
1.事物对象的封装
public class BasicTransAction extends AbstractTransaction { private Log log = LogFactory.getLog(BasicTransAction.class); private static ThreadLocal<Stack<ThreadInfo>> suspend = new ThreadLocal<Stack<ThreadInfo>>(); private static ThreadLocal<ThreadInfo> curThread = new ThreadLocal<ThreadInfo>(); private class ThreadInfo{ public DataSource curDs; public Map<String,Connection> connection = new HashMap<String,Connection>(); public String threadName; public String txid; public ThreadInfo(){ this.txid=Thread.currentThread().getId()+""; this.threadName=Thread.currentThread().getName()+System.currentTimeMillis(); } }
封装一个内部的事物对象,注意事务对象内部 连接是通过 HashMap存储,这样事务对象可以保存多个数据源的连接Connection。
事物对象信息可以根据自己的需要进行封装。例如:可以增加事物的开始时间,事物的超时时间。这样可以控制事物超时的时候,直接抛出异常。
2.启动事物
private static ThreadLocal<Stack<ThreadInfo>> suspend = new ThreadLocal<Stack<ThreadInfo>>(); private static ThreadLocal<ThreadInfo> curThread = new ThreadLocal<ThreadInfo>();
public void startTransaction()throws Exception{ if(isStartTransaction()){ throw new Exception("已经启动事务,若想再启事务,请先挂起当前事务"); } ThreadInfo thread =new ThreadInfo(); System.out.println("开启事务"+thread.threadName); curThread.set(thread); }
创建事物对象,将其放入到threadlocal中。
3.获取数据库连接
public Connection _getConnection(String ds)throws Exception{ if(isStartTransaction()){//如果启动了事物 ThreadInfo thread = curThread.get(); if(thread.connection.containsKey(ds)){//校验事物对象中是否已经有该数据源的连接,有连接直接返回,没有连接从数据源中获取连接 thread.curDs=DataSourceManagerFactory.getDatasourceManager().getDataSource(ds); return thread.connection.get(ds);//将连接放入当前事务 } Connection conn = DataSourceManagerFactory.getDatasourceManager().getConnection(ds); thread.connection.put(ds, conn); thread.curDs=DataSourceManagerFactory.getDatasourceManager().getDataSource(ds); conn.setAutoCommit(false); return conn; }else{ return DataSourceManagerFactory.getDatasourceManager().getConnection(ds); } }
4.校验是否开启事物
public boolean isStartTransaction()throws Exception{ return curThread.get()!=null; }
5.挂起事物
private static ThreadLocal<Stack<ThreadInfo>> suspend = new ThreadLocal<Stack<ThreadInfo>>();
private void addSuspend(ThreadInfo thread)throws Exception{ if(suspend.get()==null){ suspend.set(new Stack<ThreadInfo>()); } suspend.get().add(thread); } public void suspendTransaction()throws Exception{ if(!isStartTransaction()){ throw new Exception("没有当前事务,无法挂起"); } addSuspend(curThread.get());//将当前事务放到suspend curThread.remove();//清空当前事务对象 curThread.set(null); }
将当前的事物对象从curThread中移除放到suspend(ThreadLocal)中,suspend中存放的是栈:后进先出。
6.事物恢复
public void resume()throws Exception{ if(isStartTransaction()){ log.error("当前事务未作处理,事物信息会丢失,且连接泄漏"); } Stack<ThreadInfo> stack= suspend.get(); if(stack == null || stack.size() <=0){ log.error("无挂起事物,无法恢复"); } curThread.set(null); curThread.set(stack.pop()); }
将suspend中最后进入的事物取出,放到当前事务中。
7.事物提交
public void commitTransAction()throws Exception{ if(!isStartTransaction()){ throw new Exception("没有当前事务,无法提交"); } ThreadInfo thread = curThread.get(); System.out.println("提交事务"+thread.threadName); try { Connection conn=null; boolean isCommit=true; for(Iterator it = thread.connection.keySet().iterator();it.hasNext();){ try { conn=thread.connection.get(it.next()); commit(conn, isCommit); } catch (Exception e) { if(log.isErrorEnabled()) log.error(e.getMessage(), e); isCommit=false; commit(conn, isCommit); }finally{ if(!conn.isClosed()) conn.close(); } } } catch (Exception e) { if(log.isErrorEnabled()) log.error(e.getMessage(), e); }finally{ clearThreadInfo();//清楚当前的事物信息 } }
private void commit(Connection conn,boolean isCommit)throws Exception{ if(isCommit){ conn.commit(); }else{ conn.rollback(); } }
获取当前事务的所有连接,循环提交事务。这样多个数据源的连接就同时提交了。这里有一个问题:就是如果一个连接提交成功,然后后面的连接提交失败,未提交的连接会回滚,已提交的连接无法回滚,但是,如果sql有问题,一般都是在执行的时候报出错误信息,连接提交的时候很少会出错。
8.事物回滚
public void rollBackTransaction(){ try { if(!isStartTransaction()){ throw new Exception("没有当前事务,无法回滚"); } ThreadInfo thread = curThread.get(); Connection conn=null; boolean isCommit=false; for(Iterator it = thread.connection.keySet().iterator();it.hasNext();){ try { conn=thread.connection.get(it.next()); commit(conn, isCommit); } catch (Exception e) { if(log.isErrorEnabled()) log.error(e.getMessage(), e); }finally{ if(!conn.isClosed()) conn.close(); } } } catch (Exception e) { if(log.isErrorEnabled()) log.error(e.getMessage(), e); }finally{ clearThreadInfo(); } }
获取当前事务的所有连接,循环回滚事务
9.使用方式
boolean isStartTransaction=false; try { isStartTransaction = SessionFactory.getSession().isStartTransaction();//判断当前是否已经启用事物 if(isStartTransaction){ SessionFactory.getSession().suspendTransaction();//如果启动,挂起当前事务 } SessionFactory.getSession().startTransaction();//开启新事物 SessionFactory.getSession().commitTransAction();//事物提交 } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); SessionFactory.getSession().rollBackTransaction();//事物回滚 }finally{ if(isStartTransaction) SessionFactory.getSession().resume();//如果前面挂起事物,进行实物恢复 }
以上是一个在一个线程中多数据源的事物封装的简单实现,基本上能够保证多数据源的书屋一致性。由于时间比较紧,没有做过多的优化,主要是事物封装思想。若要使用,还需要添加以下内容。
1.方法和数据源的映射关系。在多数据源的情况下,该方法应该使用哪个数据源。这里可以采用注解配置,或者在分包明确的情况下,直接采用 类路径和数据源的映射。
(2)分表 分中心的确认
根据某一个特性,采用适合的映射算法,映射到不同的分表, 例如取余等。然后维护一个分表和分中心的关系。
整合spring的思想。
1.重写spring的事务对象。
2.重写事务管理器。
3.修改mybatis的sql执行类。在执行sql前,把sql语句的需要分表的表计算出对应的分表。
4.标志当前表在哪个用户下。可以给mapper文件添加标签,或者通过路径进行匹配对应的用户(数据源)