简介
这个帖子记录《软件架构与中间件》课程实验5的实现过程中,用到的一些新技术以及一些踩坑。
首先放一个实验要求:
其次这里提到的计算层,数据层,表示层,分别为:
项目架构
因为自己的笔记本性能有限,这里只开了三个虚拟机作为集群,模拟分布式环境。选用的技术有:Nginx(计算层),Sharding-JDBC和Redis(数据层),React(表示层)。
集群上部署的应用架构如上图。
首先一台主机运行Nginx,负责分发静态资源(前端使用React开发的页面),并对请求进行反向代理。然后三台主机都运行一个Tomcat后端(使用SpringBoot开发)。Ningx根据规则(比如ip_hash)将请求分别分发到这三个后端。在数据层,使用Redis作为数据的缓存,这里部署了单机的Redis在一台主机上,实际上Redis是可以搭集群的,但是这里因为懒精力有限,只使用了单机。然后使用Sharding-JDBC进行分库分表,将数据分别储存在三台主机的MySQL上。
下面介绍整个开发过程:
Mybatis Generator
实际上,在这个小项目里面直接使用Mybatis就可以了(毕竟只有一些增删改查,(虽然要求使用之前课程开发的系统上继续做,但是为了方便还是因为懒,直接写了一套简单的系统))。
我们知道MyBatis是一款优秀的Java持久层框架,但是其实自己写所有的实体类、Mapper 接口以及映射文件(XML)还是比较麻烦的。
MyBatis Generator是一个开源项目,它可以使用简单的 XML 配置文件或者 Java 注解来生成 MyBatis 的实体类、Mapper 接口以及映射文件(XML)。它可以根据数据库表结构自动生成代码,这样可以大大减轻开发人员的工作负担。
这部分主要参考解放双手!MyBatis官方代码生成工具给力!
引入依赖
在pom.xml
中引入Mybatis Generator的依赖。
<!-- MyBatis 生成器 -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.2</version>
</dependency>
参数文件
在resources
下创建文件generator.properties
其中内容如下:
jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.connectionURL=jdbc:mysql://${host}:3306/${dbname}?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
jdbc.userId=${username}
jdbc.password=${password}
需要将其中的${}
括起来的内容根据实际情况更换。
配置文件
在resources
下创建文件generatorConfig.xml
其中内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<properties resource="generator.properties"/>
<context id="MySqlContext" targetRuntime="MyBatis3" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<property name="javaFileEncoding" value="UTF-8"/>
<!--生成mapper.xml时覆盖原文件-->
<plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
<!-- 为模型生成序列化方法-->
<plugin type="org.mybatis.generator.plugins.SerializablePlugin"/>
<!-- 为生成的Java模型创建一个toString方法 -->
<!-- <plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>-->
<plugin type="org.mybatis.generator.plugins.LombokPlugin" />
<!--可以自定义生成model的代码注释-->
<!-- <commentGenerator type="com.hit.lab.back.mbg.CommentGenerator">-->
<commentGenerator >
<!-- 是否去除自动生成的注释 true:是 : false:否 -->
<property name="suppressDate" value="true"/><!-- 不生成日期 -->
<property name="suppressAllComments" value="true"/>
<property name="addRemarkComments" value="true"/>
</commentGenerator>
<!--配置数据库连接-->
<jdbcConnection driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.connectionURL}"
userId="${jdbc.userId}"
password="${jdbc.password}">
<!--解决mysql驱动升级到8.0后不生成指定数据库代码的问题-->
<property name="nullCatalogMeansCurrent" value="true" />
</jdbcConnection>
<!--指定生成model的路径-->
<javaModelGenerator targetPackage="com.hit.lab.back.bean" targetProject="src\main\java"/>
<!--指定生成mapper.xml的路径-->
<sqlMapGenerator targetPackage="com.hit.lab.back.mbg.mapper" targetProject="src\main\resources"/>
<!--指定生成mapper接口的的路径-->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.hit.lab.back.mbg.mapper"
targetProject="src\main\java"/>
<!--生成全部表tableName设为%-->
<table tableName="t_customer" domainObjectName="Customer">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
<table tableName="t_order" domainObjectName="Order">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
<table tableName="t_product" domainObjectName="Product">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
</context>
</generatorConfiguration>
这里直接贴出了实验项目中使用的文件,请根据实际内容进行替换。
接下来对这个文件进行一定解读,更详细的内容请自行查阅官方的文档。
这里会将前面的generator.properties
中的内容读入,所以前面的文件一定要写对。
这里配置生成的实体类,Mapper接口,XML映射文件的位置。
targetProject
配置项目位置,一般就是当前位置,按照上面写就好。
targetPackage
配置生成文件的包,会决定生成文件在项目的位置。
综上,对于这样一行来讲,
<javaModelGenerator targetPackage="com.hit.lab.back.bean" targetProject="src\main\java"/>
会将实体类生成到系统路径为${project_path}/src/main/java/com/hit/lab/back/bean
这个目录下。
这三个标签对数据库中的table经行定义,tableName
为数据库中的表名,domainObjectName
为你需要生成的实体名,这也会影响到Mapper接口和XML的命名。
生成器
import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class Generator {
public static void main(String[] args) throws Exception {
//MBG 执行过程中的警告信息
List<String> warnings = new ArrayList<String>();
//当生成的代码重复时,覆盖原代码
boolean overwrite = true;
//读取我们的 MBG 配置文件
InputStream is = Generator.class.getResourceAsStream("/generatorConfig.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(is);
is.close();
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
//创建 MBG
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
//执行生成代码
myBatisGenerator.generate(null);
//输出警告信息
for (String warning : warnings) {
System.out.println(warning);
}
}
}
结合LomBok
Lombok是一种Java库,可以使Java开发更加简洁, 通过注解减少样板式代码。使用Lombok可以节省开发者大量的时间和精力,并提高代码可读性。
Lombok支持许多常见的类,如getter,setter,构造函数等,而不需要像手动编写Java Bean那样繁琐。在使用Lombok之前,JavaBean通常需要写很多的setter和getter方法和有参无参构造器。而使用 Lombok 就可以只用一个简单的注解来生成代码。
这里我希望使用lombok中的@Data等注解代替Mybatis Generator生成的实体类中的getter,setter,Constructor等等。
最开始询问GPT,得到的回答说要使用一个插件,(上面的配置文件中已经使用了),但是,经过排查,发现Mybatis Generator中并没有这个插件。
所以这个插件需要我们自己实现,这里参考了mybatis-generator 整合lombok
需要我们自己创建一个LombokPlugin
,这里需要注意,它一定要在包package org.mybatis.generator.plugins;
下,没有的话就自己在项目中创建一个。(这里猜测,其实不必一定在这个package,只要和前面引用的插件路径相同即可)
下面给出代码:
package org.mybatis.generator.plugins;
import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.PluginAdapter;
import org.mybatis.generator.api.dom.java.Interface;
import org.mybatis.generator.api.dom.java.Method;
import org.mybatis.generator.api.dom.java.TopLevelClass;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
public class LombokPlugin extends PluginAdapter {
public LombokPlugin() {
}
public boolean validate(List<String> list) {
return true;
}
public boolean modelBaseRecordClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
topLevelClass.addImportedType("lombok.Data");
topLevelClass.addAnnotation("@Data");
topLevelClass.addImportedType("lombok.Getter");
topLevelClass.addImportedType("lombok.Setter");
topLevelClass.addImportedType("lombok.ToString");
topLevelClass.addAnnotation("@Getter");
topLevelClass.addAnnotation("@Setter");
topLevelClass.addAnnotation("@ToString");
topLevelClass.addJavaDocLine("/**");
topLevelClass.addJavaDocLine("* Created by Mybatis Generator " + this.date2Str(new Date()));
topLevelClass.addJavaDocLine("*/");
return true;
}
public boolean clientGenerated(Interface interfaze, TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
interfaze.addJavaDocLine("/**");
interfaze.addJavaDocLine("* Created by Mybatis Generator " + this.date2Str(new Date()));
interfaze.addJavaDocLine("*/");
return true;
}
public boolean modelSetterMethodGenerated(Method method, TopLevelClass topLevelClass, IntrospectedColumn introspectedColumn, IntrospectedTable introspectedTable, ModelClassType modelClassType) {
return false;
}
public boolean modelGetterMethodGenerated(Method method, TopLevelClass topLevelClass, IntrospectedColumn introspectedColumn, IntrospectedTable introspectedTable, ModelClassType modelClassType) {
return false;
}
private String date2Str(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
return sdf.format(date);
}
}
最终生成的实体类如图:
可以看到这样生成的实体类是非常干净的。
使用
Mybatis Generator生成的Example类主要用于条件查询和删除操作,以及分页查询的参数传递。下面简单介绍一下如何使用。
-
根据表结构生成对应的Example类
例如,根据表名为user生成的Example类为UserExample,可以在mapper.xml文件中的SQL语句中引用。 -
设置查询条件
在UserExample类中,可以设置查询条件,例如:
UserExample example = new UserExample();
example.createCriteria().andAgeGreaterThan(18);
//表示查询年龄大于18岁的用户。
- 执行查询操作
调用对应的mapper接口方法进行查询操作,例如:
List<User> userList = userMapper.selectByExample(example);
//以上代码将返回满足查询条件的所有用户对象(List类型)。
- 分页查询
在UserExample类中,还可以设置分页信息,例如:
UserExample example = new UserExample();
example.setLimit(10);
example.setOffset(0);
example.createCriteria().andAgeGreaterThan(18);
//以上代码表示返回满足条件的前10条记录。
- 删除操作
在UserExample类中,除了可以查询,还可以删除符合条件的数据,例如:
UserExample example = new UserExample();
example.createCriteria().andAgeLessThan(18);
userMapper.deleteByExample(example);
//以上代码表示删除年龄小于18岁的用户记录。
需要注意的是,在使用Example类时,需要根据具体情况进行修改,例如表名、属性名、查询条件等等。
Sharding-JDBC
引入依赖
<!--sharding jdbc -->
<!--<!– https://mvnrepository.com/artifact/org.apache.shardingsphere/sharding-jdbc-spring-boot-starter –>-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-namespace</artifactId>
<version>4.1.1</version>
</dependency>
<!--集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.16</version>
</dependency>
配置文件
在application.yml
中添加以下配置
spring:
shardingsphere:
datasource:
names: db1, db2, db3
db1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test1
username: test
password: test
db2:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test2
username: test
password: test
db3:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test3
username: test
password: test
sharding:
tables:
t_customer:
actual-data-nodes: db$->{1..3}.t_customer
database-strategy:
inline:
sharding-column: id
algorithm-expression: db$->{id % 3 + 1}
key-generator:
column: id
type: SNOWFLAKE
t_order:
actual-data-nodes: db$->{1..3}.t_order
database-strategy:
inline:
sharding-column: id
algorithm-expression: db$->{id % 3 + 1}
key-generator:
column: id
type: SNOWFLAKE
t_product:
actual-data-nodes: db$->{1..3}.t_product
database-strategy:
inline:
sharding-column: id
algorithm-expression: db$->{id % 3 + 1}
key-generator:
column: id
type: SNOWFLAKE
这里对数据进行水平切片,经数据分散到不同的表中。
接下来按照我个人的理解对这些配置进行讲解。
这里定义数据源,type
为数据源的类型,这里集成了druid连接池,所以选择com.alibaba.druid.pool.DruidDataSource
。如果引入了druid(在未对这个项目改造时,使用Mybatis顺便引入的),但是直接跟着官网教程没有使用这个type,而是使用了org.apache.commons.dbcp2.BasicDataSource
,会报错。一个坑
接下来是分片规则:
actual-data-nodes
表示这个table在哪些数据源中有。
algorithm-expression
表示按照id对3求余再加1,比如对于id为5的,对应db3,对于id为3的对应db1。
key-generator
中的SNOWFLAKE表面按照雪花算法生成id。
接下里是第二个坑
在测试时发现insert的数据,不会将id回显到实体类中,实体类中的id为0,查看mybatis generator生成的xml代码,发现对应语句,如下:
按照这样的写法,应该是可以回显到实体类中的。
但是经过手动在mysql中执行
SELECT LAST_INSERT_ID();
发现其返回也一直是0,即使对于设置了自增主键的表也是这样。
个人分析,是因为,Sharding-JDBC的key-generator是在程序中生成一个id,再将数据整个插入到数据库中,这个id并不是通过MySQL自己生成的,所以通过这个函数获取到的一直是0。
修改方法如下:
将SELECT LAST_INSERT_ID();
修改为SELECT MAX( id ) FROM t_customer
即可
<insert id="insert" parameterType="com.hit.lab.back.bean.Customer">
<selectKey keyProperty="id" order="AFTER" resultType="java.lang.Long">
SELECT MAX( id ) FROM `t_customer`
</selectKey>
insert into t_customer (type, name)
values (#{type,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR})
</insert>
Redis
关于Redis的基础配置以及连接池的设置,参考:
SpringBoot教程(十四) | SpringBoot集成Redis(全网最全)
引入依赖
<!-- 集成redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2-extension-spring5 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2-extension-spring5</artifactId>
<version>2.0.32</version>
</dependency>
这里不仅引入了redis的依赖,还有连接池的依赖,以及fastjson2的依赖。连接池和fastjson2都是为了优化系统性能。连接池的作用不必多说,fastjson2作为Redis序列化的工具可以更快的进行序列化。
配置文件
在application.yml
中添加以下配置
spring:
redis:
host: localhost
port: 6379
# password: 123456
database: 0
# cluster:
# nodes: 10.255.144.115:7001,10.255.144.115:7002,10.255.144.115:7003,10.255.144.115:7004,10.255.144.115:7005,10.255.144.115:7006
# max-redirects: 3
lettuce:
pool:
max-idle: 16
max-active: 32
min-idle: 8
自定义配置
import com.alibaba.fastjson2.support.spring.data.redis.FastJsonRedisSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* Description: 设置redis配置
*/
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(redisHost, redisPort);
return new LettuceConnectionFactory(configuration);
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 默认缓存时间
.entryTtl(Duration.ofSeconds(600))
// 设置key的序列化方式
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
// 设置value的序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// 使用fastJson序列化
// value值的序列化采用fastJsonRedisSerializer
redisTemplate.setValueSerializer(new FastJsonRedisSerializer<>(Object.class));
redisTemplate.setHashValueSerializer(new FastJsonRedisSerializer<>(Object.class));
// key的序列化采用StringRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
在redisTemplate
这个方法中,指定序列化使用import com.alibaba.fastjson2.support.spring.data.redis.FastJsonRedisSerializer;
这个序列化器。
又一个坑
这里配置一开始写成了这样:
这样在本地运行不会出什么问题,但是当把它放在集群上时,发现即使修改了配置文件中的spring.redis.host
,但是Lettuce一直都尝试连接localhost:6739
。所以需要通过以上的方式读入这些配置,也许还有其他的改法,有知道的话可以留言。
使用
关于redis的使用,一般是封装成工具类再使用,这里直接采用了SpringBoot整合Redis,封装RedisUtils里面的工具类,由于篇幅比较长,这里就不粘贴了。
使用时只要将其注入到自己的Service或者Controller中即可。
Nginx
因为Nginx在这个项目中用来分发静态资源,以及反向代理,另外还可以解决跨域问题,这里没有代码层面的开发。
这里先给出在Windows上的配置文件:
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
upstream backend{
server 127.0.0.1:8080;
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
try_files $uri $uri/ /index.html; # 配置根路径请求时默认为加载/index.html
}
location /favicon.ico {
}
location /goods {
try_files $uri $uri/ /index.html; # 将所有请求重定向到index.html
}
location /customers {
try_files $uri $uri/ /index.html; # 将所有请求重定向到index.html
}
location /sales {
try_files $uri $uri/ /index.html; # 将所有请求重定向到index.html
}
location /api {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
if ($request_method = 'OPTIONS') {
return 204;
}
}
}
}
因为在这个配置中,并没有遇到太大的问题,另外其中注释也写的比较明白,这里不再赘述。
集群环境搭建
修改hosts
首先使用ifconfig查询三台虚拟机的ip:
然后修改三台虚拟机的/etc/hosts
文件,方便后面从主机名到ip的映射。
JDK
-
下载jdk二进制文件,这里使用
jdk-8u351-linux-x64.tar.gz
-
解压下载后的文件到某个目录。
-
修改
/etc/profile
文件,配置JDK信息。然后执行
source /etc/profile
命令生效。
这里建议先重启一下,让这些环境变量生效(固化?)。
5. 验证一下:
MySQL
安装
sudo apt install mysql-server
验证
一般安装后会自动运行,使用下面指令验证:
sudo systemctl status mysql
这代表mysql在正常运行。
修改配置
接下来需要对mysql进行一些修改,使其可以远程连接。主要参考:MySQL8 设置远程访问授权
- 确定服务器上的防火墙没有阻止 3306 端口
由于我的虚拟机环境,防火墙没有开启,这里省略。 - MySQL配置文件中设置本地IP/localhost绑定
在/etc/mysql/mysql.conf.d/mysqld.cnf
文件中找到bind-address
这一行,这里原本是127.0.0.1
,代表只监听从本机来的连接,将其改为0.0.0.0
,监听所有来源。 - 创建一个远程账户,并授权
CREATE USER 'remote'@'%' IDENTIFIED BY '123456';
GRANT ALL PRIVILEGES ON *.* TO 'remote'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
- 验证一下
这里在宿主机使用Navicat来测试,确保可以远程连接。
Nginx
安装
sudo apt install nginx
验证
和mysql一样一般安装后会自动运行,使用下面指令验证:
sudo systemctl status nginx
修改配置
通过apt安装的nginx的配置文件都在/etc/nginx
中。
conf.d
中可以存放自定义的配置文件,nginx.conf
中为全局的默认配置,建议不要乱改。
但是要有以下修改:
- 将
user
从www-data
改为root
否则会报500,查看/var/log/nginx/error.log
,如下图:
解决方法参考Nginx转发请求,报13:Permission denied错误
其分析如下:
- 将最后的
include /etc/nginx/sites-enabled/*;
注释掉,它会将你自己定义的location / {}
覆盖掉,去到欢迎页面。
这里也能看出会将/etc/nginx/conf.d/*.conf
这个路径下的配置文件导入进来。
自定义配置
/etc/nginx/conf.d
路径下新建一个以conf
为扩展名的文件。
这里我直接贴出配置文件,具体其中内容不再赘述。
upstream backend{
ip_hash;
server lab1:8080;
server lab2:8080;
server lab3:8080;
}
server {
listen 80;
server_name localhost;
location / {
root /home/lsxuan/site;
index index.html index.htm;
try_files $uri $uri/ /index.html; # 配置根路径请求时默认为加载/index.html
}
location /favicon.ico {
}
location /goods {
try_files $uri $uri/ /index.html; # 将所有请求重定向到index.html
}
location /customers {
try_files $uri $uri/ /index.html; # 将所有请求重定向到index.html
}
location /sales {
try_files $uri $uri/ /index.html; # 将所有请求重定向到index.html
}
location /api {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
if ($request_method = 'OPTIONS') {
return 204;
}
}
}
防火墙
一般80端口应该都是开的吧,所以一般不用操作。当然我的虚拟机环境防火墙完全没开,更加不用操作了。
坑again
这里一开始我将nginx.conf
这个文件清空,自己写了一些server块,upstream块之类的,没有以下配置,导致浏览器一直报错,且css无法正常加载。
而且,因为浏览器默认会开启缓存,这种静态文件不会重新加载,所以,即使将这个文件修改正确,浏览器还是会一直报错。血泪啊
坑again的again
- 在同一个局域网内,ip_hash不生效(虚拟机当然也有这个问题)
详细分析参考nginx通过ip-hash算法负载不均或不起效问题 - 轮询时不起作用,所有请求都发往同一后端
参考 Nginx配置了默认轮询方式,但是轮询失效,刷新后只能访问一个tomcat服务器
解决办法:
在配置文件中添加
location /favicon.ico {
}
验证
在宿主机通过ip访问虚拟机,看看nginx是否正常。
Redis
安装
sudo apt install redis-server
验证
同前两者一样一般安装后会自动运行,使用下面指令验证:
sudo systemctl status redis-server
编辑配置文件
在/etc/redis/redis.conf
找到bind
,将其从127.0.0.1 ::1
修改为0.0.0.0 ::1
,原理同mysql的修改。
防火墙
虚拟机环境防火墙没开
验证
在宿主机使用以下指令验证其是否可以远程访问:
redis-cli -h ${host/ip} ping
部署
前端
前端使用react在webstorm中开发。
在webstorm的命令行中使用以下命令打包。
npm run build
运行完成后会生成以下文件。
在windows上将这些文件拷贝到以下目录:
在ubuntu上将这些文件拷贝到以下目录:
这个路径都是在配置文件中可以自定义的。
比如,在windows中:
在ubuntu上:
后端打包
将pom.xml
中这一行注释掉,否则打出来的包运行时会报错,没有主类。(以前的某次打包的经验,这次没有验证)
使用maven的package打包。
将这个生成的jar放到ubuntu的任意一个目录即可。
运行
前端放到nginx设置的目录下即可。
后端使用java运行。
java -jar ${jar_name}
这样运行会使用jar包内部的配置文件,如果要指定springboot的配置文件可以使用-Dspring.config.location
参数。比如
java -jar back-0.0.1-SNAPSHOT.jar -Dspring.config.location=./appication.yml
另外这里贴出appication.yml
的内容:
logging:
level:
root: INFO
mybatis:
mapper-locations: classpath*:dao/*.xml, classpath*:com/**/mapper/*.xml
server:
port: 8080
spring:
redis:
host: lab1
port: 6379
database: 0
lettuce:
pool:
max-idle: 16
max-active: 32
min-idle: 8
shardingsphere:
datasource:
names: db1, db2, db3
db1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://lab1:3306/test
username: remote
password: 123456
db2:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://lab2:3306/test
username: remote
password: 123456
db3:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://lab3:3306/test
username: remote
password: 123456
sharding:
tables:
t_customer:
actual-data-nodes: db$->{1..3}.t_customer
database-strategy:
inline:
sharding-column: id
algorithm-expression: db$->{id % 3 + 1}
key-generator:
column: id
type: SNOWFLAKE
t_order:
actual-data-nodes: db$->{1..3}.t_order
database-strategy:
inline:
sharding-column: id
algorithm-expression: db$->{id % 3 + 1}
key-generator:
column: id
type: SNOWFLAKE
t_product:
actual-data-nodes: db$->{1..3}.t_product
database-strategy:
inline:
sharding-column: id
algorithm-expression: db$->{id % 3 + 1}
key-generator:
column: id
type: SNOWFLAKE
效果
Sharding-JDBC
插入若干条数据,可以看到数据被分散到多个数据源中。
Redis
这里先介绍一下,代码中如何使用redis
这里只是一个简单的处理,从redis中取出所有的product,如果没有再从数据库中取出并存到redis中,若redis中已经缓存,则直接返回给客户端。
以下是redis中缓存的内容:
Nginx
因为ip_hash对同一局域网中的请求都会转发到同一后端服务器(这是nginx的实现算法决定的),所以这里先将ip_hash注释掉,采用默认的轮询策略。
可以看到nginx将请求平均分配到了各个后端服务器。
源代码
有机会放到github上,先鸽了。