Ⅰ spring事务中查询主库还是从库
可以给所有读的方法添加一个参数,来控制读从库还是主库。
2、数据源如何路由?
spring-jdbc 包中提供了一个抽象类:AbstractRoutingDataSource,实现了javax.sql.DataSource接口,我们用这个类来作为胡拦数据源类,重点是这个类可以用来做数据源的路由,可以在其内部配置多个真实的数据源,最终用哪个数据源,由开发者来决定。
AbstractRoutingDataSource中有个map,用来存储多个目标数据源
private Map<Object, DataSource> resolvedDataSources;
比如主从库可以这么存储
resolvedDataSources.put("master",主库数据源);
resolvedDataSources.put("salave",从库数据源);
AbstractRoutingDataSource中还有抽象方法determineCurrentLookupKey,将这个方法的返回值作为key到上面的resolvedDataSources中查找对应的数据源,作为当前操作db的数据源
protected abstract Object determineCurrentLookupKey();
3、读写分离在哪控制?
读写分离属于一个通用的功能,可以通过spring的aop来实现,添加一个拦截器,拦截目困茄标方法的之前,在目标方法执行之前,获取一下当前需要走哪个库,将这个标志存储在ThreadLocal中,将这个标志作为AbstractRoutingDataSource.determineCurrentLookupKey()方法的返回值,拦截器中在目标方法执行完毕之后,将这个标志从ThreadLocal中清除。
3、代码实现
3.1、工程结构图
3.2、DsType
表示数据源类型,有2个值,用来区分是主库还是从库。裤尺胡
package com.javacode2018.readwritesplit.base;
public enum DsType {
MASTER, SLAVE;
}
3.3、DsTypeHolder
内部有个ThreadLocal,用来记录当前走主库还是从库,将这个标志放在dsTypeThreadLocal中
package com.javacode2018.readwritesplit.base;
public class DsTypeHolder {
private static ThreadLocal<DsType> dsTypeThreadLocal = new ThreadLocal<>();
public static void master() {
dsTypeThreadLocal.set(DsType.MASTER);
}
public static void slave() {
dsTypeThreadLocal.set(DsType.SLAVE);
}
public static DsType getDsType() {
return dsTypeThreadLocal.get();
}
public static void clearDsType() {
dsTypeThreadLocal.remove();
}
}
3.4、IService接口
这个接口起到标志的作用,当某个类需要启用读写分离的时候,需要实现这个接口,实现这个接口的类都会被读写分离拦截器拦截。
package com.javacode2018.readwritesplit.base;
//需要实现读写分离的service需要实现该接口
public interface IService {
}
3.5、ReadWriteDataSource
读写分离数据源,继承ReadWriteDataSource,注意其内部的determineCurrentLookupKey方法,从上面的ThreadLocal中获取当前需要走主库还是从库的标志。
package com.javacode2018.readwritesplit.base;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
public class ReadWriteDataSource extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return DsTypeHolder.getDsType();
}
}
3.6、ReadWriteInterceptor
读写分离拦截器,需放在事务拦截器前面执行,通过@1代码我们将此拦截器的顺序设置为Integer.MAX_VALUE - 2,稍后我们将事务拦截器的顺序设置为Integer.MAX_VALUE - 1,事务拦截器的执行顺序是从小到达的,所以,ReadWriteInterceptor会在事务拦截器org.springframework.transaction.interceptor.TransactionInterceptor之前执行。
由于业务方法中存在相互调用的情况,比如service1.m1中调用service2.m2,而service2.m2中调用了service2.m3,我们只需要在m1方法执行之前,获取具体要用哪个数据源就可以了,所以下面代码中会在第一次进入这个拦截器的时候,记录一下走主库还是从库。
下面方法中会获取当前目标方法的最后一个参数,最后一个参数可以是DsType类型的,开发者可以通过这个参数来控制具体走主库还是从库。
package com.javacode2018.readwritesplit.base;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Objects;
@Aspect
@Order(Integer.MAX_VALUE - 2) //@1
@Component
public class ReadWriteInterceptor {
@Pointcut("target(IService)")
public void pointcut() {
}
//获取当前目标方法的最后一个参数
private Object getLastArgs(final ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
if (Objects.nonNull(args) && args.length > 0) {
return args[args.length - 1];
} else {
return null;
}
}
@Around("pointcut()")
public Object around(final ProceedingJoinPoint pjp) throws Throwable {
//判断是否是第一次进来,用于处理事务嵌套
boolean isFirst = false;
try {
if (DsTypeHolder.getDsType() == null) {
isFirst = true;
}
if (isFirst) {
Object lastArgs = getLastArgs(pjp);
if (DsType.SLAVE.equals(lastArgs)) {
DsTypeHolder.slave();
} else {
DsTypeHolder.master();
}
}
return pjp.proceed();
} finally {
//退出的时候,清理
if (isFirst) {
DsTypeHolder.clearDsType();
}
}
}
}
3.7、ReadWriteConfiguration
spring配置类,作用
1、@3:用来将com.javacode2018.readwritesplit.base包中的一些类注册到spring容器中,比如上面的拦截器ReadWriteInterceptor
2、@1:开启spring aop的功能
3、@2:开启spring自动管理事务的功能,@EnableTransactionManagement的order用来指定事务拦截器org.springframework.transaction.interceptor.TransactionInterceptor顺序,在这里我们将order设置为Integer.MAX_VALUE - 1,而上面ReadWriteInterceptor的order是Integer.MAX_VALUE - 2,所以ReadWriteInterceptor会在事务拦截器之前执行。
package com.javacode2018.readwritesplit.base;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableAspectJAutoProxy //@1
@EnableTransactionManagement(proxyTargetClass = true, order = Integer.MAX_VALUE - 1) //@2
@ComponentScan(basePackageClasses = IService.class) //@3
public class ReadWriteConfiguration {
}
3.8、@EnableReadWrite
这个注解用来开启读写分离的功能,@1通过@Import将ReadWriteConfiguration导入到spring容器了,这样就会自动启用读写分离的功能。业务中需要使用读写分离,只需要在spring配置类中加上@EnableReadWrite注解就可以了。
package com.javacode2018.readwritesplit.base;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ReadWriteConfiguration.class) //@1
public @interface EnableReadWrite {
}
4、案例
读写分离的关键代码写完了,下面我们来上案例验证一下效果。
4.1、执行sql脚本
下面准备2个数据库:javacode2018_master(主库)、javacode2018_slave(从库)
2个库中都创建一个t_user表,分别插入了一条数据,稍后用这个数据来验证走的是主库还是从库。
DROP DATABASE IF EXISTS javacode2018_master;
CREATE DATABASE IF NOT EXISTS javacode2018_master;
USE javacode2018_master;
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(256) NOT NULL DEFAULT ''
COMMENT '姓名'
);
INSERT INTO t_user (name) VALUE ('master库');
DROP DATABASE IF EXISTS javacode2018_slave;
CREATE DATABASE IF NOT EXISTS javacode2018_slave;
USE javacode2018_slave;
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(256) NOT NULL DEFAULT ''
COMMENT '姓名'
);
INSERT INTO t_user (name) VALUE ('slave库');
4.2、spring配置类
@1:启用读写分离
masterDs()方法:定义主库数据源
slaveDs()方法:定义从库数据源
dataSource():定义读写分离路由数据源
后面还有2个方法用来定义JdbcTemplate和事务管理器,方法中都通过@Qualifier(“dataSource”)限定了注入的bean名称为dataSource:即注入了上面dataSource()返回的读写分离路由数据源。
package com.javacode2018.readwritesplit.demo1;
import com.javacode2018.readwritesplit.base.DsType;
import com.javacode2018.readwritesplit.base.EnableReadWrite;
import com.javacode2018.readwritesplit.base.ReadWriteDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@EnableReadWrite //@1
@Configuration
@ComponentScan
public class MainConfig {
//主库数据源
@Bean
public DataSource masterDs() {
org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018_master?characterEncoding=UTF-8");
dataSource.setUsername("root");
dataSource.setPassword("root123");
dataSource.setInitialSize(5);
return dataSource;
}
//从库数据源
@Bean
public DataSource slaveDs() {
org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018_slave?characterEncoding=UTF-8");
dataSource.setUsername("root");
dataSource.setPassword("root123");
dataSource.setInitialSize(5);
return dataSource;
}
//读写分离路由数据源
@Bean
public ReadWriteDataSource dataSource() {
ReadWriteDataSource dataSource = new ReadWriteDataSource();
//设置主库为默认的库,当路由的时候没有在datasource那个map中找到对应的数据源的时候,会使用这个默认的数据源
dataSource.setDefaultTargetDataSource(this.masterDs());
//设置多个目标库
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DsType.MASTER, this.masterDs());
targetDataSources.put(DsType.SLAVE, this.slaveDs());
dataSource.setTargetDataSources(targetDataSources);
return dataSource;
}
//JdbcTemplate,dataSource为上面定义的注入读写分离的数据源
@Bean
public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
//定义事务管理器,dataSource为上面定义的注入读写分离的数据源
@Bean
public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
4.3、UserService
这个类就相当于我们平时写的service,我是为了方法,直接在里面使用了JdbcTemplate来操作数据库,真实的项目操作db会放在里面。
getUserNameById方法:通过id查询name。
insert方法:插入数据,这个内部的所有操作都会走主库,为了验证是不是查询也会走主库,插入数据之后,我们会调用this.userService.getUserNameById(id, DsType.SLAVE)方法去执行查询操作,第二个参数故意使用SLAVE,如果查询有结果,说明走的是主库,否则走的是从库,这里为什么需要通过this.userService来调用getUserNameById?
this.userService最终是个代理对象,通过代理对象访问其内部的方法,才会被读写分离的拦截器拦截。
package com.javacode2018.readwritesplit.demo1;
import com.javacode2018.readwritesplit.base.DsType;
import com.javacode2018.readwritesplit.base.IService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Component
public class UserService implements IService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private UserService userService;
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public String getUserNameById(long id, DsType dsType) {
String sql = "select name from t_user where id=?";
List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id);
return (list != null && list.size() > 0) ? list.get(0) : null;
}
//这个insert方法会走主库,内部的所有操作都会走主库
@Transactional
public void insert(long id, String name) {
System.out.println(String.format("插入数据{id:%s, name:%s}", id, name));
this.jdbcTemplate.update("insert into t_user (id,name) values (?,?)", id, name);
String userName = this.userService.getUserNameById(id, DsType.SLAVE);
System.out.println("查询结果:" + userName);
}
}
4.4、测试用例
package com.javacode2018.readwritesplit.demo1;
import com.javacode2018.readwritesplit.base.DsType;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.;
public class Demo1Test {
UserService userService;
@Before
public void before() {
context = new ();
context.register(MainConfig.class);
context.refresh();
this.userService = context.getBean(UserService.class);
}
@Test
public void test1() {
System.out.println(this.userService.getUserNameById(1, DsType.MASTER));
System.out.println(this.userService.getUserNameById(1, DsType.SLAVE));
}
@Test
public void test2() {
long id = System.currentTimeMillis();
System.out.println(id);
this.userService.insert(id, "张三");
}
}
test1方法执行2次查询,分别查询主库和从库,输出:
master库
slave库
是不是很爽,由开发者自己控制具体走主库还是从库。
test2执行结果如下,可以看出查询到了刚刚插入的数据,说明insert中所有操作都走的是主库。
1604905117467
插入数据{id:1604905117467, name:张三}
查询结果:张三
5、案例源码
Ⅱ 【sharding-jdbc】spring boot 集成sharding-jdbc 完成一主多从读写分离
说明
sharding-jdbc 的官方文档在我看册备让来是比较绕的,尤其是配置文件,看着相当头大,滚盯仔细看绝对是看得懂的。上面配置一主州局两从的情况,此框架支持一主,这点是需要注意的。而且框架不复制主从数据的同步。
Ⅲ 数据库为什么要读写分离
数据库不一定要读写分离,如果程序使用数据库较多时,而更新少,查询多的情况下会考虑使用,利用数据库 主从同步 。可以减少数据库压力,提高性能。当然,数据库也有其它优化方案。memcache 或是 表折分,或是搜索引擎。都是解决方法。
Ⅳ 如何在spring + mybatis 下进行数据库读写分离
读写分离是为了减少数据库的负荷,当用户高并发访问时,绝大部分都是用户查询,少部分洞岩滚用户是写入到数据库的。这些我们把数据库拆分成主从两个纳余数据库,主数据库用高性能
服务器承载高并枣链发的用户访问并加redis缓存。在这里我不讲mysql的主从同步配置,大家可以去查下资料,我接下来重点讲怎么动态的给每个sql注入数据源。
Ⅳ spring怎么实现读写分离
配置两个连接池,一个读,一个写
写个Service层或者(DAO层也行)的aop,读操作使用读的连接,写用写的连接实现分离
希望可以帮到你
Ⅵ Java软件工程师主要学习哪些课程
第一阶段,Java SE基础:
Java环境搭建、Java流程控制语句-for循环、switch选择判断、循环嵌套、数组拷贝、多维数组、final关键字、构造函数的调用、类的访问权限和路径、面向对象高级特性、Java异常处理、Set,Map,List接口及接口实现类、Java线程、同步阻塞、JavaIO流、文件的操作,复制,读写,删除等。第二阶段,JavaWeb:MySQL安装、管理、创建数据库、MySQL
UPDATE 查询、Mysql高级操作、JDBC、JDBC数据库连接操作,JDBC动态Sql处理、Servlet3.0
网页重定向、Servlet3.0 新增的注解支持、AJAX、responseText属性详解等。第三阶段,Java高级框架-SSH:Struts2异常处理、Struts2+Log4j集成、Struts2和JSON实例、Hibernate5、Hibernate集合映射、Hibernate组件映射、Spring4.0、SpringAOP
+ AspectJ框架、Spring 与其它Web框架集成、Spring Hibernate支持等。第四阶段,Java高级框架-SSM:SpringMVC、Spring MVC生成JSON数据、MyBatis、MyBatis 环境配置及入门、Mybatis set标签、Mybatis trim标签、Shiro、Shiro快速入门教程、Shiro Web应用等。第五阶段,SpringBoot+VUE全栈框架:SpringBoot、全局异常处理、过滤器监听器、EHCache缓存、SpringBoot Quartz定时任务、Vue、Vue.js 安装、模板语法、计算属性、事件处理器、Vue.js 自定义指令、Vue.js 路由等第六阶段,特色课程:ActiveM环境搭建、生产者和消费者、消息持久化操作、RSA数字加密算法、Codebar条形码生成器、zxing二维码生成器、HighCharts统计图、Echarts统计图、网络播放器ckplayer、嵌入式网络播放器,可以浏览器和移动端随意使用第七阶段,互联网框架的高级应用1:分布式服务框架的理解,Dubbo架构设计详解及其核心要点,框架运行原理分析、SpringData数据访问、Lucene搜索引擎、Lucene的全文搜索服务器介绍、索引建立方式、Solr海量数据搜索引擎、Socket网络通信、实现RMI远程对象通讯、使用JMS消息服务、Kafka分布式消息系统、WebService与Restful
WS等第八阶段,互联网框架的高级应用2:Spring Security安全框架、实现Web应用安全控制、缓存应用与EhCache框架、OSCache与JBossCache框架、MyBatis与Hibernate缓存机制、NoSQL应用与SQL调优、MongoDB
NoSQL数据库、Redis内存数据库、实现Redis
Session共享、SQL语句的优化、实现数据库读写分离、WEB应用集群及性能优化、Maven项目管理工具、Web服务器负载均衡、实现Nginx与Tomcat集群、使用LoadRunner测试工具、性能优化之内存调优、代码优化与重构的方法等。
对java有兴趣的小伙伴们,不妨先从java入门开始!B站上有很多的java教学视频,从基础到高级的都有,还挺不错的,知识点讲的很细致,还有完整版的学习路线图。也可以自己去看看,下载学习试试。
Ⅶ 大型互联网架构概述,看完文章又涨知识了
1. 大型网站系统的特点
2. 大型网站架构演化历程
2.1. 初始阶段架构
问题:网站运营初期,访问用户少,一台服务器绰绰有余。
特征:应用程序、数据库、文件等所有的资源都在一台服务器上。
描述:通常服务器操作系统使用 linux,应用程序使用 PHP 开发,然后部署在 Apache 上,数据库使用 Mysql,通俗称为 LAMP。汇集各种免费开源软件以及一台廉价服务器就可以开始系统的发展之路了。
2.2. 应用服务和数据服务分离
问题:越来越多的用户访问导致性能越来越差,越来越多的数据导致存储空间不足,一台服务器已不足以支撑。
特征:应用服务器、数据库服务器、文件服务器分别独立部署。
描述:三台服务器对性能要求各不相同:应用服务器要处理大量业务逻辑,因此需要更快更强大的 CPU;数据库服务器需要快速磁盘检索和数据缓存,因此需要更快的硬盘和更大的内存;文件服务器需要存储大量文件,因此需要更大容量的硬盘。
2.3. 使用缓存改善性能
问题:随着用户逐渐增多,数据库压力太大导致访问延迟。
特征:由于网站访问和财富分配一样遵循二八定律:80% 的业务访问集中在 20% 的数据上。将数据库中访问较集中的少部分数据缓存在内存中,可以减少数据库的访问次数,降低数据库的访问压力。
描述:缓存分为两种:应用服务器上的本地缓存和分布式缓存服务器上的远程缓存,本地缓存访问速度更快,但缓存数据量有限,同时存在与应用程序争用内存的情况。分布式缓存可以采用集群方式,理论上可以做到不受内存容量限制的缓存服务。
2.4. 使用应用服务器集群
问题:使用缓存后,数据库访问压力得到有效缓解。但是单一应用服务器能够处理的请求连接有限,在访问高峰期,成为瓶颈。
特征:多台服务器通过负载均衡同时向外部提供服务,解决单一服务器处理能力和存储空间不足的问题。
描述:使用集群是系统解决高并发、海量数据问题的常用手段。通过向集群中追加资源,提升系统的并发处理能力,使得服务器的负载压力不再成为整个系统的瓶颈。
2.5. 数据库读写分离
问题:网站使用缓存后,使绝大部分数据读操作访问都可以不通过数据库就能完成,但是仍有一部分读操作和全部的写操作需要访问数据库,在网站的用户达到一定规模后,数据库因为负载压力过高而成为网站的瓶颈。
特征:目前大部分的主流数据库都提供主从热备功能,通过配置两台数据库主从关系,可以将一台数据库服务器的数据更新同步到一台服务器上。网站利用数据库的主从热备功能,实现数据库读写分离,从而改善数据库负载压力。
描述:应用服务器在写操作的时候,访问主数据库,主数据库通过主从复制机制将数据更新同步到从数据库。这样当应用服务器在读操作的时候,访问从数据库获得数据。为了便于应用程序访问读写分离后的数据库,通常在应用服务器端使用专门的数据访问模块,使数据库读写分离的对应用透明。
2.6. 反向代理和 CDN 加速
问题:中国网络环境复杂,不同地区的用户访问网站时,速度差别也极大。
特征:采用 CDN 和反向代理加快系统的静态资源访问速度。
描述:CDN 和反向代理的基本原理都是缓存,区别在于 CDN 部署在网络提供商的机房,使用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据;而反向代理则部署在网站的中心机房,当用户请求到达中心机房后,首先访问的服务器时反向代理服务器,如果反向代理服务器中缓存着用户请求的资源,就将其直接返回给用户。
2.7. 分布式文件系统和分布式数据库
问题:随着大型网站业务持续增长,数据库经过读写分离,从一台服务器拆分为两台服务器,依然不能满足需求。
特征:数据库采用分布式数据库,文件系统采用分布式文件系统。
描述:分布式数据库是数据库拆分的最后方法,只有在单表数据规模非常庞大的时候才使用。不到不得已时,更常用的数据库拆分手段是业务分库,将不同的业务数据库部署在不同的物理服务器上。
2.8. 使用 NoSQL 和搜索引擎
问题:随着网站业务越来越复杂,对数据存储和检索的需求也越来越复杂。
特征:系统引入 NoSQL 数据库及搜索引擎。
描述:NoSQL 数据库及搜索引擎对可伸缩的分布式特性具有更好的支持。应用服务器通过统一数据访问模块访问各种数据,减轻应用程序管理诸多数据源的麻烦。
2.9. 业务拆分
问题:大型网站的业务场景日益复杂,分为多个产品线。
特征:采用分而治之的手段将整个网站业务分成不同的产品线。系统上按照业务进行拆分改造,应用服务器按照业务区分进行分别部署。
描述:应用之间可以通过超链接建立关系,也可以通过消息队列进行数据分发,当然更多的还是通过访问同一个数据存储系统来构成一个关联的完整系统。
纵向拆分:将一个大应用拆分为多个小应用,如果新业务较为独立,那么就直接将其设计部署为一个独立的 Web 应用系统。纵向拆分相对较为简单,通过梳理业务,将较少相关的业务剥离即可。
横向拆分:将复用的业务拆分出来,独立部署为分布式服务,新增业务只需要调用这些分布式服务横向拆分需要识别可复用的业务,设计服务接口,规范服务依赖关系。
2.10. 分布式服务
问题:随着业务越拆越小,存储系统越来越庞大,应用系统整体复杂程度呈指数级上升,部署维护越来越困难。由于所有应用要和所有数据库系统连接,最终导致数据库连接资源不足,拒绝服务。
特征:公共业务提取出来,独立部署。由这些可复用的业务连接数据库,通过分布式服务提供共用业务服务。
3. 大型网站架构模式
3.1. 分层
大型网站架构中常采用分层结构,将软件系统分为应用层、服务层、数据层:
分层架构的约束:禁止跨层次的调用(应用层直接调用数据层)及逆向调用(数据层调用服务层,或者服务层调用应用层)。
分层结构内部还可以继续分层,如应用可以再细分为视图层和业务逻辑层;服务层也可以细分为数据接口层和逻辑处理层。
3.2. 分割
将不同的功能和服务分割开来,包装成高内聚低耦合的模块单元。这有助于软件的开发和维护,便于不同模块的分布式部署,提高网站的并发处理能力和功能扩展能力。
3.3. 分布式
大于大型网站,分层和分割的一个主要目的是为了切分后的模块便于分布式部署,即将不同模块部署在不同的服务器上,通过远程调用协同工作。
分布式意味可以用更多的机器工作,那么 CPU、内存、存储资源也就更丰富,能够处理的并发访问和数据量就越大,进而能够为更多的用户提供服务。
分布式也引入了一些问题:
常用的分布式方案:
3.4. 集群
集群即多台服务器部署相同应用构成一个集群,通过负载均衡设备共同对外提供服务。
集群需要具备伸缩性和故障转移机制:伸缩性是指可以根据用户访问量向集群添加或减少机器;故障转移是指,当某台机器出现故障时,负载均衡设备或失效转移机制将请求转发到集群中的其他机器上,从而不影响用户使用。
3.5. 缓存
缓存就是将数据存放在距离最近的位置以加快处理速度。缓存是改善软件性能的第一手段。
网站应用中,缓存除了可以加快数据访问速度以外,还可以减轻后端应用和数据存储的负载压力。
常见缓存手段:
使用缓存有两个前提:
3.6. 异步
软件发展的一个重要目标和驱动力是降低软件耦合性。事物之间直接关系越少,彼此影响就越小,也就更容易独立发展。
大型网站架构中,系统解耦的手段除了分层、分割、分布式等,还有一个重要手段——异步。
业务间的消息传递不是同步调用,而是将一个业务操作拆分成多阶段,每个阶段间通过共享数据的方式异步执行进行协作。
异步架构是典型的生产者消费模式,二者不存在直接调用。异步消息队列还有如下特性:
3.7. 冗余
大型网站,出现服务器宕机是必然事件。要保证部分服务器宕机的情况下网站依然可以继续服务,不丢失数据,就需要一定程度的服务器冗余运行,数据冗余备份。这样当某台服务器宕机是,可以将其上的服务和数据访问转移到其他机器上。
访问和负载很小的服务也必须部署 至少两台服务器构成一个集群,目的就是通过冗余实现服务高可用。数据除了定期备份,存档保存,实现 冷备份 外;为了保证在线业务高可用,还需要对数据库进行主从分离,实时同步实现 热备份。
为了抵御地震、海啸等不可抗因素导致的网站完全瘫痪,某些大型网站会对整个数据中心进行备份,全球范围内部署 灾备数据中心。网站程序和数据实时同步到多个灾备数据中心。
3.8. 自动化
大型网站架构的自动化架构设计主要集中在发布运维方面:
3.9. 安全
4. 大型网站核心架构要素
架构 的一种通俗说法是:最高层次的规划,难以改变的决定。
4.1. 性能
性能问题无处不在,所以网站性能优化手段也十分繁多:
4.2. 可用性
可用性指部分服务器出现故障时,还能否对用户提供服务
4.3. 伸缩性
衡量伸缩的标准就是是否可以用多台服务器构建集群,是否容易向集群中增删服务器节点。增删服务器节点后是否可以提供和之前无差别的服务。集群中可容纳的总服务器数是否有限制。
4.4. 扩展性
衡量扩展性的标准就是增加新的业务产品时,是否可以实现对现有产品透明无影响,不需要任何改动或很少改动,既有功能就可以上线新产品。主要手段有:事件驱动架构和分布式服务。
4.5. 安全性
安全性保护网站不受恶意攻击,保护网站重要数据不被窃取。
欢迎工作一到五年的Java工程师朋友们加入Java程序员开发: 721575865
群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!
Ⅷ 数据库读写分离如何保证主从一致性
当我们的数据库压力主键变大的时候,我们会尝试增加一些从节点来分摊主节点的查询压力。而一般来说,我们是用一主多从的结构来作为读写分离的基本结构。
而一般来说我们有两种常用的方法来实现读且分离架构:
客户端直接分离
这种方式是由客户端,或者我们的微服务直接进行数据库的读写选择。将读库选择路由到主库上进行,将查询路由到从主库上进行。
这种方式的优点在于因为是直连所以性能比较高,但是需要由业务团队了解数据库的实例细节,当数据库做调整的时候就需要业务侧同步改造。
使用数据中间件代理
这种方式是由一层代理层对数据的读写做分发,业务层将所有的请求都通过代理来实现。
这种方式的优点在于对于业务层不需要感知到数据库的存在,但问题在于数据中间件的性能要求较高,还需要专人来进行优化和维护,整体架构较为复杂。
但是我们发现,尽管这两种方式各有优劣。但核心都是通过数据的写入、查询请求的路由而实现的,那么这就会引发标题的问题:
主备同步存在延迟,所以在延迟时间内对插入的内容进行查询则无法查询到最新提交的事务。
那么如何保证主从一致性的问题,其实就变成了如何处理主从延迟的问题。
根据项目的大小,团队的规模以及主机的部署模式。我们处理问题的方法也有很多种。
最简单强硬的就是强制读主库。
一般情况下我们在不同的查询中会有不同程度的一致性要求。我们可以将需要保证数据一致性的请求配置强制查询主库,而对于无强依赖的查询请求仍然查询备库。
尽管这个方案不是很优雅,但是是最简单实现的方法,并且在Spring等框架的支持下一般只需要加一个注解就能实现。但这个方法的问题也是显而易见的,如果存在大量的强一致性要求的查询语句,则相当于没有进行读写分离与扩展。那么这种方法就会导致系统在数据库层面没有有效的扩展手段了。
由于问题产生的来源是主从延迟,所以在下一次查询的时候进行一段时间的等待以弥补这种延迟即可。
所以在进行主库的数据插入之后,让数据库数据连接或者对应的执行线程等待一段时间后返回。通过等待时间来消化掉主从备份的延迟时间。但是这个方法也有一些问题比如:这个等待时间一般是固定的,即便主从已经无延迟了也会继续等待到时间结束;如果在服务高峰时期,有可能数据在等待时间结束后仍然没有完成同步则仍然会存在一致性问题。
但这种方法优雅的地方是可以配合业务来进行实现,举例来说当用户下单之后,通过下单送卷或者下单抽奖的方式从前端拖住用户,从而当用户在一次连续操作中再次查询自己订单的时候中间必然会间隔一定时间,也就让需要再次查询数据的时候保证了数据的一致性。
上述两种方案看起来可能不那么“技术”,感觉有点投机取巧。那么下面咱们可以分两种情况来讨论用更高技术的方法如何实现一致性。
对于主从复制来说,是当主库完成一个事务后,通知给从库,当从库接受到后,则主库完成返回客户端。所以当主库完成事务后,仅能确保从库已经接受到了,但是不能保证从库执行完成,也就是导致了主从备份延迟。
但是从库执行数据是有进度的,而这个进度是可以通过show slave status语句中的seconds_behind_master来进行描述,这个参数描述从库落后了主库数据多少秒,当这个参数为0时,我们可以认为从库和主库已经基本上没有延迟了,那么这时候就可以查询请求。
但seconds_behind_master是秒级的,所以只能大概地判断,由于精度较低,所以还是可能出现不一致的情况。
如果要求精准执行的话,我们可以比较同步文件的执行记录,具体来说是:
所以当Relay_Master_Log_File和Exec_Master_Log_Pos和其一致的时候,就说明从库的已执行数据已经追上主库了,那么这时就可以说保证了主从一致性了
但是比较同步文件的执行记录方法的问题在于,如果当前的这个事务的binlog尚未传入到从库,即Master_Log_File和Read_Master_Log_Pos未更新,也就无法保证从库已经包含最新的主库事务了。
而为了保证在一主一备的情况下,从库里一定接受到数据了,也就是Master_Log_File和Read_Master_Log_Pos中的数据是和主库一致的,我们可以开启semi-sync replication半同步复制。
半同步复制的原理是在主库提交事务前先将binlog发送给从库,然后当从库接受后返回一个应答,主库只有在接受到这个应答之后才返回事务执行完成。这样就可以保证从库的Master_Log_File和Read_Master_Log_Pos与主库是一致的,从而解决了主从一致的问题。
半同步复制可以解决一主一备的情况,但是当一主多备的时候,只要主库接受到一个从库的应答,就会返回事务执行完成。而这时当请求打到未完成同步的从库上时就会发生主从延迟。
所以针对一主多备的情况,我们可以将目光集中在执行查询的从库上,即确保 我们即将查询的备库已经执行了我们预期的事务。 那么我们的问题就变成两部分:1. 确认主库事务,2. 查询数据条件。
确认主库事务
当我们提交完一个事务后,可以通过执行show master status来得到主库中的数据事务文件(File)和位置记录(Position)。
查询数据条件
当我们要查询从库数据的时候,我们可以通过语句select master_pos_wait(File, Position, 1);来查询当前是否已经执行到了该记录(当返回值>=0的时候说明已经执行过了)。其中最后的数字1表示阻塞时长。
通过先确认主库事务记录,再判确认备库是否已经执行了了主库对应的事务。
但是可以发现,这种方法要求查询的时候知道主库的事务信息,对场景有很大的限制。
主从一致的问题源自主从延迟,所以我们就是从如何消除延迟来解决问题。简单点的方案我们可以不走备库、或者直接等待一段时间来忽略延迟的影响。在一主一备的情况下我们可以粗力度的用seconds_behind_master来判断或者用Relay_Master_Log_File和Exec_Master_Log_Pos来判断。而当一主多从的情况下我们则需要在查询前传入主库执行的事务记录才能保证数据一致性。
可以看出,当数据规模和部署方式变更的时候,好的解决方案将会越来越多。我认为根据实际业务情况选择最合适的方法才是最重要的。
Ⅸ SpringBoot项目中实现MySQL读写分离
但我们仔细观察我们会发现,当我们的项目都是用的单体数据库时,那么就可能会存在如下问题:
为了解决上述提到的两个问题,我们可以准备两 (多) 台MySQL,一台主( Master )服务器,一台从( Slave )服务器,主库的 数据变更 (写、更新、删除这些操作) ,需要 同步 到从库中 (主从复制) 。而用户在访问我们项目时,如果是 写操作 (insert、update、delete),则直接操作 主库 ;如果是 读操作 (select) ,则直接操作从库,这种结构就是 读写分离 啦。
在这种读写分离的结构中,从库是可以有多个的
MySQL主从复制是一个 异步 的复制过程,底层是基于Mysql数据库自带的 二进制日志 功能。就是一台或多台MySQL数据库(slave,即 从库 )从另一台MySQL数据库(master,即 主库 )进行日志的复制,然后再解析日志并应用到自身,最终实现 从库 的数据和 主库 的数据保持一致。MySQL主从复制是 MySQL数据库自带功能,无需借助第三方工具。
二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但是不包括数据查询语句。此日志对于灾难时的数据恢复起着极其重要的作用,MySQL的主从复制, 就是通过该binlog实现的。默认MySQL是未开启该日志的。
在环境搭建之前,我们需要准备好两台服务器,如果生活富裕使用的是两台云服务器的时候记得要开放安全组,即防火墙;如果是比狗子我生活好点但也是用的虚拟机的话,记得别分这么多内存启动蓝屏了(别问怎么知道的)
这里就不给大家展示数据库的安装和防火墙的操作了,这个我感觉网上好多资源都能够满足遇到的问题,在搭建主从库的时候有在网上见到过说MySQL版本要一致的,我也没太留意直接就在之前的MySQL上操作了,大家可以自己去验证一下。
服务器:192.168.150.100(别试了黑不了的,这是虚拟机的ip)
这里有三个方法都能重启MySQL,最简单的无疑就是一关一开:
登录进去MySQL之后才能够执行下面的命令,因为这是SQL命令,Linux不认识这玩意是啥。
这个时候还 不用退出MySQL ,因为下面的命令还是SQL命令,执行下面的SQL,可以拿到我们后面需要的两个重要参数。
执行完这一句SQL之后,==不要再操作主库!不要再操作主库!不要再操作主库!==重要的事情说三遍,因为再操作主库之后可能会导致红框中的 两个属性值会发生变化 ,后面如果发生了错误可能就和这里有那么两毛钱关系了。
服务器:192.168.150.101(别试了黑不了的,这也是虚拟机的ip)
这里要注意server-id和主库以及其他从库都不能相同,否则后面将会配置不成功。
这里有三个方法都能重启MySQL,最简单的无疑就是一关一开:
登录进去MySQL之后才能够执行下面的命令,因为这是SQL命令
参数说明:
这个时候还 不用退出MySQL ,因为下面的命令还是SQL命令,执行下面的SQL,可以看到从库的状态信息。通过状态信息中的 Slave_IO_running 和 Slave_SQL_running 可以看出主从同步是否就绪,如果这两个参数全为 Yes ,表示主从同步已经配置完成。
这可能是由于linux 是复制出来的,MySQL中还有一个 server_uuid 是一样的,我们也需要修改。 vim /var/lib/mysql/auto.cnf
这应该就是各位大牛设置server_id的时候不小心设置相同的id了,修改过来就行,步骤在上面的配置中。
这是狗子在操作过程中搞出来的一个错误……
出错的原因是在主库中删除了用户信息,但是在从库中同步的时候失败导致同步停止,下面记录自己的操作(是在进入MySQL的操作且是从库)。
在数据库中操作时,一定要注意当前所在的数据库是哪个,作为一个良好的实践:在SQL语句前加 USE dbname 。
Sharding-JDBC定位为 轻量级Java框架 ,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以 jar包 形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动, 完全兼容JDBC和各种ORM框架 。
使用Sharding-JDBC可以在程序中轻松的实现数据库 读写分离 。
Sharding-JDBC具有以下几个特点:
下面我们将用ShardingJDBC在项目中实现MySQL的读写分离。
在pom.xml文件中导入ShardingJDBC的依赖坐标
在application.yml中增加数据源的配置
这时我们就可以对我们项目中的配置进行一个测试,下面分别调用一个更新接口和一个查询接口,通过查看日志中记录的数据源来判断是否能够按照我们预料中的跑。
搞定!!!程序正常按照我们预期的成功跑起来了,成功借助ShardingJDBC在我们项目中实现了数据库的读写分离。
Ⅹ SpringBoot整合MybatisPius实现数据库多数据源及读写分离
其他主从方式配置模板
@DS 可以注解在方法上和类上扒早,同时存在方法注解优先于类上注解。
强烈建议只注解在service实现上。
某些springBoot的版本上面埋罩可能无法排除(尝试使用以下方式排春液雀除)