随着业务的发展,口袋数据量越来越大,访问量也在持续上升,数据库的压力也变大。
经过分析,口袋属于读多写少的业务,数据库层面,之前已经存在一主一从,但读写都是走的主库,没有真正运用起来,所以考虑对数据库进行读写分离。在之后如果数据量增大到瓶颈时,会继续进行分库分表的优化。
读写分离
MySQL读写分离基本原理是让master数据库处理写操作,slave数据库处理读操作。master将写操作的变更同步到各个slave节点。
MySQL读写分离能提高系统性能的原因在于:
- 主从只负责各自的读和写,极大程度缓解X锁和S锁争用。
- slave可以配置MyIASM引擎,提升读性能以及节约系统开销。
- master直接写是并发的,slave通过主库发送来的binlog恢复数据是异步。
- slave可以单独设置一些参数来提升其读的性能。
实现方法
1. MySQLProxy
MySQLProxy是在客户端请求与MySQLServer之间建立了一个连接池。所有客户端请求都是发向MySQLProxy,然后经由MySQLProxy进行相应的分析,判断出是读操作还是写操作,分发至对应的MySQLServer上。对于多节点Slave集群,也可以起做到负载均衡的效果。
1.1 存在的问题
当一个事务中先执行update,后执行select时,MySQLProxy 存在一个问题,由于它只是简单的将update打到master,select打到slave,由于mysql 主从复制是异步的,存在一定的延时,所以select 可能读取不到刚更新的数据。
2. Sharding JDBC
sharding jdbc官方文档
Sharding-JDBC是当当开源的一款分库分表&读写分离框架。经过评估后,决定使用该框架。
选择原因:
- 测试覆盖率达到95%
- 代码整体框架比较清晰,方便阅读及二次开发
- 社区活跃度较高,且持续维护
- 支持JPA、Hibernate、Mybatis、Spring JDBC Template或直接使用JDBC
- 可基于任何第三方的数据库连接池,如DBCP、C3P0、 BoneCP、Druid等
2.1 遇到的问题
上周在开发过程中遇到一个问题。当在一个spring Transactional中,先执行select操作,后执行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 报错原因:
- 首先执行select语句,sharding JDBC判断该语句打到slave数据库上,获取slave的连接并放到Transaction中
- 其次执行update语句,因为Transaction中已经存在slave的连接,故直接使用该连接进行update
- slave配置的用户只能对数据库进行读操作,故爆出异常
2.3 解决方案
为了避免update 使用slave导致报错,故强制select & update都适用master,方法如下:
HintManager hintManager = HintManager.getInstance();
hintManager.setMasterRouteOnly();
该方法会强制事务中的所有数据库操作使用master。