开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

200行代码实现类似Swiperjs的轮播组件

发表于 2024-04-23

前言

大家在开发过程中,或多或少都会用到轮播图之类的组件,PC和Mobile上使用 Swiper.js ,小程序上使用swiper组件等。

本文将详细讲解如何用Vue一步步实现的类似Swiper.js的功能,无任何第三方依赖,干货满满。

最终效果

在线预览:zyronon.github.io/douyin/

项目源代码:github.com/zyronon/dou…

注意:PC 必须将浏览器切到手机模式,先按 F12 调出控制台,再按 Ctrl+Shift+M才能正常预览

Demo代码

上面的预览地址是最终实现的效果,下面才是本文代码实现的效果

为提升阅读体验,正文中代码展示有部分省略处理,完整代码可以在codesandbox上查看:
codesandbox.io/p/devbox/mu…

Edit mutable-grass

实现原理

布局

我们需要用到两个div,父元素 slide 设置 overflow: hidden 禁止滚动,子元素 slide-list 使用 flex 布局,然后将需要滚动的页面做为孙元素放在子元素 slide-list 中,由于子元素 slide-list 是 flex 布局,页面会自然的平铺排列

因为父元素 slide 的overflow: hidden属性会将内容裁减,不提供滚动条,也不允许用户滚动,所以我们只能看到父元素 slide 宽高的内容。

image.png

1
2
3
4
5
html复制代码<div class="slide">
<div class="slide-list">
<slot></slot>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
html复制代码.slide {
touch-action: none;
height: 100%;
width: 100%;
transition: height 0.3s;
position: relative;
overflow: hidden;
}

.slide-list {
height: 100%;
width: 100%;
display: flex;
position: relative;
}

滑动

实现滚动的关键点在于CSS3的 transform: translate(0, 0) 属性。

translate() 这个 CSS 函数在水平和/或垂直方向上重新定位元素,它的坐标定义了元素在每个方向上移动了多少。

因为子元素 slide-list 的内容是平铺的,我们只需要在子元素 slide-list 监听对应的事件,计算滑动的距离x或y,再动态设置到子元素 slide-list 的transform: translate(x, y)里面,就可以实现页面滑动了

总结

大家可以将整个流程理解为播放胶片电影:父元素 A 是放映机,子元素 B 是胶片,而页面是印刷在胶片上的内容。胶片每移动一格,我们就能看到新的一帧电影

image.png

实现

监听事件

PC 上的点击、移动,H5 的手势操作,都离不开 DOM 事件监听。例如鼠标移动事件对应 mousemove,移动端因为没有鼠标则对应 touchmove

我们可以通过 Pointer 事件进行多端统一的事件监听,实现触屏和 PC 端通用

image.png

1
2
3
4
5
6
7
8
9
10
11
html复制代码<div class="slide horizontal">
<div
class="slide-list"
ref="wrapperEl"
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
>
<slot></slot>
</div>
</div>

初始化

组件默认变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码//slide-list的ref引用
const wrapperEl = ref(null)

const state = reactive({
judgeValue: 20,//一个用于判断滑动朝向的固定值
type: SlideType.VERTICAL,//组件类型
name: props.name,
localIndex: props.index,//当前下标
needCheck: true,//是否需要检测,每次按下都需要检测,up事件会重置为true
next: false,//能否滑动
isDown: false,//是否按下,用于move事件判断
start: {x: 0, y: 0, time: 0},//按下时的起点坐标
move: {x: 0, y: 0},//移动时的坐标
wrapper: {width: 0, height: 0, childrenLength: 0}//slide-list的宽度和子元素数量
})
1
2
3
4
5
6
7
8
9
js复制代码function slidePointerDown(e, el, state) {
Utils.$setCss(el, 'transition-duration', `0ms`)
//记录起点坐标,用于move事件计算移动距离
state.start.x = e.pageX
state.start.y = e.pageY
//记录按下时间,用于up事件判断滑动时间
state.start.time = Date.now()
state.isDown = true
}

虽然我们用 Pointer事件统一了移动端和PC端的监听事件,但 pointermove 事件在 PC 和移动端表现出来的效果却不一样,在 PC 上, pointermove 事件和 mousemove 事件一致,只要鼠标在目标元素上方,就会触发。而在移动端上却只有按下并移动时发才会触发

所以这里用一个 isDown 的变量保存是否按下的状态,pointermove事件虽然会一直触发,但仅当 isDown 时才执行我们的代码逻辑

移动过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
js复制代码function slidePointerMove(e,el,state) {
if (!state.isDown) return;

//计算移动距离
state.move.x = e.pageX - state.start.x
state.move.y = e.pageY - state.start.y

//检测能否滑动
let canSlideRes = canSlide(state)

//是否是往下(右)滑动
let isNext = state.type === SlideType.HORIZONTAL ? state.move.x < 0 : state.move.y < 0

if (canSlideRes) {
if (canNext(state, isNext)) {
//能滑动,那就把事件捕获,不能给父组件处理
Utils.$stopPropagation(e)

//获取偏移量
let t = getSlideOffset(state, el) + (isNext ? state.judgeValue : -state.judgeValue)
let dx1 = 0,
dx2 = 0
//偏移量加当前手指移动的距离就是slide要偏移的值
if (state.type === SlideType.HORIZONTAL) {
dx1 = t + state.move.x
} else {
dx2 = t + state.move.y
}
Utils.$setCss(el, 'transition-duration', `0ms`)
Utils.$setCss(el, 'transform', `translate(${dx1}px, ${dx2}px)`)
}
}
}

用鼠标当前的位置,再减去鼠标按下时的位置,就是鼠标移动的距离

移动距离再加上当前页面 * 每个页面的宽或高,即子元素 slide-list 整体要偏移的量

技术难点

1. 如何判断滑动方向?是在上下滑还是左右滑?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码//检测在对应方向上能否允许滑动,比如SlideHorizontal组件就只处理左右滑动事件,SlideVertical 
//只处理上下滑动事件
export function canSlide(state) {
//每次按下都需要检测,up事件会重置为true
if (state.needCheck) {
//判断move x和y的距离是否大于判断值,因为距离太小无法判断滑动方向
if (Math.abs(state.move.x) > state.judgeValue || Math.abs(state.move.y) > state.judgeValue) {
//放大再相除,根据长宽比判断方向,angle大于1就是左右滑动,小于是上下滑动
let angle = (Math.abs(state.move.x) * 10) / (Math.abs(state.move.y) * 10)
//根据当前slide的类型,判断能否滑动,并记录下来,后续不再判断,直接返回记录值
state.next = state.type === SlideType.HORIZONTAL ? angle > 1 : angle <= 1
state.needCheck = false
} else {
return false
}
}
return state.next
}

放大移动距离后再相除,根据结果是否大于1判断出滑动方向

2. 如何处理嵌套组件中的事件冲突?什么时候拦截事件和放行事件?

由于事件的冒泡机制,事件是从最里面的元素一级一级的往上冒泡的,所以我们只需在满足下面两个条件时拦截事件即可

  1. 是否在往到头或尾滑动

如果在第一页,不能往左/上滑动

如果在最后一面, 不能往右/下滑动

1
2
3
4
5
6
js复制代码function canNext(state, isNext) {
return !(
(state.localIndex === 0 && !isNext) ||
(state.localIndex === state.wrapper.childrenLength - 1 && isNext)
)
}
  1. 滑动方向和组件类型相匹配
  • SlideHorizontal.vue 组件只允许向左/右滑动
  • SlideVertical.vue 组件只允许向上/下滑动

满足上述两个条件时拦截事件,不满足放行事件,交给上一级组件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码//检测在对应方向上能否允许滑动
let canSlideRes = canSlide(state)
//是否是往下(右)滑动
let isNext = state.type === SlideType.HORIZONTAL ? state.move.x < 0 : state.move.y < 0
if (canSlideRes) {
if (canNext(state, isNext)) {
//能滑动,那就把事件捕获,不能给父组件处理
Utils.$stopPropagation(e)
...
滑动逻辑
...
}
}

结束滑动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
js复制代码function slidePointerUp(e, state) {
if (!state.isDown) return;
let isHorizontal = state.type === SlideType.HORIZONTAL
let isNext = isHorizontal ? state.move.x < 0 : state.move.y < 0
if (state.next) {
if (canNext(state, isNext)) {
//结合时间、距离来判断是否成功滑动
let endTime = Date.now()
let gapTime = endTime - state.start.time
let distance = isHorizontal ? state.move.x : state.move.y
let judgeValue = isHorizontal ? state.wrapper.width : state.wrapper.height
//1、距离太短,直接不通过
if (Math.abs(distance) < 20) gapTime = 1000
//2、距离太长,直接通过
if (Math.abs(distance) > judgeValue / 3) gapTime = 100
//3、若不在上述两种情况,那么只需要判断时间即可
if (gapTime < 150) {
if (isNext) state.localIndex++
else state.localIndex--
}
}
}
// 重置变量
Utils.$setCss(el, 'transition-duration', `300ms`)
let t = getSlideOffset(state, el)
let dx1 = 0,dx2 = 0
if (state.type === SlideType.HORIZONTAL) dx1 = t
else dx2 = t
Utils.$setCss(el, 'transform', `translate3d(${dx1}px, ${dx2}px, 0)`)
...
}

技术难点

  • 如何让滑动结束时的动画更丝滑?

结合滑动时间、滑动距离来判断滑动下一条还是保持当前条

1、距离太短,直接不通过

2、距离太长,直接通过

3、若不在上述两种情况,那么只需要判断时间即可,小于150毫秒以内就算是成功滑动

其他问题

PC 上滑动有图片的页面,图片“分叉”了:我们开始拖动它的“克隆”

这是因为浏览器有自己的对图片和一些其他元素的拖放处理。它会在我们进行拖放操作时自动运行,并与我们的拖放处理产生了冲突

禁用它:

1
js复制代码@dragstart="(e) => Utils.$stopPropagation(e)"

PC 上滑动结束后触发了click事件

问题分析

首先我们滑动是利用 pointerdown, pointermove, pointerup 三个事件组合形成的,但是 pointerup 执行之后, click 是一定会执行的,是无法避免的,是无法用preventDefault , stopPropagation , stopImmediatePropagation 阻止的, 因为pointer 事件和 click 事件本身就不是一个系列的,因此没有关系,所以当发生滑动之后,pointerup 一定会执行,click 也会在 pointerup 执行后执行

解决方案

我们设置一个全局变量

1
js复制代码window.isMoved = false

在 pointermove 事件中,将 window.isMoved 设为 true。然后在 pointerup 事件中,我们用一个定时器让这个变量在200毫秒之后发生改变为 false,因为 pointerup 之后 click 很快就触发了,不到200ms,因此可以保证变量还没有发生变化,click 事件里面去检测这个变量,如果是变化之前,那么不执行

如果 click 事件少还好说,直接复制几遍无所谓。

但是一般来说 click 事件在项目中使用还是挺多的,有没有什么一劳永逸的办法呢?
大部分监听 click 事件都是用 Vue 的 @click 添加的,我们无法插手

这时给大家介绍一下 Proxy 这个对象了,Vue3 的双向绑定就用到了 Proxy 对象。

在项目入口,我们直接代理 HTMLElement.prototype.addEventListener 这个事件,代理了之后,Vue 的 @click 语法糖添加事件时就会通知我们,这时再进行判断是不是 click 事件,是的话再判断 window.isMoved 的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
js复制代码window.isMoved = false
HTMLElement.prototype.addEventListener = new Proxy(HTMLElement.prototype.addEventListener, {
apply(target, ctx, args) {
const eventName = args[0]
const listener = args[1]
if (listener instanceof Function && eventName === 'click') {
args[1] = new Proxy(listener, {
apply(target, ctx, args) {
if (window.isMoved) return
try {
return target.apply(ctx, args)
} catch (e) {
console.error(`[proxyPlayerEvent][${eventName}]`, listener, e)
}
}
})
}
return target.apply(ctx, args)
}
})

设置了 overflow: auto 的页面在移动端不触发 pointermove 事件

再设置一个 touch-action:pan-y 就正常了

CSS 属性 touch-action 用于设置触摸屏用户如何操纵元素的区域 (例如,浏览器内置的缩放功能), pan-y 启用单指垂直平移手势

总结

核心代码加上注释一共217行,我们实现了一个可以在 PC 和 Mobile 上通用,并且可以无限嵌套的轮播组件
image.png

结束

以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注我的公众号前端张余让,我会更新更多实用的前端知识与技巧,期待与你共同成长~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

全链路压测自动化的探索与实践 一、背景与挑战 二、方案与目标

发表于 2024-04-23

作者简介:

魏占飞,来自货拉拉/技术中心/质量保障部,测试专家,负责货拉拉性能测试领域的质量保障和效能建设工作。

陈婷婷,来自货拉拉/技术中心/质量保障部,资深测试工程师,负责货拉拉货运性能压测保障和效能建设工作。

一、背景与挑战

在过去的几年里,货拉拉的用户和货运订单数量都实现了快速增长,现已发展成为全球最大的闭环物流交易平台。随着订单业务的飞速发展,系统稳定性的保障变得越来越重要,全链路压测在保证系统稳定性的过程中发挥了关键的验证作用,其目标是检验系统的稳定性和容量。然而,在业务快速迭代和高速增长的背景下,全链路压测也面临着更高的需求和挑战。

及时验证峰值容量: 随着业务的高速发展,订单量常常会突破历史峰值,甚至曾在半个月内多次打破记录。因此,我们必须确保压测能够及时验证服务的总体容量以及单个服务扩容后相关链路的稳定性。

提升压测效率: 传统的全链路压测是一项较为繁重的测试活动,它不仅涉及多个部门和人员,还需要核心服务的开发人员进行值班。此外,还包含了较多的脚本维护,复杂的造数,以及较长的压测流程。若要求能够实现及时、高效的全链路压测,我们必须提升测试工作的效率,同时减少对值守人员的依赖。

保障压测安全性: 全链路压测通常是在生产环境中进行,压测所产生的大流量可能导致某些服务的响应变得不稳定,甚至可能引发连锁反应,影响到更多服务并导致线上真实故障的发生。因此,在压测过程中要具备熔断机制,在服务被压崩溃前及时停止压测流量,保障系统稳定运行。

二、方案与目标

在构思初始方案时,我们在行业内寻找并比对类似的解决方案,但是发现相关资料较少。由于全链路压测和压测自动化都需要与公司业务和内部平台频繁交互,我们在考虑改造全链路压测自动化时,决定结合公司业务特点和现有全链路压测流程以及内部平台特性进行改造。

为实现全链路压测的自动化,主要目标是把人工操作的重复工作自动化。在确定整体方案后,我们详细梳理了全链路压测前、中、后的常见工作场景,并确认了对应的需求以及平台服务支撑。以下是我们的整体改造思路图。

压测体系.png

梳理出改造场景中的重点和难点问题,我们需要着重完成以下功能:

模型建模与模型效果对比: 无论是在压测场景和流量的配比,还是在压测后的效果对比中,都需要我们找到一个适合的模型算法,以确认压测场景的模型以及对比压测效果。

压测任务编排: 在压测任务中,一些任务有固定的时间顺序或因果关系顺序。这就要求我们需要有一个灵活的任务编排系统,方便自由地编排相关压测任务。

压测任务调度: 以货拉拉货运全链路压测为例,每次涉及到超 70 个脚本,300 余台压测机,以及数百万订单的压测目标流量。因此,如何对测试场景、脚本和测试数据以及压测流量进行有效的管理、分发和收集显得尤为重要。

健全的熔断机制: 既要检测出系统瓶颈,又要在出现异常时及时停止压测流量,以避免引发更大的生产问题,因此需要一个健全的熔断机制。

三、能力建设

全链路压测自动化的整体能力建设内容繁多,我们将以重点改造为例,列出相关的思考和部分实现原理。

3.1 高峰流量与压测流量建模

压测模型,简而言之,就是设定被压测的各个服务及其相关接口的具体目标 QPS 比例,压测流量模型应尽可能接近线上真实流量。模型自动化在全链路压测的整个过程中有各种应用场景,其中包括在压测前建立压测模型,在压测过程中调整压测场景,以及在压测后对比流量模型。

压测建模.png

3.1.1 高峰流量建模

当服务数量有限时,常通过手动观察各服务接口的请求量并配置对应压测流量比例来建立模型。但在大型项目全链路压测中,接口数量和脚本量的增长使这成为了压测人员最繁重的任务之一。且在人工配置过程中可能出现疏忽,导致流量比例失真。

对于压测模型的构建,我们首先从公司监控平台获取主要相关数据,然后进行关联和模型转换,最终定时生成各服务不同指标的峰值流量模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**简化代码,只保留主要流程信息,
*从监控平台获取相关服务数据,并转化为压测模型保存在压测平台
*/
public PressureModel getAndSaveOneDayMonitorData(String appId, String env, String name, String batchId, long start, long end, long step, MonitorData orderMonitorData) {
PressureModel model = new PressureModel();
String timeScope = StrUtil.isEmpty(orderMonitorData.getTimeScope()) ? MonitorData.TIME_ALL_DAY : MonitorData.TIME_CUSTOM;
//soa监控数据获取并转换模型
HashMap<String, MetricIntegratorData> monitorQpsSoaData = getMonitorQpsSoaData(env, appId, false, start, end, step);
List<MonitorData> monitorQpsSoaDataFromMetricDates = getMonitorDataFromMetricDates(appId, env, name, batchId, MonitorData.TYPE_QPS_SOA, MonitorData.TYPE_QPS_SOA, timeScope, monitorQpsSoaData);
if (!monitorQpsSoaDataFromMetricDates.isEmpty()) {
monitorDataRepository.saveAll(monitorQpsSoaDataFromMetricDates);
model = MonitorDataUtil.transitionPressureModel(model, monitorQpsSoaDataFromMetricDates, orderMonitorData);
}
//获取其它关键信息如SOA,CPU,Mysql,Redis等,均保存到模型model中。
pressureModelRepository.save(model);
}

在模型算法方面,我们在压测初期采用了业务峰值时间段峰值算法,随后发现各个服务和预测订单量的比例关系并不固定。为了细化不同级别预测订单和各服务的目标流量的系数关系,我们结合公司的订单量和业务特点,采用了以下的拟合算法:

p(x)=p1xn+p2xn−1+…+pnx2+pn+1x+a0p(x) = p_1 x^n + p_2 x^{n-1} + \ldots + p_n x^2 + p_{n+1} x + a_0p(x)=p1xn+p2xn−1+…+pnx2+pn+1x+a0。通过这样的模型,可以方便地计算不同订单级别对应的模型缩放系数,在各个订单级别的压测任务中,可以快速得到相应服务的模型数据。通过对模型数据和真实数据的回溯对比,我们发现模型和真实数据的贴合度相当高(已对相关数据进行脱敏处理)。

压测算法.png

3.1.2 流量比例调整

为了更精确地进行压测,除了获取整体服务的压测模型外,我们还需要获取各个服务的接口流量模型,即确定压测模型所在时间段内的接口使用比例。根据这些信息,系统会自动调整压测脚本的接口比例并自动上传脚本,从而使每次压测更接近最新的业务模型。

3.1.3 压测效果模型

由于压测服务中存在业务分支处理和缓存关系,实际的压测过程中常会发现压测流量覆盖不全。为全面评估压测效果,在每次压测后,对压测时间段内的压测流量单独建模,并通过Cpu%(Avg)、Cpu%(Max)、Http、Soa、Redis、Mysql 六个维度的峰值模型与压测模型进行对比,以便及时发现异常流量,并针对性优化,使全链路压测的流量更接近真实业务。

在没有模型效果对比之前,主要是关注入口服务及被动等待开发反馈异常流量,常常会遗漏部分压测流量异常的服务,影响压测效果。通过模型效果对比,可在每次压测后快速发现压测过程中的异常流量服务,以便及时针对性优化,使全链路压测的流量更接近真实业务。

效果模型.png

3.2 编排自动化任务流

3.2.1 压测任务抽像类

在全链路压测过程中,我们常会遇到一些前置条件,如数据初始化,压测信息通知等。我们通过梳理将压测活动分为五大类:运行脚本、运行场景、申请压测机、释放压测机、消息通知。我们抽象出任务的共同行为,并将任务执行前、执行中、执行后的相关操作集中处理或单独定制,这大大减少了前后端的代码量,并增强了后续扩展性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码public abstract class AbstractTaskNodeService implements TaskNodeService {
protected TaskNode taskNode;

public AbstractTaskNodeService(TaskNode taskNode) {
//代码已简化
this.taskNode = taskNode;
init();
preExecute();
execute();
postProcessor();
}

@Override
public void init() {
//解析出对应的上下文,及数据处理工作
}

@Override
public void preExecute() {
//执行预处理动作,包括获取兄弟节点任务
}

@Override
public void execute() {
//执行任务,每一种任务类重写
}

@Override
public void postProcessor() {
//保存状态,生成下一节点任务
}

@Override
public TaskNode getTaskNode() {
return this.taskNode;
}

}

3.2.2 任务流编排

在不同的压测场景下,我们可以根据特定需求,对压测任务进行流程编排和组合,来实现压测流程的个性化定制。

以货拉拉的下单运货业务场景为例,需要预先设定用户和司机的位置,确保在特定范围内,用户能看到足够的司机,司机也能看到足够的订单。因此,压测前需要通过打点脚本将相应的用户和司机部署到不同的位置,这样不仅能更符合线上真实情况,同时也避免了数据热点问题。

热点问题.png

在此类场景下,需要将位置打点脚本放置在压测场景之前,以便提前更新司机位置信息。在其他不同的压测场景下,可能存在不同的要求。人工处理不仅时间紧张,而且易出错,因此,通过编排对应的任务流,我们可以灵活地实现不同压测场景的组合。

任务编排.png

图:任务编排

3.2.3 中间态处理

如果在压测过程中遇到需暂停压测的紧急情况,我们可以暂停或结束当前正在执行的任务。在问题得到处理之后,可以继续执行当前任务节点,或者执行下一个任务节点。

任务状态.png

图:任务执行状态

3.3 压测任务调度

压测任务调度是把各种需求的压测脚本、压测文件、压测数据,根据需要分配到不同的压测机上,压测启动后对压测流量进行实时监控和补充,并收集汇总相关压测指标的过程。在小规模的压测任务中,可以通过手动部署原生Jmeter压测集群,然而,在大规模压测中,原生集群的性能瓶颈非常明显。因此,如何高效地分配任务和收集压测数据是压测平台需要解决的问题。

3.3.1 大规模压测集群管理

不同规模的压测需要不同数量的压测机,以货运全链路压测为例,每次压测需要约 300 台压测机,加上日常的其它的压测任务,需要的压测机数量会更多。货拉拉的最初采用 ECS 集群部署,在达到约 200 台时压测机规模时,其硬件成本已经比较高了,后把压测机改造采用 Serverless Container 弹性容器实例,通过 ASK 竞价申请 POD 资源,1 分钟内可完成 500 台压测机的申请与部署,在申请后,及时进行压测任务后自动或手工释放,使压测机的硬件成本得到了大幅降低。

同时,压测机资源还会根据不同项目、不同申请人、公用、私用等维度进行管理,普通用户可使用自已申请的、当前项目拥有的及公用的压测机进行压测,使压测机资源的综合利用率更高,压测启动后会先把压测任务所需要的压测资源分发到对应权限的空闲压测机中。

压测机申请.png

3.3.2 文件预分发

平台在执行具体的压测任务后,会拆分压测任务的场景、脚本、数据文件及依赖 JAR 包,并按照一定要求把对应的脚本和数据文件分发到对应的目标压测机,这中间涉及到任务的分发,文件的下载与预处理,同时为了避免压测机在各项目之间的文件冲突、数据冲突或 JAR 包冲突,还需要对相关文件做对应处理后放入纯净的压测空间。

文件分发.png

3.3.3 弹性流量管理

压测启动后,对于压测流量经常会遇到两种场景,一种是配置了流量目标,但通常因为服务响应或线程配置没有达到预期流量;另一种因业务缓存或业务分支处理问题,流量无法传递到下游服务。针对这两种情况,我们采用自动加压和自动补流量来解决。

自动加压: 在压测过程如果开始设置的线程数和机器数无法达到预期流量,平台能够提供自动加压功能,自动增加线程数或者压力机来达到测试目标,在有限的压测窗口期,自动追加压力,减少了测试人员时间以及资源成本。 当压测过程中发现QPS(每秒接口请求数)未达到预期时,无需停止本轮压测,系统可以自动探索出需要的线程数,并自动增加线程,以及用户可以手动增加压测机,以追加压测,节约了时间、人力、硬件成本。

自动补流量: 全链路压测中由于缓存和业务分支处理等因素的影响,部分流量无法有效传递到下游服务,在这种情况下,我们需要人工启动下游服务的相关脚本,以补充缺失的流量。然而,由于涉及到的下游链路高达数百个,在一次压测周期内,很难及时通过手工补全缺失服务的流量。结合压测模型对比,提前准备补流量的脚本,在压测时压测平台计算压测流量是否满足要求,以及需要补充的流量多少,自动启动相应的脚本,进行流量补充。

3.3.4 压测数据实时收集

在全链路压测进行过程中,需要实时关注压测相关指标,及时进行收集和汇总,给压测任务提供数据指标及风险参考,各压测机把自身的压测数据通过监听器实时发送到 Kafka,再由收集服务进行统一汇总,在汇总数据后进行展示或结合其它策略进一步利用。压测机的数据采用每秒收集汇总并发送一次,收集服务的收集器采用每 3 秒收集汇总一次,尽可能保证的数据实时性并减少指标图表的曲率的变化频率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码//压测机初步收集汇总本机数据,并发送到kafka
@Slf4j
public class HllBackendListenerClient extends AbstractBackendListenerClient {
@Override
public void setupTest(BackendListenerContext backendListenerContext) throws Exception {
//压测设置项
}

@Override
public void handleSampleResults(List<SampleResult> list, BackendListenerContext backendListenerContext) {
//压测数据处理并发送
}

@Override
public void teardownTest(BackendListenerContext backendListenerContext) throws Exception {
analysis();
schedule.shutdown();
}

private void analysis() {
//压测机初步数据统计
Collector<YiSample, SampleState, SampleState> c = Collector.of(
SampleState::new,
SampleState::accumulator,
SampleState::combiner,
SampleState::finisher
);
Map<String, SampleState> sampleStateMap = temp.stream()
.collect(Collectors.groupingBy(YiSample::getLabel, c));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
java复制代码//收集服务消费KAFKA并统一汇总数据
@Slf4j
public class AnalysisProcessor implements Processor<String, String> {
private ProcessorContext context;
private AtomicInteger processorNumber = new AtomicInteger();
PerformanceRecordRepository performanceRecordRepository = SpringContextUtils.getBean(PerformanceRecordRepository.class);
ReportRepository reportRepository = SpringContextUtils.getBean(ReportRepository.class);
private ConcurrentHashMap<String, List<SampleState>> sampleMap = new ConcurrentHashMap<>();

@Override
public void init(ProcessorContext context) {
//1.初始化
this.context = context;
processorNumber.getAndIncrement();
if (processorNumber.get() <= 1) {
this.context.schedule(Report.REPORT_INTERVAL, PunctuationType.WALL_CLOCK_TIME, (timeStamp) -> analysis());
log.info("init AnalysisProcessor");
}
}

@Override
public void process(String key, String message) {
//2.压测数据处理
SampleState sampleState = JSON.parseObject(message, SampleState.class);
if (!sampleMap.containsKey(key)) {
List<SampleState> list = new ArrayList<>();
sampleMap.put(key, list);
}
sampleMap.get(key).add(sampleState);
}

@Override
public void close() {
}

private void analysis() {
sampleMap.forEach((reportId, sampleStates) -> {
if (sampleStates.size() == 0) {
sampleMap.remove(reportId);
}
long start = System.currentTimeMillis();
Collector<SampleState, PerformanceRecordState, PerformanceRecord> c = Collector.of(
PerformanceRecordState::new,
PerformanceRecordState::accumulator,
PerformanceRecordState::combiner,
PerformanceRecordState::finisher
);
//汇总性能数据
Map<String, PerformanceRecord> reportRecord = sampleStates.stream()
.collect(Collectors.groupingBy(SampleState::getLabel, c));
AtomicInteger i = new AtomicInteger();
reportRecord.forEach((label, record) -> {
PerformanceRecord saveRecord = performanceRecordRepository.save(record);
i.getAndAdd((int) saveRecord.getN());
});

log.info("analysis report:{} once,it has {} record ,and {} samples ,used:{}ms ", reportId, sampleStates.size(), i.get(), (System.currentTimeMillis() - start));
//清空此批数据
if (sampleMap.containsKey(reportId)) {
sampleMap.get(reportId).clear();
}
});

}

}

最终对汇总的数据通过不同维度进行展示,达到实时观察需要或做为熔断条件的前置数据。

报告汇总:

接口数据:

性能曲线:

3.4 压测安全双重保障

压测的熔断机制是压测活动的一道保险,全链路压测因为是在线上真实进行操作的,在服务有资源风险、性能问题或是压力过大时会引起服务响应异常,需要及时停止压测,避免造成连锁反应造成生产故障。

3.4.1 主动熔断

根据压测流量发起方的相关指标如接口错误率、响应时间做为判断指标,由压测平台根据压测数据,主动判断当前压测是否需要发起熔断,从而停止压测。

主动熔断相关指标通过压测脚本的相关配置不同的阈值,在启动压测并收集压测结果时加入熔断规则的判断,触发阈值或达到指定规则后进行熔断。

3.4.2 被动熔断

由货拉拉其它如监控平台、数据库管理平台等公司内部系统平台的相关数据指标或报警为触发条件,并调用压测平台的报警信息接口,压测平台接受报警信息后进一步处理和判断,如果符合条件则会停止压测。

四、实践成果

全链路压测自动化在上线的一年时间内,在效率、成本、压测效果上都取得了显著的成果。

压测成果.png

压测效率: 对比22年的全链路压测,最直观的改变为压测频率的提升,从双周一次提升到每周一次。并具备当天实时发起全链路压测能力,能够对新的峰值做出更迅速的验证。

成本优化: 相较于以往全链路压测需30多名开发值班和测试人员一起参加压测,如今自动化压测只需压测同学进行压测即可,人力成本节省 80%+ 。同时随着压测机采用容器化改造,硬件成本节省了 90+% 以上。

压测效果: 通过模型优化和压测模型效果对比,可及时优化压测流量偏差较大的场景,服务的压测覆盖度和压测流量的合理性都得到了显著提升。经过对每个服务的压测流量和峰值流量的合理区间进行统计,发现有效覆盖率已从最初的43%提高到了90%。

五、未来展望

从过去一年压测自动化实践来看,在压测成本、压测效率及压测效果来看都有了较高的水准,在今年主要在以下方面进行探索尝试。

性能测试大模型: 目前我们对压测的数据只是简单的通过CPU使用率和各种QPS来提醒开发是否进行扩容(如下图),在大模型愈发成熟的趋势下,我们希望对压测数据方面通过AI分析进一步发掘,提高发现性能隐患及性能隐患分析能力。

性能大模型.png

压测服务化提高: 在过去压测平台了提供了一些在特定场景的服务化能力,如与故障演练平台打通直接发起压测流量、与 NOC 打通提供线上的核心功能遍历能力(如下图)。大部分压测仍需要通过脚本编写&调试、数据准备、压测各种环节,在未来会进一步结合流量回放平台、精确测试平台等平台的服务能力,通过自动生成压测场景,逐步提供更多的服务化能力,进一步提升工作中性能测试的效率。

压测服务化.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用虚拟信用卡订阅Starlink教程

发表于 2024-04-23

Starlink 是由 SpaceX 公司开发的卫星互联网服务平台。它旨在通过将成千上万的卫星部署到地球轨道上,为全球范围内的用户提供高速互联网接入。通过 Starlink,用户可以通过卫星连接接入互联网,无需依赖传统的地面基础设施,这对于偏远地区或地理条件恶劣的地方来说尤为重要。Starlink 的目标是提供更广泛的互联网接入,以减少数字鸿沟,并为全球范围内的用户提供更好的互联网服务。

服务范围:Starlink 提供几乎覆盖地球上任何地方的高速互联网服务。

家庭连接:您可以在家中使用 Starlink 进行连接。

移动连接:即使在移动中,您也可以使用 Starlink 连接互联网。

船上连接:Starlink 甚至可以在水上提供互联网连接。

商业和高级用户:Starlink 还为企业和高级用户提供可靠的高速互联网服务。

快速安装:只需两个步骤即可设置 Starlink:

插上电源。

对准天空。Starlink 需要一个无遮挡的天空视野。您可以下载 Starlink 应用程序以确定最佳安装位置。

无合同:与长期合同不同,Starlink 允许您随时取消,而且价格和条款可以根据需要进行调整。

Starlink 的购买流程相对简单首先,您需要订阅 Starlink 服务。您可以在 Starlink官网 上查看服务的可用性和速度1。订阅费用为每月 120(在大多数地区),并且硬件成本为120(在大多数地区),并且硬件成本为 120(在大多数地区),并且硬件成本为599。

订阅Starlink服务需要使用虚拟信用卡,点击获取

安装硬件:一旦您订阅了服务,您将收到 Starlink 终端设备。您需要将其插入电源并将其对准天空。确保终端有一个无遮挡的视野,以便接收卫星信号。您可以下载 Starlink 应用程序来确定最佳的安装位置。

享受高速互联网:一旦您的终端安装好并连接到卫星,您就可以开始使用高速互联网服务了。Starlink 提供可靠的高速互联网连接,无论您身在何处。

满意度保证:如果您在30天内对服务不满意,您可以申请全额退款。

访问Starlink网站:首先,你需要访问Starlink的官方网站

检查覆盖范围:在网站上,你可以输入你所在的位置信息,以检查Starlink服务是否在你的地区可用。由于Starlink正在不断扩张其卫星网络,因此服务的可用性可能会随时间而改变。

预定:如果你的地区可用,你可以选择预订Starlink服务。通常,预订会要求你支付一定的预付款,这个金额可以在之后的账单中抵扣。预付款的金额会因国家和地区而异。这里我是使用Fomepay进行支付的。

点击获取Fomepay官方虚拟信用卡

微信图片_20240108105643.png

提供信息:在预订过程中,你可能需要提供一些个人信息,例如姓名、地址、联系方式等。

Starlink虚拟信用卡

编辑

等待发货:完成预订后,你将被列入Starlink服务的排队中。一旦你的服务准备就绪并且有可用的设备库存,Starlink将会通过电子邮件或短信通知你。

image.png

安装设备:一旦你收到了Starlink的设备,你可以按照他们提供的安装指南进行安装。这通常包括在合适的位置安装卫星接收器,并将其连接到调制解调器。

激活服务:安装完成后,你需要按照Starlink提供的指南激活你的服务。这可能需要在你的设备上进行一些设置,以连接到Starlink的互联网服务。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

在国内如何快速办理一张visa卡

发表于 2024-04-23

随着社会的发展,大家对visa虚拟卡的办理需求也越来越大,但是我们去银行办理都会花费很多时间,可能还不能办理下来,因为各种原因,不用我说办理过的都体会过

对于亚马逊开多多店铺的,绑定appleid、购买服务器、海淘,等等需求,我这里已经给大家尝试过一种简单的方式办理。

通过线上办理,秒下卡,即开即用

点击获取Fomepay官方虚拟信用卡

开卡步骤也很简单,按图片步骤即可

微信图片_20240108105643.png

卡信息在卡中心cvc安全码,第一次需要自己设置密码,开好之后就可以随意使用,记得用之前要把钱充值到卡里面

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL 80 在线 DDL 深度解析:性能优化与业务连

发表于 2024-04-23

引言

随着数据库技术的发展,越来越多的数据库系统开始支持在线DDL操作,以满足企业对高可用性的需求。

MySQL一直都是有在线DDL的能力的,但是,随着MySQL 8.0 的发布,MySQL对在线 DDL 功能进行了显著性增强,在线 DDL 到底是什么呢,随着作者一起来学习吧。

一、MySQL 8.0 在线 DDL 概述

1.1 在线 DDL 的基本概念

在线 DDL(Data Definition Language)是指在不停止数据库服务,不影响用户对数据库的读写操作的情况下,对数据库结构进行修改的能力。MySQL 8.0 版本对在线 DDL 功能进行了增强,支持更多的 DDL 操作类型,如添加索引、修改列的数据类型等,同时保持了数据库的高可用性。

1.2 与传统 DDL 操作的对比

与传统 DDL 操作的对比,MySQL 8.0 版本对在线 DDL 功能进行了增强,下面将从以下几个方面详细对比一下:

image.png

  • 性能影响:
+ **传统 DDL**:在旧版本的 MySQL 中,许多 DDL 操作(如添加索引)会导致表锁定,使得在操作期间无法进行 DML(Data Manipulation Language)操作,如 `SELECT`, `INSERT`, `UPDATE`, `DELETE` 等,这在生产环境中可能导致业务中断。
+ **在线 DDL**:允许在执行 DDL 操作的同时进行 DML 操作,从而减少了对业务的影响。
  • 资源消耗:
+ **传统 DDL**:可能需要创建临时表,复制原表数据到临时表,然后再将临时表替换原表,这个过程中会消耗更多的磁盘空间和 I/O 资源。
+ **在线 DDL**:通过使用 `ALGORITHM=INPLACE` 或 `ALGORITHM=INSTANT` 选项,可以在不复制数据的情况下对表结构进行修改,减少了资源消耗。
  • 操作类型支持:
+ **传统 DDL**:只支持有限类型的 DDL 操作,对于某些操作(如修改列的数据类型)可能不支持热操作,即不能在线完成。
+ **在线 DDL**:MySQL 8.0 扩展了支持的 DDL 操作类型,包括原子 DDL 操作,如 `INSTANT ADD COLUMN`,这些操作几乎可以瞬间完成,且不会阻塞 DML。
  • 锁定行为:
+ **传统 DDL**:在执行 DDL 时可能会获取长时间的表级锁,导致其他事务无法对表进行操作。
+ **在线 DDL**:通过使用 `LOCK=NONE` 或 `LOCK=SHARED` 选项,可以在保持一定级别的并发性的同时执行 DDL 操作。
  • 主从复制影响:
+ **传统 DDL**:DDL 操作可能导致主从复制延迟,因为从库需要等待 DDL 操作完成后才能继续复制 DML 操作。
+ **在线 DDL**:尽管可以减少锁的持有时间,但是在某些情况下(如使用 `ALGORITHM=COPY`)仍然可能会对复制性能产生一定影响。
  • 易用性:
+ **传统 DDL**:需要 DBA 进行更多的规划和维护,以避免长时间的锁定和复制延迟。
+ **在线 DDL**:简化了 DDL 操作的执行,使得 DBA 可以通过简单的 `ALTER TABLE` 语句执行复杂的结构变更。

二、性能提升与并发性

在线DDL通过减少或消除DDL操作期间对数据库的锁定,显著提升了数据库的性能并增强了并发性。以下是性能提升与并发性的一些关键点:

image.png

  1. 应用程序响应时间的改善:
* 在线DDL允许应用程序继续执行DML操作,如查询和更新,即使在进行DDL更改时也不会被阻塞。这意味着应用程序的响应时间不会因DDL操作而受到显著影响,从而提升了用户体验和应用程序的总体性能。
  1. 减少锁定和等待时间:
* 传统的DDL操作,如添加索引或修改表结构,通常会在表上施加长时间的锁,导致其他事务等待锁释放后才能继续执行。在线DDL通过使用更精细的锁定机制,减少了锁的持续时间和锁定的粒度,从而减少了其他事务的等待时间。
  1. 可伸缩性增强:
* 在线DDL操作减少了对数据库资源的占用,如通过使用`ALGORITHM=INPLACE`选项避免复制整个表的数据。这种资源消耗的减少使得数据库能够更有效地处理高并发工作负载,提高了数据库的可伸缩性。
* 此外,由于减少了锁争用,系统能够支持更多的并发用户和事务,这对于大型分布式系统和高流量应用尤其重要。

三、在线 DDL 关键参数

在线DDL(Data Definition Language)是在数据库运行时修改表结构而不中断对表的访问的能力。

在MySQL中,实现在线DDL主要依赖于ALGORITHM和LOCK参数。

image.png

3.1 ALGORITHM 参数及其用法

ALGORITHM参数指定了执行DDL操作时采用的算法,它直接影响到DDL操作是否会对表进行重建或复制。该参数可以取以下值:

  1. INPLACE:尝试在原有表上进行修改,而不是创建一个新表。这个选项适用于那些不需要重建表的DDL操作,如添加或删除非主键索引,它可以减少IO和CPU消耗,保持DDL期间的良好性能和并发。
  2. COPY:需要拷贝原始表,因此不允许并发DML写操作,但允许读操作。使用COPY算法时,因为需要记录undo和redo日志,并且临时占用buffer pool,所以效率不如INPLACE。
  3. INSTANT:这是MySQL 8.0中引入的新选项,它只修改数据字典中的元数据,不需要拷贝数据,不需要重建表,也不需要加排他MDL锁,几乎瞬间完成且不会阻塞DML4。

3.2 LOCK 参数及其对并发访问的影响

LOCK参数控制了DDL操作期间对表的锁定级别,影响并发DML操作的可行性。该参数可以取以下值:

  1. NONE:允许并发查询和DML。这适用于那些可以执行就地修改的操作,如添加或删除索引。
  2. SHARED:允许并发查询但阻止DML。这可以用于数据仓库表,可以延迟数据加载操作,但不能长时间延迟查询。
  3. DEFAULT:允许尽可能多的并发(查询、DML或两者兼有)。这是默认行为,MySQL会根据操作类型选择最合适的锁定级别。
  4. EXCLUSIVE:阻止并发查询和DML。当主要关注在尽可能短的时间内完成DDL操作,并且不需要并发查询和DML访问时,可以使用此选项。

注意:即使指定了LOCK=NONE,某些操作可能仍然需要短暂的独占锁来准备和完成DDL操作。此外,如果表上有长事务运行,它们可能会阻塞DDL操作,因为DDL操作需要获取元数据锁来修改表结构。

四、支持的在线 DDL 操作

MySQL 8.0 引入了对在线 DDL (Data Definition Language) 的支持,这使得数据库管理员和开发者可以在不停止数据库服务的情况下执行 DDL 操作。以下是 MySQL 8 支持的一些在线 DDL 操作的简要说明:

image.png

4.1 索引操作

  • 在线添加或删除索引,包括普通索引和唯一索引。
  • 在线重建索引,这允许在不锁定表的情况下对索引进行重建,减少了对数据库性能的影响。

4.2 主键和外键操作

  • 在线添加或删除主键。
  • 在线修改外键约束,包括添加、删除或修改外键关系。

4.3 列操作

  • 在线添加列,允许在表中添加新的列而不影响现有数据和查询。
  • 在线删除列,可以移除不再需要的列,同时保持表的其余部分可用。
  • 在线修改列,可以更改现有列的数据类型、大小或其他属性。

4.4 表结构操作

  • 在线重命名表,允许更改表的名称而不影响数据库的其他操作。
  • 在线修改表的字符集或校对规则。

注意:尽管许多 DDL 操作可以在线进行,但某些特定的操作,如修改表的存储引擎,仍然可能需要暂时锁定表。

五、即时操作(INSTANT)

5.1 即时操作的优势

image.png

  1. 性能提升:即时操作仅修改数据字典中的元数据,不影响表数据,因此操作速度极快,通常在毫秒级别完成。
  2. 并发DML支持:在执行即时操作时,允许同时进行数据插入、更新和删除操作,这在高并发系统中非常有用。
  3. 无需锁定:使用 INSTANT 算法的操作不需要对表进行长时间的锁定,这减少了对其他数据库操作的影响。
  4. 减少资源消耗:因为不需要复制或重建整个表,所以对 CPU、内存和 I/O 的要求较低。

5.2 从 MySQL 8.0.12 版本开始的改进

在 MySQL 8.0.12 版本之前,InnoDB 的 DDL 操作通常需要复制或重建表,这在大型表上可能会非常耗时。从 8.0.12 版本开始,引入了 INSTANT 算法,带来了以下改进:

  • 元数据字典的改进:MySQL 8.0 迁移到了新的事务数据字典,这使得即时 DDL 成为可能5。
  • 操作的即时性:添加列操作现在可以即时完成,而不需要逐行复制表数据5。
  • 数据字典的扩展:为了支持即时操作,数据字典被扩展以存储更多元数据,例如 information_schema.innodb_tables 表中新增了 instant_cols 列来记录表中即时列的数量5。
  • 更多的操作支持:随着版本的提升,更多的 DDL 操作类型开始支持即时算法,例如从 8.0.28 版本开始,重命名列的操作也开始支持 INSTANT 算法10。

六、并发 DML 操作

在 MySQL 8 中,对 DDL(Data Definition Language)操作的支持得到了显著增强,特别是在与 DML(Data Manipulation Language)操作的并发执行方面。以下是 MySQL 8 中 DDL 与 DML 并发执行的能力和对业务连续性的影响:

6.1 DDL 执行期间执行 DML 操作的能力

  • 在 MySQL 8.0 及更高版本中,引入了 ALGORITHM=INSTANT 选项,使得某些 DDL 操作(如添加或删除列)可以瞬间完成,而不需要复制或锁定整个表。这样,DML 操作可以与这些 DDL 操作并发执行,从而提高了数据库的可用性。
  • ALGORITHM=INPLACE 选项允许 DDL 操作在不复制表数据的情况下进行,通常支持并发 DML 操作。这种算法避免了与表复制方法相关的磁盘 I/O 和 CPU 周期,从而最小化数据库的总体负载。

6.2 对业务连续性的影响

  • 通过允许 DDL 和 DML 操作并发执行,MySQL 8 减少了因结构变更导致的业务中断时间。这对于 24/7 运行的业务尤其重要,因为它们需要最小的停机时间来进行维护操作。
  • 在线 DDL 操作可以减少锁定和等待 MySQL 服务器资源的时间,从而提高操作的响应速度并增加整体的可伸缩性。
  • 尽管 ALGORITHM=INSTANT 提供了即时操作,但并非所有 DDL 操作都支持这种算法。例如,修改表的存储引擎或对大表进行某些类型的索引操作可能仍然需要使用 ALGORITHM=COPY,这可能会阻塞 DML 操作。

七、元数据锁和性能监控

7.1 元数据锁的作用和类型

元数据锁(Metadata Lock,简称MDL)是MySQL中用于确保数据库对象在DDL和DML操作期间的一致性和完整性的机制。MDL锁的作用范围可以是表、模式、存储过程、函数、触发器、计划事件,甚至是表空间等。

MDL锁有两种基本类型:

  1. MDL读锁:当对表进行SELECT或DML操作(如UPDATE、INSERT、DELETE)时,MySQL会自动给表加上MDL读锁。读锁允许其他线程继续读取表的元数据,但不允许修改表结构。
  2. MDL写锁:当执行DDL操作,如ALTER TABLE或CREATE INDEX时,MySQL会给表加上MDL写锁。写锁意味着只有持有锁的线程可以修改表结构,其他线程既不能修改结构也不能执行DML操作。

7.2 使用performance_schema.metadata_locks表进行监控

MySQL的performance_schema数据库提供了一个metadata_locks表,该表记录了当前所有活动的MDL锁及其相关信息,可以用于监控和分析MDL锁的使用情况。

通过查询metadata_locks表,可以获得以下信息:

  • 持有MDL锁的线程ID。
  • MDL锁的类型(读或写)。
  • 被锁定的数据库对象名称。
  • 锁的持续时间等。

启用和使用metadata_locks表的步骤如下:

  1. 确保performance_schema已经启用,这可以通过查询performance_schema.setup_instruments表来确认。
  2. 直接查询metadata_locks表来查看当前的MDL锁状态:
1
SELECT * FROM performance_schema.metadata_locks;
  1. 如果需要监控特定的MDL锁事件,可以通过performance_schema.setup_consumers和performance_schema.setup_instruments表来启用相应的事件监控。

八、复制阻塞问题

8.1 在线 DDL 与复制延迟的关系

在 MySQL 中,执行在线 DDL(Data Definition Language)操作时,为了保证数据的一致性和完整性,DDL 操作通常会在主从复制架构中引入延迟。这是因为:

  1. DDL 操作的原子性:DDL 操作如 ALTER TABLE 通常被视为一个原子操作,它需要在主服务器上完整执行后,才能将变化复制到从服务器。
  2. 复制线程的阻塞:在从服务器上,DDL 操作在默认情况下是由单个 SQL 线程顺序执行的。这意味着,当一个 DDL 操作正在进行时,它可能会阻塞后续的 DML 操作的回放,导致复制延迟。
  3. 日志记录:在线 DDL 操作会在 binlog 中记录,如果操作复杂或涉及大量数据,可能会产生大量的日志,从而增加从服务器处理的时间,进一步加剧延迟。

8.2 如何减少复制阻塞的影响

为了减少复制阻塞的影响,可以采取以下措施:

image.png

  1. 使用更快的硬件:提升主从服务器的硬件性能,尤其是 SSD 存储,可以加快 DDL 操作的执行速度,从而减少延迟。
  2. 优化 DDL 操作:尽可能在低峰时段执行 DDL 操作,减少对业务的影响。同时,优化 DDL 操作的复杂性,例如,避免对大表进行可能导致长时间锁定的操作。
  3. 并行复制:MySQL 5.6 引入了并行复制的概念,通过多个工作线程并行回放 relay log 中的事件,可以提高复制的效率。
  4. WriteSet 复制:MySQL 8.0 引入了基于 WriteSet 的并行复制方案,该方案可以更有效地利用从服务器的资源,减少单个大事务对复制延迟的影响。
  5. 使用第三方工具:使用如 pt-online-schema-change 或 gh-ost 这样的工具可以在复制架构中进行在线 DDL 而不影响业务查询,因为它们通过双写集机制来保持数据一致性。
  6. 调整复制配置:调整 sync_binlog 和 innodb_flush_log_at_trx_commit 参数,优化 redo log 和 binlog 的写入策略,可以在一定程度上减少I/O操作对复制延迟的影响。
  7. 避免大事务:尽可能避免长时间运行的事务,因为它们会锁定资源并导致复制延迟。
  8. 监控和预警:实施有效的监控系统,以便在复制延迟发生时及时发现并采取措施。
  9. 使用组提交:在 MySQL 中,可以使用组提交(Group Commit)技术来减少磁盘 I/O 操作的次数,从而提高性能。

九、与第三方工具的比较

下面是一个使用Markdown格式的表格,包含了与pt-osc、gh-ost和MySQL 8.0原生Online DDL功能相关的性能对比内容:

对比点 MySQL 8.0原生Online DDL pt-osc gh-ost
性能 优秀 良好 良好
存储开销 低 高 高
易用性 简便 中等 中等
主从复制延迟 低 中等 中等
复制阻塞问题 存在 较小 较小

十、总结

MySQL 8.0 的在线 DDL 功能显著提升了数据库维护的便捷性和性能。它通过减少锁定和支持 DDL 与 DML 操作的并发执行,降低了数据库维护对业务的影响。

此外,新引入的 INSTANT 算法使得某些 DDL 操作能瞬间完成,减少了资源消耗。这些改进不仅提高了数据库的可用性和性能,还简化了数据库的管理和优化工作,对数据库维护和开发产生了积极影响。

当然,不是说MySQL8做了升级你就可以为所欲为了,实际操作中我们还是要贴合实际情况,比如尽量不要在业务高峰期执行在线 DDL;总之一句话,稳着点,不要挑战极限。

希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。

同时,如果您觉得这篇文章有价值,请考虑点赞和收藏。这将激励我进一步改进和创作更多有用的内容。

感谢您的支持和理解!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

AI-GC-手把手教你写一个小说推文生成器(对接生成大模型)

发表于 2024-04-23

前言😀😀

昨天的文章,展示了项目的页面是如何制作的,那么显然接下来我们要实现的就是如何来对接到我们的大模型,我们要对接的大模型主要有两个:

  1. 对接moonshot
  2. 对接中转站

同时在对接中转站时要注意,我们将对接两个服务:

  1. chatgpt服务,完成翻译任务
  2. MJ绘图服务

所以接下来,我们将演示如何对接,在成功对接到我们的模型之后,我们就可以来基于模型来做一点处理了。当然,实际上,如果你本地也有模型,并且可以通过one_api进行本地化部署的话,那么就更酷了。当然,为了能够让更多的小伙伴能够正常运行这个项目,我们还是选择中转站。

那么,接下来就开始吧,我们要如何完成对接。同时在对接完成之后,我们怎么实现到我们的聊天机器人。之后在下一章节,我们将讨论,如何将这些内容与我们具体的业务服务相关联。

对接moonshot

moonshot的对接还是非常友好的,它提供了标准的openai接口格式。因此我们直接使用openai这个库就可以直接调用。你只需先下载即可,然后获取到key,并且选择到模型。
只需要这样就可以轻松完成对接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码    def signChat(self,history):
history_openai_format = []
# 先加入系统信息
history_openai_format.append(
{"role": "system",
"content": Config.settings.get("system_xiaoxi")
},
)
# 再加入解析信息
history_openai_format.extend(history)
# print(history_openai_format)
completion = client.chat.completions.create(
model=Config.settings.get("default_model"),
messages=history_openai_format,
temperature=Config.settings.get("temperature"),
)
result = completion.choices[0].message.content
return result

在这里,我们先预设了角色:

1
python复制代码    "system_xiaoxi": "你是一个全能小助手,你的名字叫小汐,尤其擅长写作和故事改编。"

那么到这里,一个moonshot就对接好了。当然这还不够,为了方便使用,我们还是要进行简单封装的。

在这里的话,还是可以看到,这里还可以进行流式对话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
python复制代码import time
from openai import OpenAI

from utils import Config
api_key = Config.settings.get("openai_api_key")
client = OpenAI(api_key=api_key,base_url=Config.settings.get("openai_api_base"))


class ChatBotHandler(object):
def __init__(self, bot_name="chat"):
self.bot_name = bot_name
self.current_message = None


def user_stream(self,user_message, history):
self.current_message = user_message
return "", history + [[user_message, None]]

def bot_stream(self,history):

if(len(history)==0):
history.append([self.current_message,None])
bot_message = self.getResponse(history[-1][0],history)
history[-1][1] = ""
for character in bot_message:
history[-1][1] += character
time.sleep(0.02)
yield history

def signChat(self,history):
history_openai_format = []
# 先加入系统信息
history_openai_format.append(
{"role": "system",
"content": Config.settings.get("system_xiaoxi")
},
)
# 再加入解析信息
history_openai_format.extend(history)
# print(history_openai_format)
completion = client.chat.completions.create(
model=Config.settings.get("default_model"),
messages=history_openai_format,
temperature=Config.settings.get("temperature"),
)
result = completion.choices[0].message.content
return result

def getResponse(self,message,history):
history_openai_format = []
for human, assistant in history:
# 基础对话的系统设置
history_openai_format.append(
{"role": "system",
"content":Config.settings.get("system_xiaoxi")
},
)
if(human!=None):
history_openai_format.append({"role": "user", "content": human})
if(assistant!=None):
history_openai_format.append({"role": "assistant", "content": assistant})

completion = client.chat.completions.create(
model=Config.settings.get("default_model"),
messages=history_openai_format,
temperature=Config.settings.get("temperature"),
)
result = completion.choices[0].message.content
return result


def chat(self,message, history):
history_openai_format = []
for human, assistant in history:
history_openai_format.append({"role": "user", "content": human})
history_openai_format.append({"role": "system", "content": assistant})
history_openai_format.append({"role": "user", "content": message})

response = client.chat.completions.create(model="moonshot-v1-8k",
messages=history_openai_format,
temperature=1.0,
stream=True)

partial_message = ""
for chunk in response:
if chunk.choices[0].delta.content is not None:
partial_message = partial_message + chunk.choices[0].delta.content
yield partial_message

对接中转站

之后就是对接中转站点。这里我们是对接两个服务,一个还是openai的服务,还有一个就是我们的绘图的服务。

对接chat

这里的对接,略有不同,但是总体上还是类似,这里我们需要使用到request,用比较原始的方式进行对接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
python复制代码import requests
import json

config = getConfig()

class MyOpenAI():

def __init__(self):
self.url = "https://api.openai-hk.com/v1/chat/completions"

self.headers = {
"Content-Type": "application/json",
# 这里采用的是中转站的openai key
"Authorization": "Bearer "+config.get("image_api_key")
}

def chat(self,message,prompt,temperature=0.8):
data = {
"max_tokens": 1200,
"model": "gpt-3.5-turbo",
"temperature": temperature,
"top_p": 1,
"presence_penalty": 1,
"messages": [
{
"role": "system",
"content":prompt
},
{
"role": "user",
"content": message
}
]
}

response = requests.post(self.url, headers=self.headers, data=json.dumps(data).encode('utf-8'))
result = response.content.decode("utf-8")
result = json.loads(result)
result = result["choices"][0]["message"]["content"]
return result

当然,这里如何对接的话,也是有文档的,这里只是为了方便使用进行一个简单封装而已。

对接绘图

之后的话,我们要对接到我们的绘图api。
这里的话,对接绘图的话有三个步骤。

  1. 提交任务
  2. 查看任务进度,获取图片地址
  3. 访问图片地址拿到图片

当然同样,这个在对方的文档当中是可以看到的。这里也是进行了封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
python复制代码
class Text2Image():
def __init__(self):
# 构建请求头
self.headers = {
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
}

self.current_file_path = os.path.abspath(__file__)
self.current_dir = os.path.dirname(self.current_file_path)
self.resource_dir = self.current_dir+"/../resource"

def get_taskId(self,prompt):

def send(prompt,headers):
data = {
"base64Array": [],
"instanceId": "",
"modes": [],
"notifyHook": "https://ww.baidu.com/notifyHook/back",
"prompt": prompt,
"remix": True,
"state": ""
}

response = requests.post(
url='https://api.openai-hk.com/fast/mj/submit/imagine',
headers=headers,
data=json.dumps(data)
)
return response.json()
try:
result = send(prompt, self.headers)
except Exception as e:
result = {'code':-1}
if result.get('code') == 1:
return result.get("result")
else:
return None

#1713283471368561
def get_Image(self,task_id):
url = f'https://api.openai-hk.com/fast/mj/task/{task_id}/fetch'

# 发送GET请求
response = requests.get(url, headers=self.headers)
return response.json()

def __create_img_stream(self):

now = datetime.now()
year_month_day = now.strftime("%Y%m%d")
file_uuid = uuid.uuid4()
audio_stream = self.resource_dir + "/img" + "/" + year_month_day + "/"
if (not os.path.exists(audio_stream)):
os.makedirs(audio_stream)
audio_stream += file_uuid.hex + ".jpg"
return audio_stream


def __getImg(self,url):
response = requests.get(url)
response.raise_for_status()
image = Image.open(BytesIO(response.content))
width, height = image.size
quarter_width = width // 2
quarter_height = height // 2
# 裁剪左上角的四分之一图片
cropped_image = image.crop((0, 0, quarter_width, quarter_height))
return cropped_image


# 只要这个任务执行失败,那么我们就返回为空
def text2image(self,prompt):
# 先拿到task_id
task_id = self.get_taskId(prompt)
if(task_id):
return self.__text2image(prompt,task_id)
else:
return None

def __text2image(self,prompt,task_id):

res = self.get_Image(task_id)
# 执行失败
if(res.get("status")=="FAILURE"):
return None
if(res.get('progress') == "100%"):
return self.__getImg(res.get("imageUrl"))
else:
# 还在生成,等待一会再去重试呗,调用api生成还是比较慢的
time.sleep(2)
return self.__text2image(prompt,task_id)

这里可以给大家展示一下,当任务提交之后,去拿到图片,会返回给我们的数据格式是怎么样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
json复制代码       response_data = {
'id': '1713283927285806',
'properties': {
'discordChannelId': '1222483390712774667',
'botType': 'MID_JOURNEY',
'notifyHook': 'https://www.open-hk.com/openai/mjapi/16158-567/https%3A%2F%2Fww.baidu.com%2FnotifyHook%2Fback',
'discordInstanceId': '1500442604632883200',
'flags': 0,
'messageId': '1229828231935426650',
'messageHash': 'b1290620-0d25-4882-a72d-102dc174fc22',
'nonce': '1501375981829570560',
'finalPrompt': 'a black cat',
'progressMessageId': '1229827516609331220',
'messageContent': '**a black cat** - <@1222482757389910027> (fast)'
},
'action': 'IMAGINE',
'status': 'SUCCESS',
'prompt': 'a black cat',
'promptEn': 'a black cat',
'description': '/imagine a black cat',
'submitTime': 1713283927285,
'startTime': 1713284128607,
'finishTime': 1713284300009,
'progress': '100%',
'imageUrl': 'https://proxy.xjai.top:33330/mjcdn/attachments/1222483390712774667/1229828230979129374/xizaizai0902_a_black_cat_b1290620-0d25-4882-a72d-102dc174fc22.png?ex=663119cb&is=661ea4cb&hm=1657fcc1bfd3f971fd2d9349ee8b5442a2b300f95e13ab72cee662d76e09789a&',
'failReason': None,
'state': '16158',
'buttons': [
{
'customId': 'MJ::JOB::upsample::1::b1290620-0d25-4882-a72d-102dc174fc22',
'emoji': '',
'label': 'U1',
'type': 2,
'style': 2
},
{
'customId': 'MJ::JOB::upsample::2::b1290620-0d25-4882-a72d-102dc174fc22',
'emoji': '',
'label': 'U2',
'type': 2,
'style': 2
},
{
'customId': 'MJ::JOB::upsample::3::b1290620-0d25-4882-a72d-102dc174fc22',
'emoji': '',
'label': 'U3',
'type': 2,
'style': 2
},
{
'customId': 'MJ::JOB::upsample::4::b1290620-0d25-4882-a72d-102dc174fc22',
'emoji': '',
'label': 'U4',
'type': 2,
'style': 2
},
{
'customId': 'MJ::JOB::reroll::0::b1290620-0d25-4882-a72d-102dc174fc22::SOLO',
'emoji': '🔄',
'label': '',
'type': 2,
'style': 2
},
{
'customId': 'MJ::JOB::variation::1::b1290620-0d25-4882-a72d-102dc174fc22',
'emoji': '',
'label': 'V1',
'type': 2,
'style': 2
},
{
'customId': 'MJ::JOB::variation::2::b1290620-0d25-4882-a72d-102dc174fc22',
'emoji': '',
'label': 'V2',
'type': 2,
'style': 2
},
{
'customId': 'MJ::JOB::variation::3::b1290620-0d25-4882-a72d-102dc174fc22',
'emoji': '',
'label': 'V3',
'type': 2,
'style': 2
},
{
'customId': 'MJ::JOB::variation::4::b1290620-0d25-4882-a72d-102dc174fc22',
'emoji': '',
'label': 'V4',
'type': 2,
'style': 2
}
]
}

对话助手实现😊

现在我们具备了对接大语言模型的能力,那么接下来我们要做的就是,将这个东西整合到我们的应用当中。
这一点不难,所以我们直接看到昨天对话部分的代码即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
python复制代码class AssistantNovel(object):

def __init__(self):
self.chat = ChatBotHandler()

def get_response(self,prompt, history):
return self.chat.signChat(history)

def clear_chat_history(self):
st.session_state.messages = [{"role": "assistant", "content": "🍭🍡你好!我是全能创作助手~小汐🥰,可以帮助您完善补充文案细节?🧐"}]

def page(self):
# 主聊天对话窗口
prompt = st.chat_input(placeholder="请输入对话")

if "messages" not in st.session_state.keys():
st.session_state.messages = [{"role": "assistant", "content": "🍭🍡你好!我是全能创作助手~小汐🥰,可以帮助您完善补充文案细节?🧐"}]

for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.write(message["content"])
if prompt:
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.write(prompt)

if st.session_state.messages[-1]["role"] != "assistant":
with st.chat_message("assistant"):
with st.spinner("Thinking..."):
try:
response = self.get_response(prompt,st.session_state.messages)
except Exception as e:
print(e)
response = "哦┗|`O′|┛ 嗷~~,出错了,请稍后再试!😥"
placeholder = st.empty()
full_response = ''
for item in response:
full_response += item
time.sleep(0.01)
placeholder.markdown(full_response)
placeholder.markdown(full_response)
message = {"role": "assistant", "content": full_response}
st.session_state.messages.append(message)
st.button('清空历史对话', on_click=self.clear_chat_history)

这里我们只是将

1
python复制代码response = self.get_response(prompt,st.session_state.messages)

替换为了我们刚刚写好的接口,这样就完成了对接。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

✅MySQL的脏读、幻读、不可重复度是什么

发表于 2024-04-23

简而言之

脏读:指读取了其他事务尚未提交的数据,可能导致不一致性。

不可重复读:在对数据进行读取的过程中,有其他事务对数据进行了修改(UPDATE、DELETE),导致第二次读取的结果与第一次不一致。

幻读:指一个事务在进行范围查询时,另一个事务在该范围内进行新增操作(INSERT),导致范围查询的结果数目不一致。

什么是脏读

脏读又称为无效数据读取,指在数据库访问中,事务T1修改了某个数值,随后事务T2读取了该数值,而后因某种原因,T1撤销了对该数值的修改,导致T2读取到的数据变为无效。

具体而言,脏读是指一个事务正在访问数据并对其进行修改,但这些修改尚未提交到数据库中。此时,另一个事务也访问该数据,并使用了它。由于这些数据尚未提交,另一个事务所读取的数据就会成为脏数据,基于这些脏数据所做的操作可能会产生不正确的结果。

什么是幻读

幻读是指在事务非独立执行时出现的现象,举例来说,第一个事务对表中的数据进行了修改,涉及到表中的“全部数据行”。与此同时,第二个事务也修改了该表的数据,插入了“一行新数据”。随后,操作第一个事务的用户发现表中仍然存在未修改的数据行,就好像出现了幻觉一般。

一般解决幻读的方法是通过增加范围锁(RangeS),将检测锁的范围限定为只读,这样便可以避免幻读的发生。

值得注意的是,幻读是不可重复读的一种特殊情况:在事务没有获取范围锁的情况下执行SELECT … WHERE操作时可能会导致幻读现象的发生。

什么是不可重复读

不可重复读是指在数据库访问中,一个事务内进行两次相同的查询却返回了不同的数据。这种现象是由于系统中其他事务的提交修改所引起的。例如,事务T1读取某一数据,事务T2读取并修改了该数据,随后T1为了检验读取值再次读取该数据,结果获取到不同的数值。

更通俗易懂的说法是:在一个事务中多次读取同一数据,在该事务未结束之前,另一个事务也访问同一数据。在第一个事务两次读取数据之间,由于第二个事务的修改,导致第一个事务读取到的数据可能不同,这就导致了在同一个事务内两次读取数据的结果不一致,因此称为不可重复读,即原始读取结果不可重复。

扩展知识之事务隔离级别

脏读、不可重复读和幻读这三种异常现象是在SQL-92标准中定义的,同时,SQL-92标准还确定了4种隔离级别来处理这些异常情况,按照严格程度从高到低排列分别为:顺序执行(Serializable)、可重复读(Repeatable reads)、提交读(Read committed)、未提交读(Read uncommitted)。

如有问题,欢迎加微信交流:w714771310,备注- 技术交流 。或微信搜索【码上遇见你】。

免费的Chat GPT可微信搜索【AI贝塔】进行体验,无限使用。

好了,本章节到此告一段落。希望对你有所帮助,祝学习顺利。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微软刚开源就删库的WizardLM-2:MT-Bench 榜

发表于 2024-04-23

前言

微软最近发布的WizardLM-2大型语言模型因其先进的技术规格和短暂的开源后突然撤回,引起了科技界的广泛关注。WizardLM-2包括三个不同规模的模型,分别是8x22B、70B和7B,均展现了在多语言处理、复杂对话、推理和代理任务上的卓越能力。

  • Huggingface模型下载: huggingface.co/MaziyarPana…
  • AI快站模型免费加速下载:aifasthub.com/models/Mazi…

模型性能和架构

WizardLM-2系列模型在多个基准测试中表现出色。其中,7B版本在基准任务上与Qwen1.5-32B相当;70B版本超过了同类的GPT-4-0613;最高规格的8x22B版本则在MT-Bench上取得了9.12的高分,超越了所有现有的GPT-4版本。这些成绩彰显了微软在模型优化和多任务处理技术上的领先地位。

独特的训练方法

WizardLM-2的训练方法体现了多个创新点:

  • 加权抽样和数据预处理: 微软通过分析数据源中不同属性的分布情况,并通过加权抽样调整训练数据中各属性的权重,使得最终的数据集更符合实际应用场景的需要。
  • 渐进式学习: 与传统的全量数据训练不同,微软采用渐进式学习方法,通过逐步增加训练数据的复杂性,使模型能在较少的数据中学到更有效的信息。
  • Evol Lab和AI Align AI: 这一框架允许多个最先进的语言模型相互教学和改进。Evol-Instruct和Evol-Answer的方法使模型能自动生成高质量的指令并优化响应。

训练阶段的详细创新

  • Evol-Instruct和Evol-Answer: 这两种方法通过重新设计和评估指令生成过程,增强了模型生成指令的质量和响应的相关性。
  • 监督学习与强化学习的结合使用: 通过结合使用监督学习和强化学习,微软优化了模型的学习过程。特别是,通过Stage-DPO和RLEIF技术,模型能在离线和在线环境下进行更为精确的学习和优化。

撤回原因与未来展望

尽管WizardLM-2在技术上取得了显著进展,但微软因忘记进行毒性测试而短暂撤回了模型。这一事件突显了在开发和部署前对AI模型进行全面测试的重要性,确保技术的安全性和可靠性。

结论

WizardLM-2的开发和短暂撤回事件虽然带来了一定的争议,但也展示了微软在人工智能领域的强大实力和对高标准的承诺。预计在完成必要的测试和优化后,这些模型将为AI研究和应用带来新的可能性,特别是在处理多语言和复杂交互任务方面。微软的这一步也可能推动整个行业向更开放、更安全的AI应用方向迈进。

模型下载

Huggingface模型下载

huggingface.co/MaziyarPana…

AI快站模型免费加速下载

aifasthub.com/models/Mazi…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于 Flexbox 的纯 CSS 框架:兼容性好、文档丰富

发表于 2024-04-23

picture

jgthms/bulma

Stars: 48.3k License: MIT

picture

bulma 是基于 Flexbox 的现代 CSS 框架。

  • 基于 Flexbox 技术。
  • 提供快速安装方式,支持 NPM、Yarn 和 Bower。
  • 仅包含 CSS 文件,没有 JavaScript 部分。
  • 兼容性良好,在主流浏览器上运行良好。
  • 提供丰富的文档和在线文档浏览功能。

emilk/egui

Stars: 19.0k License: Apache-2.0

picture

egui 是一个易于使用的 Rust 纯 GUI 库。
egui 旨在成为最易于使用的 Rust GUI 库,以及用 Rust 制作 Web 应用程序的最简单方式。它可以在 Web 上、本地和您喜爱的游戏引擎中运行。

  • 可以在 Web 上运行
  • 在 Rust 中制作 Web 应用程序
  • 可以轻松集成到游戏引擎中
  • 目标是成为最易于使用 GUI 库

Wilfred/difftastic

Stars: 18.6k License: MIT

picture

difftastic 是一个理解语法的结构化差异工具。

  • 支持超过 30 种编程语言
  • 能够识别代码嵌套、对齐和换行等特性
  • 可以用于检查合并冲突和语法更改,而不仅仅是普通的文本差异比较

Unity-Technologies/EntityComponentSystemSamples

Stars: 6.7k License: NOASSERTION

EntityComponentSystemSamples 是一个展示 DOTS(Data-Oriented Technology Stack)的示例项目。

  • 提供了实体、网络编码、物理等方面的示例
  • 包含了学习 DOTS 的推荐顺序和基础概念介绍视频
  • 提供了各种入门样本,如作业教程、HelloCube 示例等
  • 包括有关 Baking、流式传输和其他主题的样本
  • 提供基本 API 使用的代码片段和速查表

hiyouga/LLaMA-Factory

Stars: 6.5k License: Apache-2.0

LLaMA Factory 是一个用于训练和评估大型语言模型的开源项目,主要功能包括提供一站式 Web UI 来快速上手 LLaMA Factory、支持多种训练方法 (如预训练、监督微调等)、以及提供各种数据集。该项目的核心优势和关键特点包括:

  • 支持 NEFTune 技巧进行微调
  • 提供了 S^2-Attn 注意力机制
  • 集成 MMLU、C-Eval 和 CMMLU 基准测试
  • 支持使用 FlashAttention-2 加速模型在 RTX4090,A100 或 H100 GPU 上运行

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL-DDL与DML语言

发表于 2024-04-23

一.认识SQL语句


🤡我们在数据库中希望操作数据库中的数据,就需要有和数据库直接沟通的语言,这个语言就是SQL:

  1. SQL称之为结构化查询语言。
  2. 使用SQL编写出来的语句,就称之为SQL语句。
  3. SQL语言用于对数据库进行操作。

😭事实上,常见的关系型数据库SQL语句都是比较相似的,所以你学会了MySQL中的SQL语句,之后去操作oracle或者其他关系型数据库,也是非常的方便。

🔔SQL语句的常用规范:

  1. 通常关键字使用大写的,比如CREATE TABLE SHOW等等;
  2. 一条语句结束后,需要以;结尾;
  3. 如果遇到关键字作为表明或者字段名称可以使用``包裹;

二.SQL语句的分类


😊常见的SQL语句分为四类:

  1. DDL语句:数据定义语言,可以通过DDL语句对数据库或者表进行,创建,删除,修改操作。
  2. DML语句:数据操作语言,可以通过DML语句进行添加,删除,修改等操作。
  3. DQL语句:数据查询语言,可以通过DQL从数据库中查询记录(重点)。
  4. DCL语句:数据控制语言,对数据库,表格的权限进行相关性访问控制操作。

三.DDL之对数据库进行操作


🎯首先我们需要在Navicat的某个数据库下新建一个查询,就可以在查询界面执行相应的SQL语句。

  1. 查看当前所有的数据库;
1
sql复制代码SHOW DATABASES;

  1. 使用某一个数据库
1
sql复制代码USE coderhub;

  1. 查看当前使用的数据库;
1
sql复制代码SELECT DATABASE();
  1. 创建一个数据,但是这样当有了这个数据库的时候会报错的,我们更推荐使用下方的写法。
1
sql复制代码CREATE DATABASE IF NOT EXISTS mycode;
  1. 删除某一个数据库,如果数据库不存在就会报错,我们需要判断是否存在。
1
sql复制代码DROP DATABASE IF EXISTS mycode;
  1. 修改数据库(不常用了解即可)
1
sql复制代码ALTER DATABASE coderhub CHARACTER SET = utf8 COLLATE = utf8_unicode_ci;

四.DDL之对表进行操作


🥴查看当前数据库中有哪些表。

1
sql复制代码SHOW TABLES;

🎪查看某张表的结构。

1
sql复制代码DESC user;

😶‍🌫️创建一张新的表。

1
2
3
4
5
sql复制代码CREATE TABLE IF NOT EXISTS `grades`(
name VARCHAR(10),
age INT,
height DOUBLE
)

😊删除某张表

1
sql复制代码DROP TABLE IF EXISTS `user`;

🥴创建一个完整的表结构

1
2
3
4
5
6
sql复制代码CREATE TABLE IF NOT EXISTS `myuser`(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) UNIQUE NOT NULL,
level INT DEFAULT 0,
telPhone VARCHAR(20) UNIQUE NOT NULL
);

五.SQL数据类型-数字类型


👽我们知道不同的数据类型的数据会划分为不同的数据类型,在数据库中也一样,MySQL支持的数据库的书库类型有:数字类型日期和时间字符串类型(字符和字节)空间类型和JSON数据类型。

  1. 数字类型:MySQL对数字类型支持的有很多

  1. 浮点数字类型:FLOAT DOUBLE(FLOAT是四个字节,DOUBLE是八个字节)
  2. 精确数据类型:DECIMAL NUMBERIC(DECIMAL是NUMERIC的实现形式)

六.SQL数据类型-日期类型


🤡MYSQL的日期类型也很多

  1. YEAR以YYYY格式显示范围是1901到2155,和0000;
  2. DATE类型用于具有日期部分但是没有时间部分的值,显示格式是YYYY-MM-DD显示值。
  3. DATETIME用于表示具有时间部分的日期,格式是YYYY-MM-DD hh:mm:ss显示值。
  4. TIMESTAMP数据用于包含日期时间的值,但是范围是UTC的时间范围1970-2038。

七.SQL数据类型-字符串类型


😭MySQL的字符串类型表示方式如下:

  1. CHAR类型在创建表时为固定长度,长度可以是0-255之间的任意值,在查询时候会删除后面的空格。
  2. VARCHAR类型的值是可变长度的字符串,长度可以指定0-65535之间的值,在被查询时候不会删除空格。
  3. BINARY和VARBINARY类型用于存储二进制字符串,存储的是字节字符串。
  4. BLOB用于存储的二进制类型。
  5. TEXT用于存储大的字符串类型。

八.表约束


😊PRIMARY KEY:主键,在一张表中我们为了区分每一条数据的唯一性,必须有一个字段是永远不会重复的,并且不能为空,这个字段我们通常将它设置为主键。

  1. 主键是表中唯一的索引;
  2. 并且必须是NOT NULL的,如果没有设置NOT NULL,那么MySQL也会隐式设置为NOT NULL;
  3. 主键也可以是多列索引,PRMARY KEY(key_part…)我们一般称之为联合主键。
  4. 建议:开发中的组件字段应该是和业务无关的,尽量不要使用业务字段来做主键。

📆UNIQUE:某些字段在开发中我们希望是唯一的,不会重复的,

  1. 比如手机号,身份证号码,这些字段我们使用UNIQUE来约束,使用UNIQUE字段在表中必须是不同的。
  2. UNIQUE索引允许NULL包含的列具有多个值NULL。

🔔NOT NULL:不能为空。

  1. 某些字段我们要求用户必须插入值,不可以为空,这个时候我们就可以使用NOT NULL来约束。

🎯DEFAULT:默认值

  1. 某些字段我们希望在没有设置值的时候给予一个默认值,这个时候我们就可以使用DEFAULT来完成。

🎪AUTO_INCREMENT:自动增长。

  1. 某些字段我们不希望设置值的时候我们可以使用自动增长。

🚨外键也是最常见的一种表约束,等我们讲到多表关系的时候再进行讲解。

九.修改表结构


😊修改表结构:修改表名。

1
sql复制代码ALTER TABLE `myuser` RENAME TO `user`;

👽添加一个新的列

1
2
sql复制代码ALTER TABLE `user` ADD `publishTime` DATETIME;
ALTER TABLE `user` ADD `updateTime` DATETIME;

😶‍🌫️删除一列数据

1
sql复制代码ALTER TABLE `user` DROP `updateTime`;

🎯修改列的名称

1
sql复制代码ALTER TABLE `user` CHANGE `publishTime` `publishDate` DATE;

🤡修改列的数据类型

1
sql复制代码ALTER TABLE `user` MODIFY `id` INT;

十.什么是DML语句


🤡DML语句是数据库中的作用是进行数据库的操作,也被称为数据库操作语句。

1
2
3
4
5
6
7
sql复制代码CREATE TABLE IF NOT EXISTS `products`(
`id` INT PRIMARY KEY AUTO_INCREMENT,
`title` VARCHAR(20),
`decription` VARCHAR(200),
`price` DOUBLE,
`publishTime` DATETIME
)

十一.插入数据


😁DML插入语句,向数据库中插入一条数据。

1
2
3
sql复制代码INSERT INTO `products`(title,decription,price,publishTime) VALUES ('iphone100','iphone100只要价格998',998,'2021-10-09');
INSERT INTO `products`(title,decription,price,publishTime) VALUES ('huawei','huawei-mate70价格只要1000',1000,'2021-10-10');
INSERT INTO `products`(title,decription,price,publishTime) VALUES('vivo','vivo x60只需要1200',1200,'2021-10-11');

十二.删除数据


🥴删除表中的所有的数据,(谨慎使用)

1
sql复制代码DELETE from 'products';

👽根据id删除一条数据。

1
sql复制代码DELETE FROM `products` WHERE id = 3;

十三.修改数据


🎯更改表中的某个字段

1
sql复制代码UPDATE `products` SET decription = '假的别买';

🎪更改表中的某个具体字段

1
sql复制代码UPDATE `products` SET decription = '好手机666' WHERE id=2;

😶‍🌫️当我们修改某一个数据的时候,显示最新的时间。

1
sql复制代码ALTER TABLE `products` ADD `updateTime` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…303132…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%