隨著業(yè)務(wù)的發(fā)展,口袋數(shù)據(jù)量越來越大,訪問量也在持續(xù)上升,數(shù)據(jù)庫的壓力也變大。
經(jīng)過分析,口袋屬于讀多寫少的業(yè)務(wù),數(shù)據(jù)庫層面,之前已經(jīng)存在一主一從,但讀寫都是走的主庫,沒有真正運用起來,所以考慮對數(shù)據(jù)庫進行讀寫分離。在之后如果數(shù)據(jù)量增大到瓶頸時,會繼續(xù)進行分庫分表的優(yōu)化。
讀寫分離
MySQL讀寫分離基本原理是讓master數(shù)據(jù)庫處理寫操作,slave數(shù)據(jù)庫處理讀操作。master將寫操作的變更同步到各個slave節(jié)點。
MySQL讀寫分離能提高系統(tǒng)性能的原因在于:
- 主從只負責各自的讀和寫,極大程度緩解X鎖和S鎖爭用。
- slave可以配置MyIASM引擎,提升讀性能以及節(jié)約系統(tǒng)開銷。
- master直接寫是并發(fā)的,slave通過主庫發(fā)送來的binlog恢復數(shù)據(jù)是異步。
- slave可以單獨設(shè)置一些參數(shù)來提升其讀的性能。
實現(xiàn)方法
1. MySQLProxy
MySQLProxy是在客戶端請求與MySQLServer之間建立了一個連接池。所有客戶端請求都是發(fā)向MySQLProxy,然后經(jīng)由MySQLProxy進行相應(yīng)的分析,判斷出是讀操作還是寫操作,分發(fā)至對應(yīng)的MySQLServer上。對于多節(jié)點Slave集群,也可以起做到負載均衡的效果。
1.1 存在的問題
當一個事務(wù)中先執(zhí)行update,后執(zhí)行select時,MySQLProxy 存在一個問題,由于它只是簡單的將update打到master,select打到slave,由于mysql 主從復制是異步的,存在一定的延時,所以select 可能讀取不到剛更新的數(shù)據(jù)。
2. Sharding JDBC
sharding jdbc官方文檔
Sharding-JDBC是當當開源的一款分庫分表&讀寫分離框架。經(jīng)過評估后,決定使用該框架。
選擇原因:
- 測試覆蓋率達到95%
- 代碼整體框架比較清晰,方便閱讀及二次開發(fā)
- 社區(qū)活躍度較高,且持續(xù)維護
- 支持JPA、Hibernate、Mybatis、Spring JDBC Template或直接使用JDBC
- 可基于任何第三方的數(shù)據(jù)庫連接池,如DBCP、C3P0、 BoneCP、Druid等
2.1 遇到的問題
上周在開發(fā)過程中遇到一個問題。當在一個spring Transactional中,先執(zhí)行select操作,后執(zhí)行update操作時,報以下異常:
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: UPDATE command denied to user 'read'@'192.168.168.1' for table 'Book'
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:404)
at com.mysql.jdbc.Util.getInstance(Util.java:387)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:939)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3878)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3814)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2478)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2625)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2551)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1861)
at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1192)
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:2931)
at com.alibaba.druid.wall.WallFilter.preparedStatement_execute(WallFilter.java:588)
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:2929)
at com.alibaba.druid.filter.FilterEventAdapter.preparedStatement_execute(FilterEventAdapter.java:440)
at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:2929)
at com.alibaba.druid.proxy.jdbc.PreparedStatementProxyImpl.execute(PreparedStatementProxyImpl.java:118)
at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:493)
at com.dangdang.ddframe.rdb.sharding.executor.PreparedStatementExecutor.executeInternal(PreparedStatementExecutor.java:183)
2.2 報錯原因:
- 首先執(zhí)行select語句,sharding JDBC判斷該語句打到slave數(shù)據(jù)庫上,獲取slave的連接并放到Transaction中
- 其次執(zhí)行update語句,因為Transaction中已經(jīng)存在slave的連接,故直接使用該連接進行update
- slave配置的用戶只能對數(shù)據(jù)庫進行讀操作,故爆出異常
2.3 解決方案
為了避免update 使用slave導致報錯,故強制select & update都適用master,方法如下:
HintManager hintManager = HintManager.getInstance();
hintManager.setMasterRouteOnly();
該方法會強制事務(wù)中的所有數(shù)據(jù)庫操作使用master。