一.前言
1.1课程大致包含技术
首先选这套课的目的是 包含了我所学的大部分技术
比如springboot ssm redis kafka flink clickhouse 等
1.2 外界客观原因
就业环境一般 目前来看暂时还没但是有后续潜在的毕业 或者 离职
1.3技术追求
个人的技术追求暂时是在技术总监 技术架构
1.4通用性
项目具备一定的通用性 中大厂基本用得到
二.项目简介 亮点与架构
2.1亮点
2.2技术栈
2.3各微服务模块概览
2.4各模块对应关键解决方案
2.5流量包业务模型
三.服务器搭架子
3.1.服务器
我这是天翼云买了三台服务器 四核八G 配置最少要两核8G 因为后续要运行的程序会比较多 比较吃内存 最好centos7.8 64位 以免不兼容
3.2docker环境搭建
运用以下命令搭建好docker环境
yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
yum list docker-ce --showduplicates | sort -r
yum -y install docker-ce-20.10.10-3.el7
docker -v
systemctl start docker
systemctl status docker
docker info
启动使⽤Docker
systemctl start docker #运⾏Docker守护进程
systemctl stop docker #停⽌Docker守护进程
systemctl restart docker #启Docker守护进程设置开机自启动docker
systemctl enable dockerdocker ps查看容器
docker stop 容器id修改镜像仓库
vim /etc/docker/daemon.json
#改为下⾯内容,然后启docker
{
"debug":true,"experimental":true,
"registry-mirrors":
["https://pb5bklzr.mirror.aliyuncs.com","https://hub -
mirror.c.163.com","https://docker.mirrors.ustc.edu.c n"]
}注意:不使⽤1.13.1版本,该版本在jenkins使⽤docker命令时会说找不到配置⽂件!
3.3安装mysql8
#⽹络安全组记得开放端⼝ 3306
3.3.1启动mysql容器
docker run \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=你自己的密码 \
-v /home/data/mysql/data:/var/lib/mysql:rw \
-v /etc/localtime:/etc/localtime:ro \
--name xdclass_mysql \
--restart=always \
-d mysql:8.0
3.3.2客户端连接报错解决
踩坑:
这里使用客户端连接mysql报错了:caching_sha2_password can't be loaded
解决方式如下
docker exec -it docker容器id bash
mysql -hlocalhost -uroot -p892660rG -P3306
use mysql;
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '892660rG';
FLUSH PRIVILEGES;
3.3.3设置最大连接数
3.4安装redis6
#⽹络安全组记得开放端⼝ 6379
docker run -itd --name xdclass-redis -p 6379:6379 -v /mydata/redis/data:/data redis:6.2.4 --requirepass 你的密码
没配置密码进控制台操作的时候会报错
执行auth 你的密码即可
3.5安装nacos2
3.5.1建表
需要把配置持久化 所以要先创建必须的表
创建nacos_config库
3.5.2运行容器
记住要先创建上面的库表 不然启动报错
#⽹络安全组记得开放端⼝ 805
docker run -d \
-e NACOS_AUTH_ENABLE=true \
-e MODE=standalone \
-e JVM_XMS=128m \
-e JVM_XMX=512m \
-e JVM_XMN=512m \
-p 805:8848 \
-e SPRING_DATASOURCE_PLATFORM=mysql \
-e MYSQL_SERVICE_HOST=156.224.26.212 \
-e MYSQL_SERVICE_PORT=800 \
-e MYSQL_SERVICE_USER=root \
-e MYSQL_SERVICE_PASSWORD=892660rG \
-e MYSQL_SERVICE_DB_NAME=nacos_config \
-e MYSQL_SERVICE_DB_PARAM='characterEncoding=utf8&con nectTimeout=10000&socketTimeout=30000&autoReconnec t=true&useSSL=false' \
--restart=always \
--privileged=true \
-v /home/data/nacos/logs:/home/nacos/logs \
--name xdclass_nacos \
nacos/nacos-server:2.0.2
默认账号密码是 nacos nacos
通过以下url访问nacos控制台首页
ip:805/nacos
3.6 安装RabbitMQ
docker run -d --name xdclass_rabbit -e \
RABBITMQ_DEFAULT_USER=admin -e \
RABBITMQ_DEFAULT_PASS=你的密码 -p 807:15672 -p 808:5672 rabbitmq:3.8.15-management
访问首页 服务器ip:15672 可以看到 页面如下
# ⽹络安全组记得开放端⼝4369 erlang 发现⼝5672 client 端通信⼝15672 管理界⾯ ui 端⼝25672 server 间内部通信⼝
四.代码搭架子
4.1创建maven project
name: dcloud-short-link
踩坑:
我使用的 idea2023版本 最开始通过下面Maven Archetype 来new project 不下载jar包,
还一直以为maven配置问题,后面换成New Project 就好了
4.2创建 maven module
然后在该maven project下创建以下需要的maven module
dcloud-account clodud-app dcloud-common
dcloud-data dcloud-gateway dcloud-link dcloud-shop
4.3配置pom.xml
4.3.1父项目的pom.xml
内容如下链接
https://blog.youkuaiyun.com/JavaCoder_juejue/article/details/131757162
它要进行依赖管理,就是其它的子模块需要的一些依赖直接版本给他确定好
4.3.2 dcloud-common的pom.xml
内容如下链接
https://blog.youkuaiyun.com/JavaCoder_juejue/article/details/131757214
这个是一些通用的依赖
4.4 常用工具类
内容如下链接
https://blog.youkuaiyun.com/JavaCoder_juejue/article/details/131757258
这个是一些常用的类 比如json 时间 处理 自定义异常捕获等工具类
4.5开发规范
POJO(Plain Ordinary Java Object): 在⼿册中, POJO 专指只有 setter / getter / toString 的简单类,包括 DO/DTO/BO/VO等 , 禁⽌命名成 xxxPOJO
4.6接入短信服务
发送短信我们这里用的测试账号 免费五条 只能发送数字 有效期一个月
加好templateid与appcode 配置
实现代码
相关配置属性类
package net.xdclass.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@ConfigurationProperties(prefix = "sms")
@Configuration
@Data
public class SmsConfig {
private String templateId;
private String appCode;
}
封装一个发送http请求的bean
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory requestFactory){
return new RestTemplate(requestFactory);
}
@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(10000);
factory.setConnectTimeout(10000);
return factory;
}
}
发送短信的主要bean
@Component
@Slf4j
public class SmsComponent {
/**
* 发送地址
*/
private static final String URL_TEMPLATE = "https://jmsms.market.alicloudapi.com/sms/send?mobile=%s&templateId=%s&value=%s";
@Autowired
private RestTemplate restTemplate;
@Autowired
private SmsConfig smsConfig;
/**
* 发送短信验证码
* @param to
* @param templateId
* @param value
*/
public void send(String to,String templateId,String value){
String url = String.format(URL_TEMPLATE,to,templateId,value);
HttpHeaders headers = new HttpHeaders();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.set("Authorization","APPCODE "+smsConfig.getAppCode());
HttpEntity entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
log.info("url={},body={}",url,response.getBody());
if(response.getStatusCode().is2xxSuccessful()){
log.info("发送短信验证码成功");
}else {
log.error("发送短信验证码失败:{}",response.getBody());
}
}
}
测试发送短信
注入该对象 调用send方法即可
@SpringBootTest(classes = AccountApplication.class)
@RunWith(SpringRunner.class)
@Slf4j
public class SmsTest {
@Autowired
SmsComponent smsComponent;
@Autowired
SmsConfig smsConfig;
@Test
public void smsTest() {
smsComponent.send("16621177280",smsConfig.getTemplateId(),"888888");
}
}
4.7池化+异步 进行接口调优
4.7.1异化整合自定义线程池
首先 我们假设这个短信发送接口调用非常频繁 那么为了提升接口的相应速度
可以做异化 也就是加上 @Async注解 (需要在该类或者启动类上加上@EnableAsync才能生效)
该注解的原理是 动态代理整了一个线程池 线程池的 队列容量为默认的 太大 在极端情况下
流量暴涨或者别人恶意压测攻击时 会导致OOM
所以我们进行了自定义线程池,异化整合自定义线程池代码如下
这里主要的就是我们将队列的容量从原本默认的整形最大值改成了10000
@Configuration
public class ThreadPoolTaskConfig {
@Bean("threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(8);
threadPoolTaskExecutor.setMaxPoolSize(32);
threadPoolTaskExecutor.setKeepAliveSeconds(30);
threadPoolTaskExecutor.setQueueCapacity(10000);
threadPoolTaskExecutor.setThreadNamePrefix("juege-thread-");
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}
4.7.2http连接池化
就是修改代码RestTemplateConfig类, 优化原理就是原本的http连接是通过SimpleClientHttpRequestFactory创建(已注释),每来一个请求就新创建一个,这里做到连接的复用
4.8整合redis
主要是初始化 redis相关配置 比如用户名密码 db啥的(默认是0库)
然后是序列化方式 一般key为StringRedisSerializer value为Jackson2JsonRedisSerializer
具体步骤如下
4.9图形验证码+使用redis保存
先引入依赖由于父项目pom上做了版本管理 具体可看 4.3.1 子模块只需要不带版本引入即可
<!--验证码kaptcha依赖包-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>kaptcha-spring-boot-starter</artifactId>
</dependency>
然后redis主要保存的是验证码文本,key的生成方式这里使用的
模块名信息:接口名信息:ip+useragent的MD5加密
@RestController
@RequestMapping("/api/v1/account/notify")
@Slf4j
public class NotifyController {
@Autowired
private Producer captchaProducer;
@Autowired
private NotifyService notifyService;
@Autowired
private RedisTemplate redisTemplate;
private final long CAPTCHA_KEY_TIMEOUT = 1000*10*60;
/**
* 生成验证码
* @param request
* @param response
*/
@GetMapping("captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response){
String captchaText = captchaProducer.createText();
log.info("验证码内容:{}",captchaText);
//存储redis,配置过期时间
String captchaKey = getCaptchaKey(request);
redisTemplate.opsForValue().set(captchaKey,captchaText,CAPTCHA_KEY_TIMEOUT,TimeUnit.MILLISECONDS);
Object o = redisTemplate.opsForValue().get(captchaKey);
BufferedImage bufferedImage = captchaProducer.createImage(captchaText);
try {
ServletOutputStream outputStream = response.getOutputStream();
ImageIO.write(bufferedImage,"jpg",outputStream);
outputStream.flush();
outputStream.close();
} catch (IOException e) {
log.error("获取流出错:{}",e.getMessage());
}
}
private String getCaptchaKey(HttpServletRequest request) {
String ip = CommonUtil.getIpAddr(request);
String userAgent = request.getHeader("User-Agent");
String captchaKey = "account-service:captcha:"+CommonUtil.MD5(ip+userAgent);
return captchaKey;
}
}
4.10短信发送接口实现及防刷
代码如下:实现短信发送 然后防刷:一分钟内只能发一次短信 必须后台做 因为别人可能绕过前端
4.11登录注册及拦截器相关
登录注册的逻辑这里就不多说了
JWT token相关类
package net.xdclass.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import net.xdclass.model.LoginUser;
import java.util.Date;
/**
* 小滴课堂,愿景:让技术不再难学
*
* @Description
* @Author 二当家小D
* @Remark 有问题直接联系我,源码-笔记-技术交流群
* @Version 1.0
**/
@Slf4j
public class JWTUtil {
/**
* 主题
*/
private static final String SUBJECT = "xdclass";
/**
* 加密密钥
*/
private static final String SECRET = "xdclass.net168";
/**
* 令牌前缀
*/
private static final String TOKNE_PREFIX = "dcloud-link";
/**
* token过期时间,7天
*/
private static final long EXPIRED = 1000 * 60 * 60 * 24 * 7;
/**
* 生成token
*
* @param loginUser
* @return
*/
public static String geneJsonWebTokne(LoginUser loginUser) {
if (loginUser == null) {
throw new NullPointerException("对象为空");
}
String token = Jwts.builder().setSubject(SUBJECT)
//配置payload
.claim("head_img", loginUser.getHeadImg())
.claim("account_no", loginUser.getAccountNo())
.claim("username", loginUser.getUsername())
.claim("mail", loginUser.getMail())
.claim("phone", loginUser.getPhone())
.claim("auth", loginUser.getAuth())
.setIssuedAt(new Date())
.setExpiration(new Date(CommonUtil.getCurrentTimestamp() + EXPIRED))
.signWith(SignatureAlgorithm.HS256, SECRET).compact();
token = TOKNE_PREFIX + token;
return token;
}
/**
* 解密jwt
* @param token
* @return
*/
public static Claims checkJWT(String token) {
try {
final Claims claims = Jwts.parser().setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKNE_PREFIX, "")).getBody();
return claims;
} catch (Exception e) {
log.error("jwt 解密失败");
return null;
}
}
}
登录拦截器相关类 包含把 从header或者param中的token中 解析出 用户信息然后放到 threadLocal 方便后续 这个线程中需要的获取
package net.xdclass.interceptor;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import net.xdclass.enums.BizCodeEnum;
import net.xdclass.model.LoginUser;
import net.xdclass.util.CommonUtil;
import net.xdclass.util.JWTUtil;
import net.xdclass.util.JsonData;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 小滴课堂,愿景:让技术不再难学
*
* @Description
* @Author 二当家小D
* @Remark 有问题直接联系我,源码-笔记-技术交流群
* @Version 1.0
**/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (HttpMethod.OPTIONS.toString().equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpStatus.NO_CONTENT.value());
return true;
}
String accessToken = request.getHeader("token");
if (StringUtils.isBlank(accessToken)) {
accessToken = request.getParameter("token");
}
if (StringUtils.isNotBlank(accessToken)) {
Claims claims = JWTUtil.checkJWT(accessToken);
if (claims == null) {
//未登录
CommonUtil.sendJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
return false;
}
Long accountNo = Long.parseLong(claims.get("account_no").toString());
String headImg = (String) claims.get("head_img");
String username = (String) claims.get("username");
String mail = (String) claims.get("mail");
String phone = (String) claims.get("phone");
String auth = (String) claims.get("auth");
LoginUser loginUser = LoginUser.builder()
.accountNo(accountNo)
.auth(auth)
.phone(phone)
.headImg(headImg)
.mail(mail)
.username(username)
.build();
//request.setAttribute("loginUser",loginUser);/
//通过threadlocal
threadLocal.set(loginUser);
return true;
}
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
threadLocal.remove();
}
}
以及对某些请求路径的放行 或禁止访问
package net.xdclass.config;
import net.xdclass.interceptor.LoginInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/api/account/*/**", "/api/traffic/*/**")
.excludePathPatterns("/api/account/*/register","/api/account/*/upload","/api/account/*/login",
"/api/notify/v1/captcha","/api/notify/*/send_code");
WebMvcConfigurer.super.addInterceptors(registry);
}
}
4.12整合shardingJDBC实现分表
短链平台当流量大了之后数据也多了 需要做分表