springboot redis druid 负载均衡 mysql集群
springboot redis druid 负载均衡 mysql集群
准备2台mysql8数据库
192.168.18.111 mysql8
192.168.18.253 mysql8
用druid配置连接池,AOP实现负载均衡(轮询,用redis存放数据库集群数量下标)mysql数据库集群
springboot druid 负载均衡 mysql集群
pom.xml
<!-- jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--集成redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
application.yml
spring:
redis:
timeout: 15000
#单机
host: localhost
port: 6379
password:
#数据库
datasource:
username: x
password: x
driver-class-name: com.mysql.cj.jdbc.Driver
#druid连接池
type: com.alibaba.druid.pool.DruidDataSource
#设置成双机热备数据库最好
mysql1:
url: jdbc:mysql://192.168.18.111:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
# 初始连接数
initial-size: 10
# 最小连接池数量
min-idle: 10
# 最大连接池数量
max-active: 100
# 配置获取连接等待超时的时间
max-wait: 6000
# 打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 30000
# 配置检测连接是否有效
validationQuery: SELECT 1
#申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
keepAlive: true
mysql2:
url: jdbc:mysql://192.168.18.253:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
# 初始连接数
initial-size: 10
# 最小连接池数量
min-idle: 10
# 最大连接池数量
max-active: 100
# 配置获取连接等待超时的时间
max-wait: 6000
# 打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 30000
# 配置检测连接是否有效
validationQuery: SELECT 1
#申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
keepAlive: true
#监控信息配置
monitor:
username: root
password: root
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.fu.demo.entity
DemoApplication.java启动类
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@MapperScan("com.fu.demo.mapper")
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})//取消自动配置数据库
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
DynamicDataSourceContextHolder.java
public class DynamicDataSourceContextHolder {
/**
* 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置数据源的变量
*/
public static void setDateSourceType(String dsType) {
CONTEXT_HOLDER.set(dsType);
}
/**
* 获得数据源的变量
*/
public static String getDateSourceType() {
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源变量
*/
public static void clearDateSourceType() {
CONTEXT_HOLDER.remove();
}
}
DynamicDataSource.java
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
this.setDefaultTargetDataSource(defaultTargetDataSource);
this.setTargetDataSources(targetDataSources);
this.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDateSourceType();
}
}
DruidConfig.java
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
@Configuration
public class DruidConfig {
/**
* 配置别名
*/
@Value("${mybatis.type-aliases-package}")
private String typeAliasesPackage;
/**
* 配置mapper的扫描,找到所有的mapper.xml映射文件
*/
@Value("${mybatis.mapper-locations}")
private String mapperLocations;
/**
* 获取druid监控信息
* */
@Value("${spring.datasource.monitor.username}")
private String username;
@Value("${spring.datasource.monitor.password}")
private String password;
@Bean
@ConfigurationProperties("spring.datasource.mysql1")
public DataSource mysql1DataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.mysql2")
public DataSource mysql2DataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("mysql1", mysql1DataSource());
targetDataSources.put("mysql2", mysql2DataSource());
return new DynamicDataSource(mysql1DataSource(), targetDataSources);
}
/**
* SqlSessionFactory 配置并放入容器中
*/
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage);
return sqlSessionFactoryBean.getObject();
}
/**
* 事务
*/
@Bean
public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
/**
* 连接池监控
* */
@Bean
public ServletRegistrationBean druidServlet() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
servletRegistrationBean.addInitParameter("loginUsername", username);
servletRegistrationBean.addInitParameter("loginPassword", password);
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean druidFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
DataSourceAspect.java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
@Resource
private RedisTemplate<String,Integer> redisTemplate;
@Around("execution(public * com.fu.demo.mapper.*.*(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
//轮询数据库
String INDEX = "index";
if (!redisTemplate.hasKey(INDEX)) redisTemplate.opsForValue().set(INDEX, 0);//如果key不存在就创建key,并设置下标初始值为0
int index = redisTemplate.opsForValue().get(INDEX);//有下标则直接获取
List<String> list = new ArrayList<>();
list.add("mysql1");
list.add("mysql2");
if (index >= list.size()) index = 0;//超过list集合的值就重新赋值(轮询)
DynamicDataSourceContextHolder.setDateSourceType(list.get(index));
redisTemplate.opsForValue().set(INDEX, ++index);//利用redis单线程的特性存放全局index下标
try {
return point.proceed();
} finally {
// 销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.clearDateSourceType();
}
}
}
演示
192.168.18.111(本机)
192.168.18.253(主要修改了名称)
启动springboot项目访问测试接口,如:localhost:81/user/select?id=1
第一次
第二次
第三次
到此springboot负载均衡mysql数据库已经实现
题外
2台数据库没有双机热备,因此数据不一致。要解决这个问题只要把2台服务器的mysql做成双机热备集群即可。
《mysql双机热备、互为主从集群》
因为有负载均衡了,因此可以不用nginx实现高可用了。
当redis单机宕机时会影响mysql负载均衡,因此redis建议也搭成集群。
《redis 集群搭建 分布式锁》
至此mysql负载均衡、高可用均已实现,但当项目宕机时,用户仍然无法使用。因此要保证springboot项目高可用、负载均衡…
至此。。。你会发现你的项目要改成springcloud微服务了。
如果mysql数据库压力不大,可以只做高可用。
如果数据库压力大可以弄双击热备mysql集群,负载均衡。
压力巨大,建议分库分表、读写分离了。
没图了,我摊牌了,我不会了!
优化:实现高可用、负载均衡
application.yml
把druid的max-wait改成1毫秒,这样宕机一台,马上可以换另外一台
# 配置获取连接等待超时的时间
max-wait: 1
DataSourceAspect.java
把轮询放到外面去,这样就不用写2次。catch MyBatisSystemException捕获到连接异常。我只是简单的捕获,实际开发当中应该捕获具体的异常。
@Around("execution(public * com.fu.demo.mapper.*.*(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
try {
robin();//轮询数据库
return point.proceed();
} catch (MyBatisSystemException e) {
robin();//连接不上就再轮询一次,获取另外一个mysql数据库连接
return point.proceed();
} finally {
// 销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.clearDateSourceType();
}
}
/**
* 轮询mysql数据库
*/
public void robin(){
String INDEX = "index";
if (!redisTemplate.hasKey(INDEX)) redisTemplate.opsForValue().set(INDEX, 0);//如果key不存在就创建key,并设置下标初始值为0
int index = redisTemplate.opsForValue().get(INDEX);//有下标则直接获取
List<String> list = new ArrayList<>();
list.add("mysql1");
list.add("mysql2");
if (index >= list.size()) index = 0;//超过list集合的值就重新赋值(轮询)
DynamicDataSourceContextHolder.setDateSourceType(list.get(index));
redisTemplate.opsForValue().set(INDEX, ++index);//利用redis单线程的特性存放全局index下标
}
这样,就算关掉其中一台数据库,也是能立马响应的,只是控制台会不断抛出异常。这个留给你们自己优化了。
更多推荐
所有评论(0)