本篇是一个真实落地项目整个后端开发的记录,记录了我看到和参与的项目从0到1的过程。
目录
一、项目概述
大概讲一下这个项目做的是什么和我参与的部分。
此项目是一个下单接单的平台,管理员在后台实现订单的录入、用户的管理、平台附件功能的管理和平台资源的管理;注册用户实现订单浏览、查询、申请接单和评价订单的功能。
后台包括的模块有:common工具类及通用代码模块,MyBatisGenerator生成的数据库操作代码模块,Oauth2认证中心模块,Gateway网关服务模块,monitor微服务监控中心模块,后台管理系统模块,门户网站服务模块,Elasticsearch搜索服务模块,配置中心模块。
开发环境:JDK 8、Mysql 5.7、Redis 5.0、Elasticsearch 7.6.2、Logstash 7.6.2、Kibana 7.6.2、Gitee
技术架构:SpringBoot、MyBatis-Plus、Spring Security Oauth2、Redis、ElasticSearch、MinIO
参与部分:
1、业务层面:负责用户管理(注册/登录、个人信息管理)、管理员管理(账号信息管理、角色权限管理);此部分基于RBAC,包括建立数据表和实现具体业务代码。
2、技术层面:负责数据库主从复制+多数据源+Redis缓存的实现。
二、项目开发流程层面
因为是第一次参与到真实项目中,所以对整个项目的开发流程是没有一点概念的。因此这次开发学习从0到1的过程,我感觉是和学习具体某项技术一样重要。下面是我总结的一个项目开发需要经过的流程:
1.项目沟通会:
项目开始前的第一次全体会议,这次会议的内容包括:
①开发人员的相互熟悉
②项目负责人介绍前期制定的项目功能需求概述,介绍前期归规划的项目整体架构、项目开发结构、项目开发周期,让开发人员对项目有一个初步的认识和定位
③发布PRD文档,让开发人员对项目具体内容有进一步发认识
2.分任务分组:
这一步实质上就是将项目划分为一个个小的实现模块,是项目实施重要的一步。
①根据具体内容分成架构任务和业务任务
②根据之前的项目认识结合个人方向选择任务和分组、建群
3.任务模块设计
①根据任务模块,按照PRD文档确定具体的需求
②根据需求设计数据库(数据字段、数据类型、数据表结构)
③根据具体需求设计Controller层的接口(接口的地址,接口参数、接口结果)
4.任务模块具体代码实现
按照之前的模块设计,创建数据表,编写具体代码(domain、dao、service、controller、confing)
5.项目技术业务设计
之前实现的是具体的业务层面,在业务层面开发完成之后需要完成整体项目的技术层面开发。
①主从复制+多数据源实现读写分离
②Redis 缓存实现访问加速
③ELK:Elasticsearch+ Logstash+ Kibana 实现日志系统
④Elasticsearch 实现项目搜索业务
6.项目技术业务实现
按照之前的技术设计完成具体实现(例如完成数据库主从复制的配置流程整理)
7.文档编写
项目完成之后需要对整个项目开发过程中所有的设计与开发进行文档编写
三、开发技术层面
这部分主要记录一些具体开发时用到的之前没用过的技术,包括:
①RBAC;
②数据库相关:索引、类型、非空;
③swagger注解;
④@Validation注解参数校验;
⑤mybatis plus lamda表达式使用;
⑥工具类的使用:ObjectUtil.isNotEnpty()等;
⑦一切要规范;
⑧IDEA + Gitee使用;
⑨主从复制+多数据源+读写分离
1.RBAC
一个平台有用户登录、注册等账号管理需求,最普及的就是RBAC模型:基于角色的访问控制(Role-Based Access Control)。这个模型就是将用户数据、角色数据、权限数据分开在不同的数据表,从而达到用户基于角色关联权限访问的灵活管理。RBAC模型根据规模又可分成RBAC0、RBAC1、RBAC2、RBAC3四种模型,此项目用RBAC0(用户、角色、权限)即可,具体的实现是:
① 创建:用户表(user)、角色表(role)、用户角色关联表(user_role)、权限表(permission)、角色权限关联表(role_permi)五张数据表;
#用户表
CREATE TABLE `user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '用户表主键id',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '姓名',
`phone_number` char(11) NOT NULL DEFAULT '' COMMENT '手机号',
`password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码',
`is_deleted` bigint unsigned NOT NULL DEFAULT 0 COMMENT '是否被逻辑删除 id表示被删除的用户id 0 表示不是',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次更新时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_phone_number` (`phone_number`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
-
建立数据表的时候,像用户信息这种重要的数据,要考虑逻辑删除。即执行删除业务代码的时候数据不会真的从数据库删除,虽然用户无法查看到,但管理员能通过后台看到数据。
-
数据表里的每一行数据都应该记录创建和修改时间,为后续管理分析提供支持,所以需要有创建和修改时间的字段。(此处使用数据库自带方法,在插入和修改数据时自动填入时间)
#角色表
CREATE TABLE `role` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '角色表主键id',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '角色名字',
`role_description` varchar(64) NOT NULL DEFAULT '' COMMENT '角色描述',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否被逻辑删除 1表示是 0 表示不是',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次更新时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色表';
#用户角色关联表
CREATE TABLE `user_role` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '用户角色关联表主键id',
`user_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '用户id',
`role_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '角色id',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否被逻辑删除 1表示是 0 表示不是',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次更新时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户角色关联表';
#权限详情表
CREATE TABLE `permission` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '权限详情表主键id',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '权限名字',
`permission_description` varchar(64) NOT NULL DEFAULT '' COMMENT '权限描述',
`url` varchar(1000) NOT NULL DEFAULT '' COMMENT '权限地址 json 数组',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否被逻辑删除 1表示是 0 表示不是',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次更新时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限详情表';
#角色权限关联表
CREATE TABLE `role_permission` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '角色权限关联表主键id',
`rid` bigint unsigned NOT NULL DEFAULT 0 COMMENT '角色id',
`pid` bigint unsigned NOT NULL DEFAULT 0 COMMENT '权限id',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '是否被逻辑删除 1表示是 0 表示不是',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次更新时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限关联表';
② user表唯一索引和“is_delete"字段建立联合索引提高查找速度,但个联合索引在插入数据的时候会引起冲突问题,具体问题详看博客:https://blog.csdn.net/u013310037/article/details/113045905
-
修改建议如下:
yml文件配置
mybatis-plus:
global-config:
db-config:
logic-delete-value: id #逻辑删除值为id,即:update is_deleted=id
logic-not-delete-value: 0 #逻辑未删除值为0
或者在实体类字段中使用@TableLogic注解
@ApiModelProperty(value = "0:未删除 其它(id):已删除")
@TableLogic(delval = "id")
private Long isDeleted;
并且逻辑删除字段:从tinyint改为bigint,即跟主键字段属性相同
RBAC参考博客:设计一个权限系统-RBAC_鱼儿-1226的博客-CSDN博客
2.数据库建表相关:索引、类型、非空
-
此项目是根据阿里规范进行开发的,规范里明确对索引的建立、某些字段的数据类型进行了要求。
-
字段非空:字段为null值时会放弃索引从而全表扫描
3. swagger注解
官方描述:Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。
简单来说就是一个可视化的后端接口操作框架。在前后端分离的项目中,后端开发者使用swagger注解来生成接口文档(接口地址、接口参数、接口结果);前端开发者通过此文档来了解后端接口从而完成前端开发。
此项目使用 Knife4j 工具来完成 swagger 的集成
knife4j是什么:百度安全验证
使用参考:knife4j的使用_驴三骑的博客-CSDN博客_knife4j 页面地址
knifej 使用步骤:
① 添加依赖:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
② 创建配置文件:
@Configuration
@EnableSwagger2
@EnableKnife4j
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerConfiguration {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2) // 选择swagger2版本
.apiInfo(apiInfo()) //定义api文档汇总信息
.select()
.apis(RequestHandlerSelectors
.basePackage("com.dave.controller")) // 指定生成api文档的包
.paths(PathSelectors.any()) // 指定所有路径
.build()
;
}
/**
* 构建文档api信息
* @return
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("文档标题") // 文档标题
.contact(new Contact("name", "url", "mail")) //联系人信息
.description("描述") //描述
.version("0.1") //文档版本号
.termsOfServiceUrl("http://localhost:8080") //网站地址
.build();
}
}
③ 使用 swagger 注解
④ 通过访问http://localhost:端口号/doc.html
即可见到knife4j的api文档页面页面
swagger注解
官网WIKI:Annotations 1.5.X · swagger-api/swagger-core Wiki · GitHub
常用注解:
@Api()用于类 | 表示标识这个类是swagger的资源 |
---|---|
@ApiOperation() 用于方法 | 表示一个http请求的操作 |
@ApiImplicitParam() 用于方法 | 表示单独的请求参数 |
@ApiImplicitParams() 用于方法 | 包含多个 @ApiImplicitParam |
@ApiModel() 用于类 | 表示对类进行说明,用于参数用实体类接收 |
@ApiModelProperty() 用于方法,字段 | 表示对model属性的说明或者数据操作更改 |
@RestController
@Api(tags = "用户管理")
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@ApiOperation(value = "登录", notes = "登录接口")
@ApiImplicitParam(name = "condition", value = "用户登录所需信息", dataType = "User")
@PostMapping("/login")
public CommonResult<User> login(@RequestBody @Validated User condition) {
userService.userLogin(condition);
return CommonResult.success(null,"登录成功");
}
}
@Data
@ApiModel(value = "User对象",description = "用户表")
public class User implements Serializable {
private static final long serialVersionUID=1L;
@ApiModelProperty(value = "user主键id")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
}
4.@Validated 注解参数校验
为了软件系统的安全,接口参数除了前端要校验,后端也需要校验。通过引入 Validation 实现使用注解完成参数的校验。
常用注解:
@Validation | 使用在接口参数上,后续注解校验才生效 |
---|---|
@NotNull | 检查带注解的值是否不为null |
@NotEmpty | 检查带注释的元素是否为null或空 |
@NotBlank | 检查带注解的字符序列是否不为null,且长度是否大于0。与@NotEmpty 的区别在于,此约束只能应用于字符串,并且尾部空白被忽略 |
@Range(min, max) | 检查带注解的值是否介于(包括)指定的最小值和最大值之间 |
@Pattern(regex, flag) | 检查带注解的字符串是否与正则表达式regex匹配 |
接口参数使用 @Validated ,后面User实体类中的参数才能检验
@ApiOperation(value = "登录", notes = "登录接口")
@ApiImplicitParam(name = "condition", value = "用户登录所需信息", dataType = "User")
@PostMapping("/login")
public CommonResult<User> login(@RequestBody @Validated User condition) {
userService.userLogin(condition);
return CommonResult.success(null,"登录成功");
}
public class User implements Serializable {
@NotEmpty(message = "密码不能为空")
@Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误")
@TableField(value = "phone_number")
private String phoneNumber;
@NotEmpty(message = "密码不能为空")
private String password;
}
学习博客:@Validated注解详解,分组校验,嵌套校验,@Valid和@Validated 区别,Spring Boot @Validated_昌杰的攻城狮之路的博客-CSDN博客_@validated注解
和:Bean Validation具体描述_BrightZhuz的博客-CSDN博客
5. mybatis plus lamda表达式使用
使用lambdaQuery() 可以调用mybatis plus 中的方法从而可以不用写mapper.xml文件
更重要的是可以通过方法引用的方式来使用实体字段名的操作,避免直接写数据库表字段名时无意的错写
示例:
/**
* 根据条件查询用户数据列表
* @param condition 条件
* @return
*/
@Override
public List<User> selectByCondition(User condition) {
List<User> list = this.lambdaQuery()
.eq(ObjectUtil.isNotNull(condition.getId()),User::getId, condition.getId())
.like(StrUtil.isNotBlank(condition.getName()), User::getName, condition.getName())
return list;
}
参考1:lambda四种表达形式
https://blog.csdn.net/weixin_44472810/article/details/105649901
一、LambdaQueryWrapper<>
二、QueryWrapper<实体>().lambda()
三、Wrappers.<实体>lambdaQuery()
四、LambdaQueryChainWrapper<实体>(xxxxMapper)
参考2:条件构造器
MyBatis-Plus | 最简单的查询操作教程(Lambda)_10000guo的博客-CSDN博客_querywrapper.lambda() .eq
参考3:链式查询
https://blog.csdn.net/qq_25851237/article/details/111713102
6. 工具类的使用:ObjectUtil.isNotEnpty()等
项目中应使用集成的工具类来加速工程的开发,更重要的是使用工具类能规范项目代码,同时减少错误。
/**
* 对象工具类,包括判空、克隆、序列化等操作
*
* @author Looly
*/
public class ObjectUtil {
/**
* 检查对象是否为null
*/
public static boolean isNull(Object obj) {
//noinspection ConstantConditions
return null == obj || obj.equals(null);
}
}
使用示例:
/**
* 根据用户id查询用户信息
* @param id 用户id
* @return 用户信息
*/
@Override
public User selectUserById(Long id) {
User user = this.getById(id);
if (ObjectUtil.isNull(user)){
Asserts.fail("查无此用户");
}
return user;
}
/**
* 集合相关工具类
* <p>
* 此工具方法针对{@link Collection}及其实现类封装的工具。
* <p>
* 由于{@link Collection} 实现了{@link Iterable}接口,因此部分工具此类不提供,而是在{@link IterUtil} 中提供
*
* @author xiaoleilu
* @see IterUtil
* @since 3.1.1
*/
public class CollUtil {
/**
* 集合是否为非空
*/
public static boolean isNotEmpty(Collection<?> collection) {
return false == isEmpty(collection);
}
}
使用示例:
/**
* 增加或者更新一条历史记录
* @param browsingHistory
*/
@Override
@Transactional(propagation = Propagation.REQUIRED,rollbackFor = {Exception.class})
public void add(BrowsingHistory browsingHistory) {
//同一用户访问同一个项目,则只存储最新的一个数据
LambdaQueryWrapper<BrowsingHistory> eq = new LambdaQueryWrapper<BrowsingHistory>()
.eq(BrowsingHistory::getUserId, browsingHistory.getUserId())
.eq(BrowsingHistory::getProjectId, browsingHistory.getProjectId());
List<BrowsingHistory> list = baseMapper.selectList(eq);
if (CollUtil.isNotEmpty(list)){
int i = baseMapper.update(browsingHistory,eq);
if (i==0){
Asserts.fail("历史记录更新失败");
}
}else{
int i = baseMapper.insert(browsingHistory);
if (i==0){
Asserts.fail("历史记录存入失败");
}
}
}
Java中常用的16个工具类:java中常用的16个工具类 - ppjj - 博客园
7.一切要规范
参加此项目后我明白了规范的重要性,规范能大大提高效率和开发质量。一句话就是一切都要规范:数据库要规范、字段要规范、接口要规范、方法要规范、代码要规范,要把事情做仔细。
本项目是按照阿里规范开发的:《阿里巴巴Java开发手册1.7.0(嵩山版)》规范包含以下内容:
这次开发中查看最多的是关于MySQL数据库的部分,在前面2.数据库建表相关已经提及过。IDEA也添加了阿里代码规范插件:
此外,项目中要规范代码的逻辑结构:
①:controller层不要有具体的业务数据操作逻辑,仅调用service中的方法。这样能降低代码的耦合度,其他controller也能通过调用service中的方法实现业务数据操作。
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
public CommonResult<User> login(@RequestBody @Validated User condition) {
userService.userLogin(condition);
return CommonResult.success(null,"登录成功");
}
②:service层报错,全局来拦截错误,根据错误来进行相应操作,简化逻辑代码(例如下面的)
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
public void userLogin(User condition) {
User user = selectByPhoneNumber(condition.getPhoneNumber());
boolean flag = condition.getPassword().equals(user.getPassword());
if (!flag){
Asserts.fail("登录失败");
}
}
}
8.IDEA + Gitee使用
之前做项目都是一个人做,所以没用过 gitee 来做协同开发。刚上手时有点懵,大概记录一下:
①拉取项目:
首先创建本地仓库:VCS -> import into Version Control -> Create Git Repository
然后clone:VCS -> Get from Version Control
②提交项目:
需要先提交到本地:
再push到远程仓库:
参考学习:码云(gitee)多人开发代码提交流程说明_Baymax_PP的博客-CSDN博客_gitee私有项目可以让比人提交吗
和:https://blog.csdn.net/weixin_45606067/article/details/109536927
9. 主从复制+多数据源+读写分离
Spring Boot + MyBatis Plus + Druid 实现多数据源读写分离
(1)只实现多数据源发现的问题
最开始的时候根据博客配置多数据源:(springboot+mybatis+mysql+yml配置多数据源)
springboot+mybatis+mysql+yml配置多数据源_tq_theSuperMan的博客-CSDN博客_springboot yml mysql
配置了两个目录的Mapper文件:
在 Service 使用 dao 的时候就会出现两个 dao 的问题
但在一个 service 里面是有写有读的,引入多数据源也是为了实现读写分离,在此无法同时引入两个dao,所以这个方案在此并不适用
(2)多数据源实现读写分离
此博客提供了四种方案:(Spring+MyBatis实现数据库读写分离方案)
Spring+MyBatis实现数据库读写分离方案 - 简书
方案四:
如果你的后台结构是spring+mybatis,可以通过spring的AbstractRoutingDataSource和mybatis Plugin拦截器实现非常友好的读写分离,原有代码不需要任何改变。
参考博客实现方案四:(SpringBoot+MybatisPlus多数据源配置,主从库读写分离完整讲解)
SpringBoot+MybatisPlus多数据源配置,主从库读写分离完整讲解_FJekin的博客-CSDN博客_mybatis plus 主从
①配置文件application.yaml
spring:
application:
name: kkb-portal
datasource:
slave:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3308/kkb-parent-v2?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123465
filters: mergeStat
on-off: true
master:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
jdbc-url: jdbc:mysql://127.0.0.1:3307/kkb-parent-v2?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123465
filters: mergeStat
druid:
initial-size: 5 #连接池初始化大小
min-idle: 10 #最小空闲连接数
max-active: 20 #最大连接数
PS:因为此方案中
Master 数据源连接池采用 HikariDataSource
Slave 数据源连接池采用 DruidDataSource
所以 master 的 url 要写成 jdbc-url,否则会报如下错误:
参考如下链接解决:
方案目录结构:
②DruidProperties Druid属性配置
package com.kkb.kkbportal.config;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* <p>数据库数据源配置</p>
*/
@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.slave")
public class DruidProperties {
private String url;
private String username;
private String password;
private String driverClassName;
private Integer initialSize = 2;
private Integer minIdle = 1;
private Integer maxActive = 20;
private Integer maxWait = 60000;
private Integer timeBetweenEvictionRunsMillis = 60000;
private Integer minEvictableIdleTimeMillis = 300000;
private String validationQuery = "SELECT 'x' from dual";
private Boolean testWhileIdle = true;
private Boolean testOnBorrow = false;
private Boolean testOnReturn = false;
private Boolean poolPreparedStatements = true;
private Integer maxPoolPreparedStatementPerConnectionSize = 20;
private String filters = "stat";
private Boolean onOff = false;
public void coinfig(DruidDataSource dataSource) {
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverClassName);
//定义初始连接数
dataSource.setInitialSize(initialSize);
//最小空闲
dataSource.setMinIdle(minIdle);
//定义最大连接数
dataSource.setMaxActive(maxActive);
//最长等待时间
dataSource.setMaxWait(maxWait);
// 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
// 配置一个连接在池中最小生存的时间,单位是毫秒
dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
dataSource.setValidationQuery(validationQuery);
dataSource.setTestWhileIdle(testWhileIdle);
dataSource.setTestOnBorrow(testOnBorrow);
dataSource.setTestOnReturn(testOnReturn);
// 打开PSCache,并且指定每个连接上PSCache的大小
dataSource.setPoolPreparedStatements(poolPreparedStatements);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
try {
dataSource.setFilters(filters);
} catch (Exception e) {
e.printStackTrace();
}
}
}
③DatabaseType 数据源类型
package com.kkb.kkbportal.dynamic;
public enum DatabaseType {
master("write"),slave("read");
private String name;
private DatabaseType(String name) {
this.name = name();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
④ DatabaseContextHolder 数据源类型处理
package com.kkb.kkbportal.dynamic;
public class DatabaseContextHolder {
private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>();
public static void setDatabaseType(DatabaseType type) {
contextHolder.set(type);
}
public static DatabaseType getDatabaseType() {
return contextHolder.get();
}
}
⑤ DynamicDataSource 数据源选择记录
package com.kkb.kkbportal.dynamic;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
DatabaseType type = DatabaseContextHolder.getDatabaseType();
if(type == null) {
logger.info("========= dataSource ==========" + DatabaseType.slave.name());
return DatabaseType.slave.name();
}
logger.info("========= dataSource ==========" + type);
return type;
}
}
⑥DatabasePlugin 拦截器:动态选择数据源
package com.kkb.kkbportal.dynamic;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class }) })
public class DatabasePlugin implements Interceptor {
protected static final Logger logger = LoggerFactory.getLogger(DatabasePlugin.class);
private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";
private static final Map<String, DatabaseType> cacheMap = new ConcurrentHashMap<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
if(!synchronizationActive) {
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
DatabaseType databaseType = null;
if((databaseType = cacheMap.get(ms.getId())) == null) {
//读方法
if(ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
//!selectKey 为自增id查询主键(SELECT LAST_INSERT_ID() )方法,使用主库
if(ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
databaseType = DatabaseType.master;
} else {
BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
if(sql.matches(REGEX)) {
databaseType = DatabaseType.master;
} else {
databaseType = DatabaseType.slave;
}
}
}else{
databaseType = DatabaseType.master;
}
logger.warn("设置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), databaseType.name(), ms.getSqlCommandType().name());
cacheMap.put(ms.getId(), databaseType);
}
DatabaseContextHolder.setDatabaseType(databaseType);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
}
}
⑦MybatisPlusConfig 数据源配置
package com.kkb.kkbportal.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.github.pagehelper.PageInterceptor;
import com.kkb.kkbportal.dynamic.DatabasePlugin;
import com.kkb.kkbportal.dynamic.DatabaseType;
import com.kkb.kkbportal.dynamic.DynamicDataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName MybatisPlusConfig
* @Description MybatisPlus配置类
* @Author mzj
* @Date 2021/8/13
**/
@Configuration
@MapperScan(basePackages = "com.kkb.kkbportal.dao")
public class MybatisPlusConfig {
@Autowired
DruidProperties druidProperties;
/**
* mp分页拦截器
* @return PageInterceptor
*/
@Bean
public PageInterceptor pageInterceptor() {
return new PageInterceptor();
}
/**
* 配置 master 数据源
* 根据 yaml 中的 spring.datasource.master
* @return masterDataSource
*/
@Bean(name = "masterDataSource")
@Qualifier("masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 配置 slave 数据源
* 根据之前配置的 DruidProperties 来配置
* @return slaveDataSource
*/
@Bean(name = "slaveDataSource")
@Qualifier("slaveDataSource")
public DataSource slaveDataSource() {
DruidDataSource dataSource = new DruidDataSource();
druidProperties.coinfig(dataSource);
return dataSource;
}
/**
* 构造多数据源连接池
* Master 数据源连接池采用 HikariDataSource
* Slave 数据源连接池采用 DruidDataSource
* @param master
* @param slave
* @return
*/
@Bean
@Primary
public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DatabaseType.master, master);
targetDataSources.put(DatabaseType.slave, slave);
DynamicDataSource dataSource = new DynamicDataSource();
// 该方法是AbstractRoutingDataSource的方法
dataSource.setTargetDataSources(targetDataSources);
// 默认的datasource设置为myTestDbDataSourcereturn dataSource;
dataSource.setDefaultTargetDataSource(slave);
return dataSource;
}
@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactory(@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave) throws Exception {
MybatisSqlSessionFactoryBean fb = new MybatisSqlSessionFactoryBean();
fb.setDataSource(this.dataSource(master, slave));
// 是否启动多数据源配置,目的是方便多环境下在本地环境调试,不影响其他环境
if (druidProperties.getOnOff() == true) {
fb.setPlugins(new Interceptor[]{new DatabasePlugin()});
}
return fb;
}
}
(3)效果
(4)问题
①插入相同数据不是返回失败而是变成查询,目前还不知道什么问题
②待优化
目前仅参考博客实现了功能,但感觉还有很多地方需要优化,我还没完全搞懂是怎么实现的,需要后续优化
5、MyBatis Plus 多数据源官网配置
官网:https://mp.baomidou.com/guide/dynamic-datasource.html#%E6%96%87%E6%A1%A3-documentation
弄了半天发现官网里面有多数据源的配置和使用@DS注解实现读写分离
使用方法:
① 引入dynamic-datasource-spring-boot-starter
②配置数据源
③使用 @DS 切换数据源
@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
注解 | 结果 |
---|---|
没有@DS | 默认数据源 |
@DS("dsName") | dsName可以为组名也可以为具体某个库的名称 |
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
四、项目进行期间读书笔记
项目开始的时候选择的书是《Java核心技术整理》,因为时间有限所以只看了以下内容,并进行了整理。读书这个任务我虽然不能完全做到,但我还是十分认同的。每天抽出时间来看书学习真的很重要也不容易,但知识就是这样积累起来的,希望之后能鞭策一下自己继续坚持读书学习。
1、MySQL部分
MySQL存储引擎(表处理器)
功能:接收上层指令,让后操作表中数据。
主要的存储引擎:MyISAM、InnoDB、Memory(存储结构不同,存取算法不同)。
MyISAM 和 InnoDB 的区别:
①支持事务:MyISAM不支持;InnoDB支持;
②存储结构:MyISAM存成三个文件:.frm(存储表结构).MYD(存储数据).MYI(存储索引);InnoDB存成两种文件:.frm(存储表结构).ibd(存储数据和索引【可多个】);
③表锁差异:MyISAM只支持表级锁;InnoDB支持事务和行级锁;
④表主键:MyISAM允许没有任何索引和主键的表存在;InnoDB会自动生成一个6字节主键
⑤CRUD操作:MyISAM如执行大量SELECT操作更优;如执行大量INSERT或UPDATE操作更优
⑥外键:MyISAM不支持;InnoDB支持
MySQL记录存储(页结构)
页头:记录页面的控制信息,共56字节,包括页左右兄弟指针、页面空间使用情况
虚记录:①最大虚记录:比页内最大主键还打;②最小虚记录:比页内最小主键还小
记录堆:行记录存储区,分为有效记录和已删除记录两种
自由空间列表:已删除记录组成的链表
未分配空间:页面未使用的存储空间
Slot区:槽(实现定长)
页尾:页面最后部分,占8个字节,主要存储页面的校验信息
基于Slot实现二分查找效果:因为页内数据存储结构是链表,链表结构不具备有序定长的结构,所以无法实现二分查找。MySQL解决方案是:①使用Slot-list实现有序;②每个Slot定长;③在每个定长的Slot里遍历。近似达到二分查找的效果
InnoDB内存管理:
Buffer pool:预分配内存池
page:Buffer pool的最小单位
Free list:空闲page组成的链表
Flush list:脏页链表
page hash表:维护内存page和文件page映射关系
LRU:内存淘汰算法
LRU:访问热度高的数据放在头部,热度低的自然移动到尾部。新数据进来时,淘汰掉尾部热度低的数据。
(避免热度数据被淘汰的Mysql解决方案:两个LRU表)
索引:
提高数据检索效率,降低数据库IO成本(理解为:快速查找排好序的一种数据结构)
MySQL索引结构为 B+树:
①B+树只有叶子节点存数据;②非叶子节点起索引作用;③所有叶子节点用链表相连
优势: 减少磁盘IO次数,读写代价低;叶子节点高度相同,速度更稳定;B+树有顺序ID,效率更快
分类:聚簇索引、二级索引、联合索引
聚簇索引:叶子节点存储行记录(InnoDB必须有且只有一个聚簇索引)
二级索引:叶子节点存储主键PK值
【回表】(二级索引的查询过程:①先通过二级索引定位到主键值;②再通过聚簇索引定位到行记录)
联合索引(覆盖索引):包含主键在内的多个字段组成的索引,只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快
2、JUC并发编程【java.util.concurrent】
Volatile关键字:
①能够保证可见性和有序性
②但不能保证原子性
③禁止指令重排(内存屏障(内存栅栏))
CAS:【CompareAndSwapInt】
CAS是系统原语,属于操作系统用语规范,由若干指令组成。用于完成某个功能的一个过程,并且原语的执行是连续的,不允许中断;体现在Java中就是sum.misc.Unsafa类中的各种方法。
CAS是一条CPU原子指令,不会造成所谓的数据不一致问题。
缺点:①自旋锁,一直循环,开销大
②只有一个共享变量时能保证原子性,多个变量时需要加锁保证原子性
③引发ABA问题
ABA问题:CAS实现需要取出内存某刻数据在当下时刻比较替换数据,会带来时间差,时间差会带来问题
CAS只注重头尾,头尾一致就接受,但有需求要注重过程,中间不能修改(AtomicReference 原子引用)
解决方案:AtomicStampedReferce类中引入版本“Stamp”
3、Java基础
Java 面向对象具体是什么:
①面向对象的三大特性:封装、继承、多态
②封装:就是把具体事务抽象化,仅对外提供接口
③继承:就是可在不改变原有类的情况下,实现了原有类的所有功能,并能根据实际修改拓展
④多态:就是调用同样的方法,能有不同响应。具体又分为编译时的多态:方法重载(overload)和运行时的多态:方法重写(override)
⑤简单来说面向对象就是把一个具体过程抽象成一个个对象,之后根据不同的需求使用和修改对象,从而达到高复用性、高可用性和低耦合读。
JVM:①类加载;②内存模型;③GC
类加载:由类加载器classloader和它的子类实现【加载->连接->初始化】
首先是加载阶段,把类.class文件读入内存,然后产生对应的class对象;
接着就是连接阶段,此阶段完成校验、准备、解析工作。具体是为静态变量分配内存并设置默认初始值;将符号引用替换为直接引用;
最后就是初始化阶段:如果类存在父类且没有初始化就先初始化父类,再依次初始化(【双亲委派】请求委派给父类加载器加载类)
加载器:根加载器(Bootstrap);扩展加载器(Extension);应用加载器(Application)
内存模型:
PC寄存器:记录字节指令地址
Java虚拟堆:基本数据类型、对象引用、方法出口
本地方法堆:基本数据类型、对象引用、方法出口(服务于本地)
Java堆:对象实例、数组
方法区(常量池):已被加载类信息、常量、静态变量
新生代(Young):老生代(Old)=1:2
【新生代】Eden:Survivor(from:to)=8:1:1
垃圾收集器:①CMS收集器:标记清除算法;②GI收集器:标记整理算法
完整GC过程:
Eden区满了触发一次Minor GC;
存活下来的纳入到Survivor区,年龄+1;
年龄超过15晋升到老生代区;
老生代区满了触发一次Major GC(Full GC)
五、总结
本次项目开发参与度不算太高(这点需要检讨),但也还是有不少的收获。至少通过项目从0到1的开发过程,对项目的具体实施落地流程有了一个初步的认识,对项目开发流程管理也有了一定的了解。同时项目开发过程中师哥作为业内前辈给我们分享了不少人生阅历,为我这个门外汉提供了不少对此行业的认识和个人认识的建议。
首先因为个人原因没有参与架构开发,也没有主动承担组长等促进项目开发的角色。只是按分配完成任务,不够积极主动是这次项目我觉得需要检讨的地方。师哥说过的我很在意的一点:一起工作的人希望是有能力和性格好的人。因为和有能力的人一起干活会很轻松同时也能学到东西;而和性格好的人一起干活能开开心心的把活干完。就是说一个人应该有一定的能力和相对好的性格,这两点将会对以后的职业生涯有深远的影响。我在意的是我这两点都不及格,所以接下来我应该要好好想想,想想怎么往这两个方向努力。
接着就是沟通,项目开发很重要的一点是沟通,协同开发这一点显得更重要。因为从确认需求到模块设计再到代码实现和测试,都需要经过多次的讨论、实现、修改、完善。所以需要各位成员及时沟通,及时发现问题和解决问题,缺乏沟通就是闭门造车,造出来的东西也是不能用的。这次开发我就面临了这个问题,个人比较内向加上没有人引导导致了进度跟不上其它组,这点希望在以后工作中能重视起来并解决。
还有就是通过项目,我至少算是有点入门了。因为在开发过程中,其实每次分配完任务后我都是有点小懵的,有满腔的热血但却不知道应该具体做什么。比如说让我们看PRD文档整理需求,看是看完了,也整理了,但就是不知道具体整理是干什么用的。直到看到了其它组整理的任务文档,看到了具体的数据表设计,我才知道了为什么要整理需求文档。每一阶段要干什么都是在下一阶段开始时才真正搞明白。这应该就是学习吧,通过这次参与不能说我懂了,但至少我是了解了,不会再发蒙。师哥也说参与真实项目重点不在写代码上,而是通过项目了解项目的开发流程,学习例如RBAC这样的通用技术。同时要通过项目找到方向,找到学习和努力的方向。通过项目发现自己和其他人和行业的差距,从而继续努力。
最后就是要感谢师哥和其他/她小伙伴这一个多月来的陪伴,接下来要做的就是好好消化项目内容和继续努力。