springboot第70集:字节跳动后端三面经,一文让你走出微服务迷雾架构周刊

科技   其他   2024-04-26 21:48   广东  

创建一个使用Kubernetes (K8s) 和 Jenkins 来自动化 GitLab 前端项目打包的CI/CD流水线,需要配置多个组件。下面,我将概述一个基本的设置步骤和示例脚本,以帮助你理解如何使用这些工具整合一个自动化流程。

前提条件

确保你已经有:

  1. Kubernetes 集群:用于部署 Jenkins 和可能的其他相关服务。

  2. Jenkins:安装在 Kubernetes 集群上,并配置好相关插件(例如 GitLab 插件、Kubernetes 插件等)。

  3. GitLab:托管你的前端代码。

步骤一:在 Kubernetes 上部署 Jenkins

首先,你需要在 Kubernetes 集群上部署 Jenkins。你可以使用 Helm chart 来简化部署过程。以下是一个基本的 Helm 安装命令:

helm repo add jenkins https://charts.jenkins.io helm repo update helm install jenkins jenkins/jenkins

helm repo add jenkins https://charts.jenkins.io
helm repo update
helm install jenkins jenkins/jenkins

确保配置 Jenkins 的持久卷和服务,以便它能稳定运行并保持数据。

步骤二:配置 Jenkins 与 GitLab 的集成

在 Jenkins 中安装并配置 GitLab 插件:

  1. 在 Jenkins 中安装 GitLab Plugin

  2. 在 GitLab 中创建一个具有适当权限的访问令牌。

  3. 在 Jenkins 的系统配置中配置 GitLab 连接,输入 GitLab 的URL和创建的访问令牌。

步骤三:创建 Jenkins Pipeline

在 Jenkins 中创建一个新的 Pipeline 项目,你可以使用 Jenkinsfile 来定义流水线。这个 Jenkinsfile 需要放在你的前端项目的根目录下:

pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
spec:
containers:
- name: node
image: node:14-buster
command:
- cat
tty: true
"""
}
}
stages {
stage('Checkout') {
steps {
git branch: 'main', url: 'https://gitlab.example.com/your-username/your-project.git'
}
}
stage('Install') {
steps {
script {
container('node') {
sh 'yarn install'
}
}
}
}
stage('Build') {
steps {
script {
container('node') {
sh 'yarn build'
}
}
}
}
stage('Deploy') {
steps {
// 添加部署脚本或 Kubernetes 部署命令
}
}
}
}

说明

  • Jenkins Pipeline 使用 Kubernetes 插件在 Kubernetes Pod 内运行。

  • 使用 Node.js 容器来执行前端构建任务(例如 yarn install 和 yarn build)。

  • 这个示例基于 Node.js 容器,你需要确保镜像版本与你的项目兼容。

  • 根据需要调整 GitLab 仓库 URL 和分支。

步骤四:触发器和部署

  • 在 Jenkins 中配置触发器,以便在 GitLab 中推送更新时自动启动构建。

  • 在 "Deploy" 阶段,你可以添加部署到 Kubernetes 的步骤,如使用 kubectl 命令或 Helm chart。

在GitLab CI/CD流水线中,当你使用Yarn来安装依赖,这些依赖通常会被安装在项目的node_modules目录下。这是Node.js和Yarn的标准行为。GitLab CI/CD流水线使用的是GitLab Runner来执行定义在.gitlab-ci.yml文件中的作业。

GitLab Runner工作目录

GitLab Runner通常会为每一个CI/CD作业创建一个隔离的环境,这通常是在Runner的系统上的一个临时目录。这个目录通常会在作业完成后被清理掉,除非你特别配置了缓存或者工件(artifacts)来存储这些文件。

使用 npm 安装 CLI 到开发依赖

$ npm install --save-dev @tarojs/cli@1.3.9

使用 yarn 安装 CLI 到开发依赖

$ yarn add --dev @tarojs/cli@1.3.9

使用 cnpm 安装 CLI 到开发依赖

$ cnpm install --save-dev @tarojs/cli@1.3.9

  1. 简化代码:将操作合并到单个流操作中,减少了重复的代码。

  2. 使用 flatMap() :使用 flatMap() 处理 feeRuleIds 字段,避免了在循环中手动拆分字符串。

  3. 一致的命名风格:使用了统一的命名风格(驼峰命名法),提高了代码的一致性和可读性。

  4. 使用 StringBuilder 替代字符串拼接,提高效率。

npm install echarts --save

// echarts-4.0
import echarts from 'echarts'
Vue.prototype.$echarts = echarts

// 引入echarts-5.0
import * as echarts from 'echarts'
Vue.prototype.$echarts = echarts

let myChart = this.$echarts.init(document.getElementById('map'))

import * as echarts from "echarts";

let myChart = echarts.init(document.getElementById("map"));

import "../../node_modules/echarts/map/js/china"; // 引入中国地图 注意是4.0才有 不然会报错

methods: {
//中国地图
chinaMap() {
let myChart = echarts.init(document.getElementById("map")); // 初始echarts
let option = {
// 绘制地图
tooltip: {
// 提示框信息配置
// triggerOn: "click", // 触发方式
trigger: "item", // 对象
// backgroundColor:'#6950a1',
formatter: (params) => {
// 格式化提示框信息。 若要访问 data中的数据则要用箭头函数
return `${params.name} <br/>
地区ID: ${
this.arrMap.find((item) => item.name === params.name)
?.id ?? 0
}`;
},
},
series: [
// 配置地图
{
type: "map", // 类型
mapType: "china", // 地图名称,要和引入的地图名一致
roam: true, // 是否开启鼠标缩放和平移漫游
label: {
// 地图省份模块配置
normal: { show: true }, // 是否显示省份名称
position: "right", // 显示位置
},
emphasis: {
// 高亮状态下的多边形和标签样式。
label: {
show: true, // 是否显示标签。
},
},
itemStyle: {
//地图区域的多边形图形样式
normal: {
areaColor: "#2a5caa", //地图区域颜色
borderColor: "#afb4db", //图形的描边颜色
borderWidth: 1, //描边线宽。为 0 时无描边
borderType: "solid", // 边框样式
opacity: 0.6, // 透明度
},
},
data: this.arrMap, // 提示框的数据源
},
],
};
myChart.setOption(option);
},

/**
* echarts tooltip轮播
* @param chart ECharts实例
* @param chartOption echarts的配置信息
* @param options object 选项
* {
* interval 轮播时间间隔,单位毫秒,默认为2000
* loopSeries boolean类型,默认为false。
* true表示循环所有series的tooltip,false则显示指定seriesIndex的tooltip
* seriesIndex 默认为0,指定某个系列(option中的series索引)循环显示tooltip,
* 当loopSeries为true时,从seriesIndex系列开始执行。
* updateData 自定义更新数据的函数,默认为null;
* 用于类似于分页的效果,比如总数据有20条,chart一次只显示5条,全部数据可以分4次显示。
* }
* @returns {{clearLoop: clearLoop}|undefined}
*/
export function loopShowTooltip(chart, chartOption, options) {
let defaultOptions = {
interval: 2000,
loopSeries: false,
seriesIndex: 0,
updateData: null,
};

if (!chart || !chartOption) {
return;
}

let dataIndex = 0; // 数据索引,初始化为-1,是为了判断是否是第一次执行
let seriesIndex = 0; // 系列索引
let timeTicket = 0;
let seriesLen = chartOption.series.length; // 系列个数
let dataLen = 0; // 某个系列数据个数
let chartType; // 系列类型
let first = true;
let lastShowSeriesIndex = 0;
let lastShowDataIndex = 0;

if (seriesLen === 0) {
return;
}

// 待处理列表
// 不循环series时seriesIndex指定显示tooltip的系列,不指定默认为0,指定多个则默认为第一个
// 循环series时seriesIndex指定循环的series,不指定则从0开始循环所有series,指定单个则相当于不循环,指定多个
// 要不要添加开始series索引和开始的data索引?

if (options) {
options.interval = options.interval || defaultOptions.interval;
options.loopSeries = options.loopSeries || defaultOptions.loopSeries;
options.seriesIndex = options.seriesIndex || defaultOptions.seriesIndex;
options.updateData = options.updateData || defaultOptions.updateData;
} else {
options = defaultOptions;
}

// 如果设置的seriesIndex无效,则默认为0
if (options.seriesIndex < 0 || options.seriesIndex >= seriesLen) {
seriesIndex = 0;
} else {
seriesIndex = options.seriesIndex;
}

/**
* 清除定时器
*/
function clearLoop() {
if (timeTicket) {
clearInterval(timeTicket);
timeTicket = 0;
}

chart.off('mousemove', stopAutoShow);
zRender.off('mousemove', zRenderMouseMove);
zRender.off('globalout', zRenderGlobalOut);
}

/**
* 取消高亮
*/
function cancelHighlight() {
/**
* 如果dataIndex为0表示上次系列完成显示,如果是循环系列,且系列索引为0则上次是seriesLen-1,否则为seriesIndex-1;
* 如果不是循环系列,则就是当前系列;
* 如果dataIndex>0则就是当前系列。
*/
let tempSeriesIndex =
dataIndex === 0
? options.loopSeries
? seriesIndex === 0
? seriesLen - 1
: seriesIndex - 1
: seriesIndex
: seriesIndex;
let tempType = chartOption.series[tempSeriesIndex].type;

if (tempType === 'pie' || tempType === 'radar' || tempType === 'map') {
chart.dispatchAction({
type: 'downplay',
seriesIndex: lastShowSeriesIndex,
dataIndex: lastShowDataIndex,
}); // wait 系列序号为0且循环系列,则要判断上次的系列类型是否是pie、radar
}
}

/**
* 自动轮播tooltip
*/
function autoShowTip() {
let invalidSeries = 0;
let invalidData = 0;
function showTip() {
// chart不在页面中时,销毁定时器
let dom = chart.getDom();
if (document !== dom && !document.documentElement.contains(dom)) {
clearLoop();
return;
}

// 判断是否更新数据
if (
dataIndex === 0 &&
!first &&
typeof options.updateData === 'function'
) {
options.updateData();
chart.setOption(chartOption);
}

let series = chartOption.series;
let currSeries = series[seriesIndex];
if (
!series ||
series.length === 0 ||
!currSeries ||
!currSeries.type ||
!currSeries.data ||
!currSeries.data.length
) {
return;
}

chartType = currSeries.type; // 系列类型
dataLen = currSeries.data.length; // 某个系列的数据个数

let tipParams = {
seriesIndex: seriesIndex,
};
switch (chartType) {
case 'pie':
// 处理饼图中数据为0或系列名为空的不显示tooltip
if (
!currSeries.data[dataIndex].name ||
currSeries.data[dataIndex].name === '空' ||
!currSeries.data[dataIndex].value
) {
invalidData += 1;
dataIndex = (dataIndex + 1) % dataLen;
if (options.loopSeries && dataIndex === 0) {
// 数据索引归0表示当前系列数据已经循环完
// 无效数据个数个总数据个数相等,则该系列无效
if (invalidData === dataLen) {
invalidSeries += 1;
}

// 新系列,重置无效数据个数
invalidData = 0;

// 系列循环递增1
seriesIndex = (seriesIndex + 1) % seriesLen;
// 系列数循环至起始值时重置无效系列数
if (seriesIndex === options.seriesIndex) {
if (seriesLen !== invalidSeries) {
// 下一次系列轮回,重置无效系列数
invalidSeries = 0;
showTip();
} else {
// 下一次系列轮回,重置无效系列数
invalidSeries = 0;
clearLoop();
}
} else {
showTip();
}
} else if (!options.loopSeries && dataIndex === 0) {
if (dataLen !== invalidData) {
invalidData = 0;
showTip();
} else {
invalidData = 0;
clearLoop();
}
} else {
showTip();
}

return;
}
// eslint-disable-next-line no-fallthrough
case 'map':
case 'chord':
tipParams.name = currSeries.data[dataIndex].name;
break;
case 'radar': // 雷达图
tipParams.seriesIndex = seriesIndex;
// tipParams.dataIndex = dataIndex;
break;
case 'lines': // 线图地图上的lines忽略
dataIndex = 0;
seriesIndex = (seriesIndex + 1) % seriesLen;
invalidSeries++; // 记录无效系列数,如果无效系列数和系列总数相等则取消循环显示
if (seriesLen !== invalidSeries) {
showTip();
} else {
clearLoop();
}
return;
default:
tipParams.dataIndex = dataIndex;
break;
}

if (chartType === 'pie' || chartType === 'radar' || chartType === 'map') {
if (!first) {
cancelHighlight();
}

// 高亮当前图形
chart.dispatchAction({
type: 'highlight',
seriesIndex: seriesIndex,
dataIndex: dataIndex,
});
}

// 显示 tooltip
tipParams.type = 'showTip';
chart.dispatchAction(tipParams);

lastShowSeriesIndex = seriesIndex;
lastShowDataIndex = dataIndex;

dataIndex = (dataIndex + 1) % dataLen;
if (options.loopSeries && dataIndex === 0) {
// 数据索引归0表示当前系列数据已经循环完
invalidData = 0;
seriesIndex = (seriesIndex + 1) % seriesLen;
if (seriesIndex === options.seriesIndex) {
invalidSeries = 0;
}
}

first = false;
}

showTip();
timeTicket = setInterval(showTip, options.interval);
}

// 关闭轮播
function stopAutoShow() {
if (timeTicket) {
clearInterval(timeTicket);
timeTicket = 0;

if (chartType === 'pie' || chartType === 'radar' || chartType === 'map') {
cancelHighlight();
}
}
}

let zRender = chart.getZr();

function zRenderMouseMove(param) {
if (param.event) {
// 阻止canvas上的鼠标移动事件冒泡
// param.event.cancelBubble = true;
}
stopAutoShow();
}

// 离开echarts图时恢复自动轮播
function zRenderGlobalOut() {
// console.log("移出了")
// console.log(timeTicket)
if (!timeTicket) {
autoShowTip();
}
}

// 鼠标在echarts图上时停止轮播
chart.on('mousemove', stopAutoShow);
zRender.on('mousemove', zRenderMouseMove);
zRender.on('globalout', zRenderGlobalOut);

autoShowTip();

return {
clearLoop: clearLoop
};
}


getFirst() 方法是 List 接口没有的方法。它可能是某个特定实现类的方法,比如 LinkedList 或者 ArrayList 的方法,但是在标准的 List 接口中并不存在。

如果你希望获取列表中的第一个元素,可以使用 get(0) 方法,它会返回列表中索引为 0 的元素。这是通用的方法,可以用于任何实现了 List 接口的类。

问题出在List接口上,它没有getFirst()方法。如果你想获取列表中的第一个元素,可以使用get(0)方法来实现。所以,你需要将下面这行代码:

AppProductSkuSaveReqVO appProductSkuSaveReqVO = updateReqVO.getSkus().getFirst();

修改为:

AppProductSkuSaveReqVO appProductSkuSaveReqVO = updateReqVO.getSkus().get(0);

这样就可以正确获取列表中的第一个元素了。

StringBuilder url = new StringBuilder()
.append("https://api.weixin.qq.com/sns/jscode2session")
.append("?appid=").append(appid)
.append("&secret=").append(secret)
.append("&js_code=").append(code)
.append("&grant_type=authorization_code");

var mycharts = echarts.init(this.$refs.echartsMap);
var option ={};
mycharts.setOption(option);

var index = 0; //播放所在下标
this.mTime = setInterval(function() {
mycharts.dispatchAction({
type: 'showTip',
seriesIndex: 0,
dataIndex: index
});
index++;
if(index >= option.series[0].data.length) {
index = 0;
}
}, 6000);

let index = 0; //播放所在下标
const mTime = setInterval(function() {
mycharts.dispatchAction({
type: 'showTip',
seriesIndex: 0,
dataIndex: index
});
index++;
if(index >= option.value.series[0].data.length) {
index = 0;
}
}, 6000);

发起过一次订阅申请之后用户在设置中不允许通知,则无法弹出申请对话框: 

后端获取Token:

private TokenObj getAccessToken() {
Map<String, Object> params = new HashMap<>();
params.put("grant_type", "client_credential");
params.put("appid", appletsConfig.getWxAppId());
params.put("secret", appletsConfig.getWxAppSecret());
String wxAppletDomain = "https://api.weixin.qq.com/cgi-bin/token";
String res = HttpClientUtils.get(HttpClientUtils.getDefaultPoolClient(),wxAppletDomain,params);
TokenObj obj = null;
if (StringUtils.hasText(res)) {
obj = JSON.parseObject(res, TokenObj.class);
}
if (obj == null || StringUtils.isEmpty(obj.getAccess_token())) {
throw new BusinessException("获取token失败:" + res);
}
return obj;
}

public String sendMsg(HttpServletRequest request){
//请求 微信接口 获取 accessToken
String accessToken = getAccessToken();
String openid = "接收消息的微信用户的openId";
String templateId = "微信订阅消息模板";
String page = "点击消息的跳转路径";
// 构建订阅消息内容的JSON对象
// 构建订阅消息内容的JSON对象
JSONObject messageData = new JSONObject();
messageData.put("name2", createDataItem("张三"));
messageData.put("name3", createDataItem("李四"));
messageData.put("time4", createDataItem("2023-06-30"));
// 将订阅消息内容转换为JSON字符串
String jsonData = messageData.toJSONString();
pushMessage(accessToken,openid,templateId,page,jsonData);
return "success";
}
private static Map<String, Object> createDataItem(String value) {
Map<String, Object> item = new HashMap<>();
item.put("value", value);
return item;
}

public void pushMessage(String accessToken, String openId, String templateId, String page, Map<String, Map<String,Object>> jsonData) {
try {
Map<String, Object> params=new HashMap<>();
params.put("touser",openId);
params.put("data", jsonData);
if(StringUtils.hasText(page)){
params.put("page",page);
}
params.put("miniprogram_state", "trial");
params.put("template_id",templateId);
String pushUrl = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s";
String result= HttpClientUtils.post(HttpClientUtils.getDefaultPoolClient(), String.format(pushUrl, accessToken), params);
logger.info("【微信推送】微信推送返回结果 ,{}",result);
} catch (Exception e) {
logger.error("【微信推送】微信推送请求失败", e);
}
}

js

@RestController
public class SendWxMessage {
/*
* 发送订阅消息
* */
@GetMapping("/pushOneUser")
public String pushOneUser() {
return push("o3DoL0WEdzxxx96gbjM");
}
public String push(String openid) {
RestTemplate restTemplate = new RestTemplate();
//这里简单起见我们每次都获取最新的access_token(时间开发中,应该在access_token快过期时再重新获取)
String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + getAccessToken();
//拼接推送的模版
WxMssVo wxMssVo = new WxMssVo();
wxMssVo.setTouser(openid);//用户的openid(要发送给那个用户,通常这里应该动态传进来的)
wxMssVo.setTemplate_id("CFeSWarQL-MPyBzTU");//订阅消息模板id
wxMssVo.setPage("pages/index/index");
Map<String, TemplateData> m = new HashMap<>(3);
m.put("thing1", new TemplateData("小程序1"));
m.put("thing6", new TemplateData("小程序2"));
m.put("thing7", new TemplateData("小程序3"));
wxMssVo.setData(m);
ResponseEntity<String> responseEntity =
restTemplate.postForEntity(url, wxMssVo, String.class);
return responseEntity.getBody();
}
@GetMapping("/getAccessToken")
public String getAccessToken() {
RestTemplate restTemplate = new RestTemplate();
Map<String, String> params = new HashMap<>();
params.put("APPID", "wx7c54942sssssd8"); //
params.put("APPSECRET", "5873a729csxssssd49"); //
ResponseEntity<String> responseEntity = restTemplate.getForEntity(
"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={APPID}&secret={APPSECRET}", String.class, params);
String body = responseEntity.getBody();
JSONObject object = JSON.parseObject(body);
String Access_Token = object.getString("access_token");
String expires_in = object.getString("expires_in");
System.out.println("有效时长expires_in:" + expires_in);
return Access_Token;
}
}

发送订阅消息三步走

  • 1,拿到用户的openid

  • 2,获取access_token

  • 3,调用小程序消息推送的接口

使用Vite

如果你的项目是使用Vite创建的,你的build.sh脚本可以修改为:

bashCopy code
echo "===打包文件==="
# 设置环境模式
export VITE_APP_BASE_URL=your_production_url

echo "===打包文件===" # 设置环境变量
export NODE_ENV=production
yarn build echo "===传输文件==="
  1. Kafka:优化用于高吞吐量消息传输,尤其在消息较小时表现出色。

  2. MySQL:作为关系型数据库,适合复杂查询,但高写负载或复杂事务可能影响性能。

  3. MongoDB:作为NoSQL文档数据库,优于处理大量读写操作,但取决于数据模型和索引。

  4. Elasticsearch:搜索和分析工作负载优化,但受限于索引和查询优化。

数据库类型吞吐量参考值(每秒操作数)影响因素
Kafka数十万到上百万消息消息大小、网络带宽、磁盘I/O、分区策略
MySQL数百到数千事务查询优化、索引、数据模型、硬件资源
MongoDB数百到数千读写操作文档设计、索引、查询模式、服务器配置
Elasticsearch数百到数千查询和索引操作索引结构、查询类型、数据量、硬件资源

Kafka:在良好的硬件和网络条件下,单机Kafka可以处理每秒数十万到上百万消息的吞吐量,尤其在消息大小较小(几百字节到几KB)的情况下。

MySQL:对于MySQL,一个常见的参考吞吐量是每秒几百到几千个事务,但这极大地取决于事务的复杂性和数据库的优化。

数据库和消息队列系统(如Kafka、MySQL、MongoDB、Elasticsearch)的单机吞吐量受多种因素影响,因此很难给出具体的数值。实际的吞吐量取决于硬件配置、网络环境、数据模型、查询类型以及系统的配置和优化。以下是一些影响各系统吞吐量的关键因素以及如何评估各自的性能:

  1. Kafka

  • Kafka设计用于处理高吞吐量的数据流,其性能受制于网络带宽、磁盘I/O以及分区策略。

  • Kafka吞吐量的评估通常考虑消息大小和生产者/消费者的数量。

  • MySQL

    • 作为关系型数据库,MySQL的性能受到查询优化、索引、数据模型和硬件资源(如CPU、内存、磁盘I/O)的影响。

    • 评估MySQL性能时,通常考虑每秒可以处理的事务数(TPS)和查询响应时间。

  • MongoDB

    • MongoDB是一个文档型数据库,其性能受到文档设计、索引、查询模式和服务器配置的影响。

    • MongoDB吞吐量的评估可以考虑每秒读写操作的数量。

  • Elasticsearch

    • Elasticsearch是一个搜索引擎和分析平台,其性能取决于索引结构、查询类型、数据量和硬件资源。

    • Elasticsearch的性能通常以每秒查询数和索引操作来衡量。

    对于其他数据库(如PostgreSQL、Redis、Cassandra等),同样的原则适用。它们的性能取决于特定的使用场景和系统配置。

    要准确地了解这些系统的吞吐量,最佳做法是在特定的环境下进行基准测试。基准测试应模拟真实的工作负载和数据模式,以获得有意义的性能指标。此外,性能调优(如查询优化、索引调整、合理的分区和复制策略)也是提高系统吞吐量的重要方面。

    加群联系作者vx:xiaoda0423

    仓库地址:https://github.com/webVueBlog/JavaGuideInterview

    算法猫叔
    程序员:进一寸有一寸的欢喜
     最新文章