注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

为什么算法复杂度分析,是学算法最核心的一步

基本介绍 算法复杂度这个概念,是算法中比较重要的一个核心的点。假设你现在要去分辨一串代码写的好与坏,那么是不是就得需要有一个可以衡量的标准,而算法复杂度的分析,就是一把标准之尺,有了这把尺子,你就能分辨出那些写的糟糕的代码,同时你也知道了要怎样去优化这段代码。...
继续阅读 »

基本介绍


算法复杂度这个概念,是算法中比较重要的一个核心的点。假设你现在要去分辨一串代码写的好与坏,那么是不是就得需要有一个可以衡量的标准,而算法复杂度的分析,就是一把标准之尺,有了这把尺子,你就能分辨出那些写的糟糕的代码,同时你也知道了要怎样去优化这段代码。


而目前我们常用的分析法,也就是大O表示法


常见复杂度


我们看一下 下面的代码


   function fn(n) {
let m = 0
console.log(m)
for (let i = 0; i <= n; i = ++ ) {
m += i
m--
}
}

我们假设 一行代码的执行的消耗时间是 1run_time 那么以此推导上面代码执行的时间消耗是(3n + 2)run_time 那么用大O表示法就是O(3n + 2)。


ps:本文中中次的概念对应 每行代码 而不是整个代码片段


大O表示法,并不会具体分析出每行代码执行花费的时间,他是一个粗略的抽象的统计概念,主要是是表示的某段代码的,所消耗的(时间/空间)增长趋势


O表示是 总耗时 和总次数的一个比值,可以简单理解为 每一次代码执行所需要花费的耗时,也就是 总时间/总次数 = 每次执行需要消耗的平均时长。


那么刚刚的O(3n + 2) 其实就是 (3n + 2) * 每次代码需要消耗的平均时长,那么就可以得出一个公式 T(n) = O(代码执行的总次数)


其中 T(n) 表示的是 整段代码执行需要的总耗时


在大O表示法中,常数,低阶,系数,在表示的时候是可以直接忽略统计的的,那么最后实际表示的复杂度就是O(n) 了


我们再来看下面的代码


 function fn(n) {
let aa = 0
let bb = 0

for (let i = 0; i < n; ++i) {
aa += i
}

for (let i = 0; i < n; ++i) {
for (let k = 0; k < n; ++i) {
bb += k
}
}

}

前两行代码 很好看出来 就是个2,第一个for循环的消耗是 2n 第二个for循环 消耗是n的二次方那么实际用大O 表示就是 O(2 + 2n + n²) 最后表示的时候取3块代码中增长趋势最大的也就是O(n²)


O(logn) O(nlogn)


理解了上面分析的内容之后,这两个 O(logn), O(nlogn) 复杂度就很容易去学会了


 function fn(n) {
let m = 0
for (let i = 0; i < n; i *= 2) {
m++
}
}

我们来假设 n 是8 那么 2³ 就是8 那么也就是 2的x次方就是 n 那么用大O 表示法就是O(log2ⁿ)


function fn(n) {
let m = 0
for (let i = 0; i < n; i *= 3) {
m++
}
}

那么上面这段代码就很容易看出来是O(log3ⁿ) 了 我们忽略他的底数,都统一表示O(logn)


在这基础上O(nlogn) 就更好理解了,它表示的就是 一段 执行n遍的 logn复杂度的代码,我们把上面的代码稍稍修改一下


 function fn(n) {
for(let j =0;j<n;j++){
let m = 0
for (let i = 0; i < n; i *= 2) {
m++
}
}
}

空间复杂度


其实空间复杂度 和时间复杂度 计算方式是一模一样的,只不过是着重的点不一样,当你回了时间复杂度的计算,空间复杂对你来说就是张飞吃豆芽了


   function fn(n) {
let m = []
for (let i = 0; i <= n; i = ++ ) {
m.push(i)
}
}

这块代码我们关注他的空间使用 就知道 是O(n)了


案例分析


我们来举个前端中一个经典的复杂度优化的例子,react,vue 他们的diff算法。


要知道目前最好的 两棵树的完全比较,复杂度也还是O(n³) ,这对频繁触发更新的情况,是一个严重的瓶颈。 同样的问题也存在于 react中 useEffect 和useMemo 的dep 以及 memo 的props。


所以他们都将比较操作 只停留在了当前的一层,比如diff只比较 前后同一层级的节点变化,不同层级的变化比对在出发更新时做出决定,这样就可以始终把复杂度维持在O(n)



结语


其实你分析出来的复杂度不等于,代码真实的复杂度,不管是大O表示法也好 还是别的表示法也好,都是针对代码复杂度分析的一个抽象工具,比如有一段处理分页的代码的业务逻辑,你清清楚楚的知道,目前是不允许改变分页大小的,也就是每次调用最多传进来的只有10 条数据,但是代码写的复杂度是O(n²) 这时候其实是没有多大的影响的,但是假设你现在写了一个无线滚动的功能,每次加载还都需要对所有的数据做O(n²)的操作,那么这时候,你就需要去想想怎么做优化了


作者:烟花易冷人憔悴
来源:juejin.cn/post/7302644330883612672
收起阅读 »

从canvas到B站弹幕

web
canvas是HTML自带的一个用于绘制图形的标签,它身上的API太多了,本文会介绍几个常见的属性,以及应用到B站的实现 Canvas 我们在body中放一个canvas标签,然后在Script中添加属性 <body> <canvas i...
继续阅读 »

canvas是HTML自带的一个用于绘制图形的标签,它身上的API太多了,本文会介绍几个常见的属性,以及应用到B站的实现



Canvas


我们在body中放一个canvas标签,然后在Script中添加属性


<body>
<canvas id="canvas" width="300" height="300"></canvas>
<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.fillStyle = "green"
ctx.fillRect(10,10,55,50)
</script>
</body>

let ctx = canvas.getContext("2d")其实就是对canvas实例化一个对象。先得到一只2维的画笔,接下来的操作都是针对这只画笔


ctx.fillStyle = "green"给这只画笔沾上墨水,否则怎么画都画不出


fillRext用来画填充矩形。其中四个参数分别为左上角坐标,和右下角坐标,此时效果如下


1.png


	<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.strokeRect(10,10,55,55)
</script>

strokeRect是用来画矩形的,只有边框,不会进行填充,stroke这个单词可能大家只知道有中风的意思,其实还有笔画,轻拭的意思,此时效果如下


2.png


再来一个自定义描边


	<script>
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.beginPath()
ctx.moveTo(10, 10)
ctx.lineTo(10, 55)
ctx.lineTo(55, 10)
ctx.closePath()
ctx.stroke()
</script>

beginPath就是让画笔落在纸上


moveTo接收的一个起始位置坐标


两个lineTo是终点坐标


closePath将所有点连接起来,stroke开始画,一定要有stroke,否则没有效果


3.png


当然,你也可以对其填充


        ctx.beginPath()
ctx.moveTo(10, 10)
ctx.lineTo(10, 55)
ctx.lineTo(55, 10)
ctx.fill()

默认颜色黑色


4.png


当然,你也可以画贝塞尔曲线(bezierCurve):不规则的曲线,这个内容我这里不做介绍,方法可以网上自寻搜索


再来画个圆


    let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.arc(50,50,40,0,2 * Math.PI)
ctx.stroke()

arc方法用于画圆或圆弧,前两个参数为圆心坐标,第三个参数为圆的半径,第四个参数是起始角度(通常为0,三点钟方向),最后一个参数为终止角度。


5.png


绘制文本


    let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d")
ctx.font = '50px sans-serif'
ctx.fillText('床前明月光',10, 100)

fillText中后两个参数为起始坐标,strokeText绘制的是空心字


6.png


如果两个fillText的起始坐标一样,就可以重叠在一起,我现在再加一句同其实坐标的文字


7.png


B站弹幕其实就是用的画布,但是实现起来还是比较困难,为了方便文章排版,注释都放在了代码里面


b站弹幕


html部分


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#canvas {
position: absolute;
}
/* 宽高不在css设置,是因为是和视频一起变化,动态的,用js */
</style>
</head>
<body>
<div class="wrap">
<h1>Peaky Blinders</h1>
<div class="main">
<canvas id="canvas"></canvas>
<video src="./video.mp4" id="video" controls="true" width="720" height="480"></video>
</div>
<div class="content">
<input type="text" id="text">
<input type="button" value="发弹幕" id="btn">
<!-- 取色器 -->
<input type="color" id="color">
<!-- 控制弹幕的大小 -->
<input type="range" id="range" max="40" min="20">
</div>
</div>
<script src="./index.js"></script>
<!-- 执行js 后加载js js可能是本地文件,也可能是在线地址,如果放在开头,执行这个的时候会堵住html的加载,甚至会报错,读取canvas都不知道,如果保证js内容在页面加载完毕后执行也可以放在前面,就是window.onload这个方法 -->
</body>
</html>

js部分


// 这个js代码很难写,一般高级程序员才会这样写
// window.onload = function(){}
// 1.读取用户内容 2.把内容颜色大小放到画布上,绘制
// 历史弹幕,数组(里面放对象)还要接受新的弹幕,到了时间就绘制,要递归
let data = [
{ value: 'By order of the peaky bliears', color: 'red', fontSize: 22, time: 5 },
{ value: 'No Fucking Fighting', color: 'green', fontSize: 30, time: 10},
{ value: 'Fucking Shelby', color: 'black', fontSize: 22, time: 22}
]
// 整理弹幕数据,弹幕的y,历史弹幕问题 形参跟外面的一样没毛病,辨识度更高,代码太多了abc是啥都不知道,都可以 ,形参可以默认值,万一没有传值呢
function CanvasBarrage(canvas, video, opts = {}){
if(!canvas || !video) return
this.video = video
this.canvas = canvas
// 伪代码 canvas 宽高 和 video宽高保持一致
// canvas.width = style.width style读取宽高,js设置宽高
this.canvas.width = video.width
this.canvas.height = video.height
// 获取画布
this.ctx = canvas.getContext("2d")
// 初始化代码
// 没有认为修改弹幕的设置,默认值
let defOpts = {
color: '#e91e63',
fontSize: 20,
speed: 1.5,
// 透明度
opacity: 0.5,
data: []
//value和time不需要默认值
}
Object.assign(this, defOpts, opts)
// 视频播放,弹幕才会进行绘制
this.isPaused = true
// 默认暂停
// 获取到所有的弹幕 map(返回一个新的数组)里面是箭头函数(把item交给一个新的箭头函数) map循环了,每个弹幕都被修饰了一下
this.barrages = this.data.map((item) => new Barrage(item, this))
this.render()
}
Barrage.prototype.init = function(){
// 左边是自己新建的右边是传进来的,如果第一个是没有的,就是给出的默认的颜色
this.color = this.obj.color || this.context.color
this.speed = this.obj.speed || this.context.speed
this.opacity = this.obj.opacity || this.context.opacity
this.fontSize = this.obj.fontSize || this.context.fontSize

let p = document.createElement('p')
// 让字体大小等于设置的大小
p.style.fontSize = this.fontSize + 'px'
p.innerHTML = this.value
document.body.appendChild(p)
// 右边是获取这个容器的宽度
this.width = p.offsetWidth
// 放完之后要删掉
document.body.removeChild(p)
// 设置弹幕的位置
this.x = this.context.canvas.width
// y的高度是随机值
this.y = this.context.canvas.height * Math.random()
// 弹幕可能上下方超出边界
if(this.y < this.fontSize){
this.y = this.fontSize
}else if(this.y > this.context.canvas.height - this.fontSize){
this.y = this.context.canvas.height - this.fontSize
}
}

// Barrage 修饰一条弹幕 为箭头函数那里服务 (实例对象,this对象)
function Barrage(obj, context){
this.value = obj.value
this.time = obj.time
// 挂在构造函数中后面更方便
this.obj = obj
this.context = context
}

CanvasBarrage.prototype.clear = function(){
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
// 将这条弹幕会绘制在画布上
Barrage.prototype.renderEach = function(){
// canvas绘制过程
// 设置画布的文字字体,字号
// 设置画布的文字颜色
// 绘制颜色
this.context.ctx.font = `${this.fontSize}px Arial`
this.context.ctx.fillStyle = this.color
this.context.ctx.fillText(this.value, this.x, this.y)
}
// 将弹幕绘制到画布上
CanvasBarrage.prototype.renderBarrages = function(){
// 伪代码 拿到视频播放的当前时间,根据时间绘制
let time = this.video.currentTime
// 遍历所有的弹幕
this.barrages.forEach(function(barrage){
// 出屏之外之后就不用再操作了
if(time >= barrage.time && !barrage.flag){
// 这属性没有就是false 这个操作就是为了防止放过了的弹幕不需要初始化
if(!barrage.isInit){
barrage.init()
barrage.isInit = true
}
// 控制弹幕左移
barrage.x -= barrage.speed
// rednerEach相当于ctx.fillstyle
barrage.renderEach()
// 弹幕是有长度的
if(barrage.x < -barrage.width){
barrage.flag = true
}
}
})
}
// 这里就是render ,把弹幕弄到画布中
CanvasBarrage.prototype.render = function(){
// 清除画布,习惯问题
this.clear()
// 要先绘制才能操作画笔,并且要向左移动
this.renderBarrages()
// 播放状态才能移动
if(!this.isPaused){
// setInterval这里不用,下面定时器的更高级,16.7ms(内定时间)之后就执行一次,递归之后就是一直循环下去
requestAnimationFrame(this.render.bind(this))
// bind(this)以后再讲
}
}
// 添加新的弹幕
CanvasBarrage.prototype.add = function(obj){
// barrages是终极数组,data修饰之后的
// this.barrages16.7ms之后也会重修渲染一次
this.barrages.push(new Barrage(obj,this))
}
// 传的参数是canvas和video dom结构 opts是一个对象含value color time fontSize 这个会替代掉,合并对象,相同的覆盖,不同的加进去
let canvas = document.getElementById('canvas')
// video知道此时视频多少秒
let video = document.getElementById('video')
// $没有意义,区分罢了
let $text = document.getElementById('text')
let $btn = document.getElementById('btn')
let $color = document.getElementById('color')
let $range = document.getElementById('range')
// 整理弹幕的实例对象
// 对象里key和value可以直接由{data: data}变成{data}
let canvasBarrage = new CanvasBarrage(canvas, video, {data})
// play是播放,处理所有弹幕实例对象
video.addEventListener('play',function(){
canvasBarrage.isPaused = false
// 处理每一条弹幕,canvasBarrage相当于一个管家
canvasBarrage.render()
})

function send(){
// 读取文本内容
let value = $text.value
// video 自带一个属性读取时间
let time = video.currentTime
let color = $color.value
let fontSize = $range.value
// 把上面的内容整理成一个对象,交给函数去操作
let obj = {
value: value,
color: color,
fontSize: fontSize,
time: time
}
// 多么希望add可以把obj放进去,接收新的弹幕,处理弹幕再走一遍send
canvasBarrage.add(obj)
}
$btn.addEventListener('click', send)
$text.addEventListener('keyup',function(e){

console.log(e);
if(e.keyCode === 13){
send()
}

})


  1. 数据结构:

    • data 数组包含表示单个弹幕项的对象。每个对象具有诸如 value(文本内容)、colorfontSizetime(显示时间)等属性。



  2. CanvasBarrage 类:

    • CanvasBarrage 是一个构造函数,用于初始化弹幕系统。

    • 它接受一个画布元素、一个视频元素和可选的配置选项。

    • 默认选项(defOpts)包括诸如 colorfontSizespeedopacitydata 等属性。

    • 根据提供的数据创建了一个 Barrage 对象的数组。

    • render 方法负责在画布上渲染和动画弹幕。

    • clear 方法在渲染之前清除画布。



  3. Barrage 类:

    • Barrage 是用于单个弹幕项的构造函数。

    • 它接受一个对象(obj)和一个上下文(context),即 CanvasBarrage 的实例。

    • init 方法使用属性如 colorspeedopacityfontSizewidthxy 初始化弹幕项。

    • renderEach 方法在画布上渲染单个弹幕项。



  4. 渲染和动画:

    • renderBarrages 方法负责根据当前视频时间渲染所有弹幕项。

    • render 方法使用 requestAnimationFrame 不断调用自身以进行连续动画。

    • add 方法允许向系统添加新的弹幕项。



  5. 事件监听器:

    • 对视频的 play 事件监听器触发在视频播放时渲染弹幕。

    • 对按钮($btn)的 click 事件监听器触发 send 函数以添加新的弹幕。

    • 对文本输入框($text)的 keyup 事件监听器在按下Enter键时触发 send 函数。



  6. 用户输入处理:

    • send 函数读取输入值(文本、时间、颜色、fontSize)并创建一个新的弹幕对象,然后将其添加到弹幕系统中。



  7. 初始化:

    • 使用画布、视频和提供的数据创建了 CanvasBarrage 的实例。



  8. 使用:

    • 当视频播放时,弹幕系统开始渲染,并且用户可以使用提供的输入元素添加新的弹幕




效果如下:


效果.gif




如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]


作者:Dolphin_海豚
来源:juejin.cn/post/7302310196311719988
收起阅读 »

风控规则引擎(一):Java 动态脚本

风控规则引擎(一):Java 动态脚本 日常场景 共享单车会根据微信分或者芝麻分来判断是否交押金 汽车租赁公司也会根据微信分或者芝麻分来判断是否交押金 在一些外卖 APP 都会提供根据你的信用等级来发放贷款产品 金融 APP 中会根据很复杂规则来判断用户是否...
继续阅读 »

风控规则引擎(一):Java 动态脚本


日常场景



  1. 共享单车会根据微信分或者芝麻分来判断是否交押金

  2. 汽车租赁公司也会根据微信分或者芝麻分来判断是否交押金

  3. 在一些外卖 APP 都会提供根据你的信用等级来发放贷款产品

  4. 金融 APP 中会根据很复杂规则来判断用户是否有借款资格,以及贷款金额。


在简单的场景中,我们可以通过直接编写一些代码来解决需求,比如:


// 判断是否需要支付押金
return 芝麻分 > 650

这种方式代码简单,如果规则简单且不经常变化可以通过这种方式,在业务改变的时候,重新编写代码即可。


在金融场景中,往往会根据不同的产品,不同的时间,对接的银行等等多个维度来配置规则,单纯的直接编写代码无法满足业务需求,而且编写代码的方式对于运营人员来说无论实时性、可视化都很欠缺。


在这种情况往往会引入可视化的规则引擎,允许运营人员可以通过可视化配置的方式来实现一套规则配置,具有实时生效、可视化的效果。减少开发和运营的双重负担。


这篇主要介绍一下如何实现一个可视化的表达式的定义和执行。


表达式的定义


在上面说到的使用场景中,可以了解中至少需要支持布尔表达式。比如



  1. 芝麻分 > 650

  2. 居住地 不在 国外

  3. 年龄在 18 到 60 之间

  4. 名下无其他逾期借款


...


在上面的例子中,可以将一个表达式分为 3 个部分



  1. 规则参数 (ruleParam)

  2. 对应的操作 (operator)

  3. 对应操作的阈值 (args)


则可以将上面的布尔表达式表示为



  1. 芝麻分 > 650


{
"ruleParam": "芝麻分",
"operator": "大于",
"args": ["650"]
}


  1. 居住地 不在 国外


{
"ruleParam": "居住地",
"operator": "位于",
"args": ["国内"]
}


  1. 年龄在 18 到 60 之间


{
"ruleParam": "年龄",
"operator": "区间",
"args": ["18""60"]
}


  1. 名下无其他逾期借款


{
"ruleParam": "在途逾期数量",
"operator": "等于",
"args": ["0"]
}

表达式执行


上面的通过将表达式使用 json 格式定义出来,下面就是如何在运行中动态的解析这个 json 格式并执行。


有了 json 格式,可以通过以下方式来执行对应的表达式



  1. 因为表达式的结构已经定义好了,可以通过手写代码来判断所有的情况实现解释执行, 这种方案简单,但增加操作需要修改对应的解释的逻辑, 且性能低


/*
{
"ruleParam": "在途逾期数量",
"operator": "等于",
"args": ["0"]
}
*/

switch(operator) {
case "等于":
// 等于操作
break;
case "大于":
// 等于操作
break;
...
}



  1. 在第一次得到 json 字符串的时候,直接将其根据不同的情况生成对应的 java 代码,并动态编译成 Java Class,方便下一次执行,该方案依然需要处理各种情况,但因为在第一次编译成了 java 代码,性能和直接编写 java 代码一样




  2. 使用第三方库实现表达式的执行




使用第三方库实现动态表达式的执行


在 Java 中有很多表达式引擎,常见的有



  1. jexl3

  2. mvel

  3. spring-expression

  4. QLExpress

  5. groovy

  6. aviator

  7. ognl

  8. fel

  9. jsel


这里简单介绍一下 jexl3 和 aviator 的使用


jexl3 在 apache commons-jexl3 中,该表达式引擎比较符合人的书写习惯,其会判断操作的类型,并将参数转换成对应的类型比如 3 > 4 和 "3" > 4 这两个的执行结果是一样的


aviator 是一个高性能的 Java 的表达式类型,其要求确定参数的类型,比如上面的 "3" > 4 在 aviator 是无法执行的。


jexl3 更适合让运营手动编写的情况,能容忍一些错误情况;aviator 适合开发来使用,使用确定的类型参数来提供性能


jexl3 使用


加入依赖


<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jexl3</artifactId>
<version>3.2.1</version>
</dependency>

// 创建一个带有缓存 jexl 表达式引擎,
JexlEngine JEXL = new JexlBuilder().cache(1000).strict(true).create();

// 根据表达式字符串来创建一个关于年龄的规则
JexlExpression ageExpression = JEXL.createExpression("age > 18 && age < 60");

// 获取需要的参数,java 代码太长了,简写一下
Map<String, Object> parameters parameters = {"age": 30}

// 执行一下
JexlContext jexlContext = new MapContext(parameters);

boolean result = (boolean) executeExpression.evaluate(jexlContext);

以上就会 jexl3 的简单使用


aviator


引入依赖


<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>5.3.1</version>
</dependency>

Expression ageExpression = executeExpression = AviatorEvaluator.compile("age > 18 && age < 60");

// 获取需要的参数,java 代码太长了,简写一下
Map<String, Object> parameters parameters = {"age": 30}

boolean result = (boolean) ageExpression.execute(parameters);

注意 aviator 是强类型的,需要注意传入 age 的类型,如果 age 是字符串类型需要进行类型转换


性能测试


不同表达式引擎的性能测试


Benchmark                                         Mode  Cnt           Score           Error  Units
Empty thrpt 3 1265642062.921 ± 142133136.281 ops/s
Java thrpt 3 22225354.763 ± 12062844.831 ops/s
JavaClass thrpt 3 21878714.150 ± 2544279.558 ops/s
JavaDynamicClass thrpt 3 18911730.698 ± 30559558.758 ops/s
GroovyClass thrpt 3 10036761.622 ± 184778.709 ops/s
Aviator thrpt 3 2871064.474 ± 1292098.445 ops/s
Mvel thrpt 3 2400852.254 ± 12868.642 ops/s
JSEL thrpt 3 1570590.250 ± 24787.535 ops/s
Jexl thrpt 3 1121486.972 ± 76890.380 ops/s
OGNL thrpt 3 776457.762 ± 110618.929 ops/s
QLExpress thrpt 3 385962.847 ± 3031.776 ops/s
SpEL thrpt 3 245545.439 ± 11896.161 ops/s
Fel thrpt 3 21520.546 ± 16429.340 ops/s
GroovyScript thrpt 3 91.827 ± 106.860 ops/s

总结


这是写的规则引擎的第一篇,主要讲一下



  1. 如何讲一个布尔表达式转换为 json 格式的定义方便做可视化存储和后端校验

  2. 如何去执行一个 json 格式的表达式定义


在这里也提供了一些不同的表达式引擎和性能测试,如果感兴趣的可以去尝试一下。


下一篇主要讲一下在引擎里面规则参数、操作符是如何设计的,也讲一下可视化圆形的设计


作者:双鬼带单
来源:juejin.cn/post/7302805039450210313
收起阅读 »

技术大佬问我 订单消息重复消费了 怎么办?

技术大佬 :佩琪,最近看你闷闷不乐了,又被虐了? 佩琪:(⊙o⊙)…,又被大佬发现了。这不最近出去面试都被揉捏的像一个麻花了嘛 技术大佬 :哦,这次又是遇到什么难题了? 佩琪: 简历上的技术栈里,不是写了熟悉kafka中间件嘛,然后就被面试官炮轰了里面的细节 ...
继续阅读 »

技术大佬 :佩琪,最近看你闷闷不乐了,又被虐了?


佩琪:(⊙o⊙)…,又被大佬发现了。这不最近出去面试都被揉捏的像一个麻花了嘛


技术大佬 :哦,这次又是遇到什么难题了?


佩琪: 简历上的技术栈里,不是写了熟悉kafka中间件嘛,然后就被面试官炮轰了里面的细节


佩琪: 其中面试官给我印象深刻的一个问题是:你们的kafka消息里会有重复消息的情况吗?如果有,是怎么解决重复消息的了?


技术大佬 :哦,那你是怎么回答的了?


佩琪:我就是个crud boy,根本不知道啥是重复消息,所以就回答说,没有


技术大佬 :哦,真是个诚实的孩子;然后了?


佩琪:然后面试官就让我回家等通知了,然后就没有然后了。。。。


佩琪:对了大佬,只是简单的接个订单消息,为啥还会有重复的订单消息了?


技术大佬 :向上抬了抬眼睛,清了清嗓子,面露自信的微笑回答道:以我多年的经验,这里面大概有2个原因:一个是重复发送,一个是重复消费


写作 (6).png


佩琪哦哦,那重复发送是啥了?


技术大佬 :重试发送是从生产端保证消息不丢失,但不保证消息不会重发;业界还有个专业术语定义这个行为叫做"at least once"。在重试的时候,如果发送消息成功,在记录成功前程序崩了/或者因为网络问题,导致消息中间件存储了消息,但是调用方失败了,调用方为了保证消息不丢,会再次重发这个失败的消息。具体详情可参见上篇文章《kafka 消息“零丢失”的配方》


佩琪重复消费又是啥了?


技术大佬 :重复消费,是指消费端重复消费。举个例子吧, 比如我们一般在消费消息时,都建议处理完业务后,手工提交offset;但是在提交offset的时候,因为某些原因程序崩了。再次重启消费者应用后,会继续消费上次未提交的消息,像下面这段代码


  while(true){
consumer.poll(); // ①拉取消息
XXX // ② 业务处理;
consumer.commit(); //③提交消费位移
}

在第三步,提交位移的时候,程序突然崩了(jvm crash)或者网络闪断了;再次消费时,会拉取未提交的消息重复执行第二步的业务处理。


佩琪:哦哦,原来是这样。我就写了个消费者程序,咋这么多的“技术坑”在这里面。请问大佬,解决重复消费的底层逻辑是啥了?


技术大佬 : 两个字:幂等。 即相同的请求参数,请求1次,和请求100W次,得到的结果和对业务的影响是一样的。比如:同一个订单消息消费一次,然后进行积分累加;和同一个订单的消息重复消费10次,进行积分累加,最后效果是一样的,总积分不能变,不能变。 如果同一个订单消费了10次,积分也给累加了10次,这种就不叫幂等了。


佩琪:哦哦。那实现幂等的核心思想和通用做法又是什么了?


技术大佬 :其实也挺简单 存储+唯一key 。在进行业务处理时,查询下是否已处理过这个唯一key的消息;如果存在就不进行后续业务处理;如果不存在就继续后续业务的处理吧。


佩琪摸了摸头,唯一key是个啥了?


技术大佬 :唯一key是消息里业务数据的唯一标识; 比如对于某些业务场景是订单号;某些业务场景是订单号+订单状态;


佩琪存储又是指什么了?


技术大佬 :一般指的就是存储引擎了;比如业界常用的mysql,redis;或者不怎么常用的mongo,hbase等。


佩琪对了大佬,目前业界有哪些幂等解决方案?


技术大佬常用的大概有两种方式:强校验和弱校验


佩琪强校验是什么了?能具体说说吗?


技术大佬 :强校验其实是以数据库做为存储;唯一key的存储+业务逻辑的处理放入一个事务里;要么同时成功,要么同时失败。举个例子吧,比如接收到 用户订单支付消息后;根据订单号+状态,累加用户积分;先查询一把流水表,发现有这个订单的处理记录了,直接忽略对业务的处理;如果没有则进行业务的操作 ,然后把订单号和订单状态做为唯一key,插入流水表,最后做为一个整体的事务进行提交;


整体流程图如下:


写作 (4).png
待做


佩琪大佬好强。能具体说说你的这种方案有什么优缺点吗?


技术大佬 :给你一张图,你学习下?


优点缺点
在并发情况下,只会严格执行一次。数据库唯一性+事务回滚能保证业务只执行一次; 不会存在幂等校验穿透的问题处理速度较慢: 处理性能上和后续的redis方案比起来,慢一个数量级。毕竟有事务加持;另外插入唯一数据时极大可能读磁盘数据,进行唯一性校验
可提供查询流水功能:处理流水记录的持久化,在某些异常问题排查情况下,还能较为方便的提供查询记录历史数据需要额外进行清理:如果采用mysql进行存储,历史记录数据的清理,需要自己单独考虑和处理,但这应该不是个事儿
实现简单:实现难度还是较为简单的,一个注解能包裹住事务+回滚
适用资金类业务:非常适合涉及资金类业务的防重;毕竟涉及到钱,不把数据持久化和留痕,心理总是不踏实
架构上简约:架构上也简单,大多数业务系统都需要依赖数据库中间件吧。

佩琪: 果然是大佬,请收下我的打火机。


佩琪弱弱的问下,那弱校验是什么了?


技术大佬 :其实是用redis进行唯一key存储和防重。比如订单消息,订单号是一条数据的唯一标识吧。然后使用lua脚本设置该消息正在消费中;此时重复消息来,进行相同的设置,发现该订单号,已经被标识为正在处理中,那这条消息放入延时队列中,延时重试吧;如果发现已经消费成功,则直接返回,不执行业务了;业务执行完,设置该key执行成功。
大概过程是这样的
写作 (5).png


佩琪那用redis来进行防重,会存在什么问题吗?


技术大佬 : 可能会存在防重数据的丢失吧,最后带来防不了重。


佩琪redis为什么会丢防重数据了?


技术大佬 : 数据丢失一部分是因为redis自身的原因,因为它把数据放入了内存中;虽然提供了异步复制机制+高可用方案,但还是不能100%保证数据不丢失。


技术大佬 : 另外一个原因是,数据过期清理后,可能还有极低的概率处理相同的消息,此时就防不了重了。


佩琪那能不设置过期时间吗?


技术大佬 : 额,除非家里有矿。


技术大佬 : redis毕竟是用内存进行存储,存储容量比起硬盘来小很多,存储单位是G(硬盘线上存储单位是T开始),而且价格比起硬盘来又贵多个数量级;属于骄贵性存储;所以为了节约存储空间,一般都会设置一个较短的过期时间,进行数据的淘汰;而这个较短过期时间,是根据业务情况进行定义,有5分钟的,有10分钟的。


技术大佬 : 在说了,这些防重key是不太具备业务属性和高频率访问特性的,不属于热点数据,为啥还要一直放到缓存里了???


佩琪:果然是大佬,请再次收下我的打火机。用redis来做防重,缺点这么的多,那为什么还要用redis来进行防重了?


技术大佬 :你不觉得它的优点也很多吗。用它,主要是利用redis操作数据速度快,性能高;并且还能自动清理过期数据的特性,简直不要太方便;另外做防重的目标是啥了?还不是为了那些少数异常情况下产成的重复数据对其过滤;所以引入redis做防重,是为了大多数情况下,降低对业务性能的伤害;从而在性能和数据准确性中间取了个平衡。


技术大佬 : 建议对处理的及时性有一定要求,并且非资金类业务;比如消费下单消息,然后发送通知等业务使用吧。


技术大佬 :我知道你想要问什么了?我这里画了图,列了下优缺点,你拿去看看?


优点缺点
处理速度快因为数据有过期时间和redis自身特性;防重数据有丢失可能性,结果就是有不能防重的风险
无需自动清理唯一key记录实现上比起数据库,稍显复杂,需要写lua脚本;但学过编程的,相信我半天时间熟悉语法+写这个lua脚本应该是没问题的
消息一定能消费成功架构上稍显复杂,为了保证一定能消费成功,引入了延时队列

佩琪:嘿嘿大佬,我听说防重最好的是用布隆过滤器,占用空间小,速度很快,为啥不用布隆过滤器了?


技术大佬 :不使用redis布隆过滤器,主要是 redis 布隆过滤器特性会导致,某些消息会被漏掉。因为布隆过滤器底层逻辑是,校验一个key如果不存在,绝对不会存在;但是某个key如果存在,那么他是可能存在,又可能不存在。所以这会导致防重查询不准确,最终导致漏消息,这太不能接受了。


技术大佬 :还有个不算原因的原因,是redis 4.0之前的版本还都不支持布隆过滤器了。


佩琪大佬 redis我用过, redis 有个setnx,既能保证并发性,又能进行唯一key存储,你为啥不用了?


技术大佬 :不使用它,主要是redis的 setnx操作和后续的业务执行,不是一个事务单元;即可能setnx成功了,后续业务执行时进程崩溃了,然后在消息重试的时候,又发现setnx里有值了,最终会导致消费失败的消息重试时,会被过滤,造成消息丢失情况。所以才引入了redis lua+延时消息。在lua脚本里记录消费业务的执行状态,延时消息保证消息一定不会丢失。


佩琪我想偷个懒有现成的框架吗?


技术大佬 :有的。实现核心的幂等key的设置和校验lua脚本。



  1. lua代码如下:


local status = redis.call('get',KEYS[1]);
if status == nil //不存在,则redis放入唯一key和过期时间
then
redis.call('SETEX',KEYS[1],ARGV[1],1)
return "2" //设置成功
else //存在,返回处理状态
return status
end


  1. 消费者端的使用,伪代码如下


//调用lua脚本,获得处理状态
String key = null; //唯一id
int expiredTimeInSeconds = 10*60; //过期时间
String status = evalScript(key,expiredTimeInSeconds);

if(status.equals("2")){//设置成功,继续业务处理
//业务处理
}

if(status.equals("1")){ //已在处理中
//发送到延时队列吧
}

if(status.equals("3")){ //已处理成功
//什么都不做了
}


总结




  1. 生产端的重复发送和消费端的重复消费导致消息会重




  2. 解决消息重复消费的底层逻辑是幂等




  3. 实现幂等的核心思想是:唯一key+存储




  4. 有两种实现方式:基于数据库强校验和基于redis的弱校验。




感悟


太难了


为了保证上下游消息数据的完整性;引入了重试大法和手工提交offerSet等保证数据完整性解决手段;
可引入了这些解决手段后;又带来了数据重复的问题,数据重复的问题,是可以通过幂等来解决的。


太难了


作为应用层开发的crud boy的我,深深的叹了口气,开发的应用要在网络,主机,操作系统,中间件,开发人员写个bug等偶发性问题出现时,还需要保证上层应用数据的完整性和准确性。


此时佩琪头脑里突然闪现过一道灵光,业界有位大佬曾说过:“无论什么技术方案,都有好的一面,也有坏的一面。而且,每当引入一个新的技术方案解决一个已有的技术问题时,这个新的方案会带来更多的问题,问题就像一个生命体一样,它们会不断的繁殖和进化”。在消息防丢+防重的解决方案里,深感到这句话的哲理性。


原创不易,请 点赞,留言,关注,转载 4暴击^^


作者:程序员猪佩琪
来源:juejin.cn/post/7302023698721570857
收起阅读 »

重要提醒!第三方 Cookie 即将被禁用

web
Chrome 浏览器计划从 2024 年第一季度开始禁用 1% 用户的第三方 Cookie,以方便测试,然后在 2024 年第三季度逐步覆盖到 100% 用户。Chrome 推出了一系列API,为诸如身份验证、广告和欺诈检测等用例提供了以隐私为重点的替代方案。...
继续阅读 »

chrome-4.webp


Chrome 浏览器计划从 2024 年第一季度开始禁用 1% 用户的第三方 Cookie,以方便测试,然后在 2024 年第三季度逐步覆盖到 100% 用户。Chrome 推出了一系列API,为诸如身份验证、广告和欺诈检测等用例提供了以隐私为重点的替代方案。


本文将带您了解禁用时间表,建议您立即采取行动,以确保您的网站做好准备。


1.禁用时间表


privacysandbox.com 的时间轴上,可以看到 2023 年第四季度和 2024 年第一季度将有两个里程碑,作为 Chrome 辅助测试模式的一部分。该测试主要针对测试隐私沙盒相关性和测量 API 的组织,但作为测试的一部分,将对 1% 的 Chrome 稳定版用户禁用第三方 cookies。


配图:时间表


这意味着从 2024 年开始,即使您没有积极参与 Chrome 浏览器辅助测试,您也可以预期在您的网站上看到越来越多的 Chrome 浏览器用户禁用了第三方 Cookie。这一测试周期将持续到 2024 年第三季度,届时将禁用所有 Chrome 浏览器用户的第三方 Cookie。


2.需要做哪些准备?


为确保您的网站在没有第三方 cookie 的情况下可以正常运行,需要做好以下准备:



  • 梳理第三方 cookie 的使用情况。

  • 进行破坏测试。

  • 对于存储在每个网站上的跨站点 cookie(例如嵌入式 cookie),请考虑使用 CHIPS 进行分区。

  • 对于在一小组相关联网站之间的跨站点 cookie,请考虑使用相关网站集。

  • 对于其他第三方 cookie 的使用情况,请迁移到相关的 Web API。


2.1.梳理第三方 cookie 的使用情况


Chrome开发者工具的网络面板显示请求中设置和发送的Cookie。在应用程序面板中,在存储下可以看到Cookie标题。您可以浏览每个访问的站点存储的Cookie,作为页面加载的一部分。您可以按照SameSite列进行排序,以将所有 Cookie分组。


第三方 cookie 可以通过其 SameSite= 值来识别。您应该搜索代码以查找将 SameSite 属性设置为此值的实例。如果您在 2020 年左右之前对添加 SameSite= 到您的 Cookie 进行了更改,那么这些更改可能是一个很好的起点。


Chrome DevTools 网络面板显示根据请求设置和发送的 cookie。在 Application 面板中,您可以在 Storage 下看到 Cookie。您可以浏览为页面加载过程中访问的每个站点存储的 cookie。您可以按列 SameSite 排序以对所有 值的 cookie 进行分组。


从 Chrome 118 开始,DevTools 问题选项卡显示了重大更改问题:"跨站点上下文中发送的 Cookie 将在未来的 Chrome 版本中被阻止", 该问题列出了当前页面可能受影响的 cookie。


chrome-issue-cookie.png


如果您发现第三方设置的 cookie,您应该与这些提供商核实,看看他们是否有逐步淘汰第三方 cookie 的计划。例如,您可能需要升级正在使用的库的版本、更改服务中的配置选项,或者如果第三方正在自行处理必要的更改,则不采取任何操作。


2.2.进行破坏测试


您可以使用 --test-third-party-cookie-phaseout 命令行标志或从Chrome 118开始,使用 chrome://flags/#test-third-party-cookie-phaseout 启用。这将设置 Chrome 阻止第三方 cookie,并确保新功能和缓解措施处于活动状态,以最佳模拟淘汰后的状态。


您也可以尝试通过 chrome://settings/cookies 阻止第三方 cookie 进行浏览,但请注意,该标志也确保了新功能和更新功能的启用。阻止第三方cookie是检测问题的好方法,但并不一定能验证您已经修复了问题。


如果您为您的网站保持一个活跃的测试套件,那么您应该进行两次并行运行:一次是使用常规设置运行的Chrome,一次是使用启用--test-third-party-cookie-phaseout 标志启动的相同版本的 Chrome。第二次运行中的任何测试失败而第一次运行中没有的都是需要调查的第三方cookie依赖的好候选项。请确保报告您发现的问题。


一旦您确定了存在问题的cookie并了解了它们的用例,您可以通过以下选项来选择必要的解决方案。


2.3.将 Partitioned cookie 与 CHIPS 结合使用


如果您的第三方 Cookie 在与顶级站点进行 1:1 嵌入的上下文中使用,则可以考虑使用带有独立分区状态的 Cookie(CHIPS)的分区属性,以允许使用每个站点使用的单独的 Cookie 进行跨站点访问。


partitioned.png


要实现 CHIPS,您需要将 Partitioned 属性添加到您的 Set-Cookie 头中:


通过设置 Partitioned,该网站选择将 cookie 存储在由顶级网站分隔的单独的 cookie 存储区。在上面的示例中,cookie 来自store-finder.site,该网站托管了一个店铺地图,用户可以保存他们喜欢的店铺。通过使用 CHIPS,当 brand-a.site 嵌入store-finder.site 时,fav_store cookie 的值为123。然后,当 brand-b.site 也嵌入 store-finder.site 时,他们将设置并发送自己分隔的fav_store cookie 实例,例如值为456。


这意味着嵌入式服务仍然可以保存状态,但没有允许跨站点跟踪的共享跨站点存储。


潜在的使用案例:第三方聊天嵌入、第三方地图嵌入、第三方支付嵌入、子资源内容分发网络(CDN)负载均衡、无头内容管理系统提供商、用于提供不受信任的用户内容的沙盒域名、使用 Cookie 进行访问控制的第三方 CDN、需要在请求中添加 Cookie 的第三方 API 调用、按发布商进行状态范围的嵌入广告。


2.4.使用相关网站集


当仅在一小部分相关网站上使用第三 Cookie 时,您可以考虑使用相关网站集合(RWS),以便在这些定义的网站上上下文中允许跨站点访问该Cookie。


要实施 RWS,您需要定义并提交网站组。为确保这些网站之间存在有意义的关联,有效集合的策略要求按以下方式对这些网站进行分组:具有可见关联的相关网站(如公司产品的变体)、服务域(如 API、CDN)或国家代码域(如 .uk、.jp)。


RWS.png


网站可以使用 Storage Access API 来请求跨站点的 Cookie 访问权限,使用 requestStorageAccess() 方法或使用requestStorageAccessFor() 方法委派访问权限。当网站在同一组中时,浏览器会自动授予访问权限,并且跨站点的 Cookie将可用。


这意味着相关网站的组仍然可以在有限的上下文中使用跨站点的 Cookie,但不会冒着以允许跨站点追踪的方式在不相关的站点之间共享第三方cookie的风险。


潜在的用例包括:特定于应用程序的域,特定于品牌的域,特定于国家的域,用于提供不受信任的用户内容的沙盒域,用于API的服务域,CDN。


2.5.迁移到相关的 Web API


CHIPS 和 RWS 能够在保护用户隐私的同时实现特定类型的跨站点 Cookie访问,但是其他使用第三方 Cookie 的实例必须迁移到以隐私为重点的替代方案。


Privacy Sandbox 提供了一系列针对特定用例的专用API,无需使用第三方cookie:



  • 联合身份管理(FedCM)允许用户登录到站点和服务。

  • 私有状态令牌通过在站点之间交换有限的、非识别信息,实现反欺诈和反垃圾邮件功能。

  • 主题功能实现基于兴趣的广告和内容个性化。

  • 受保护的受众功能实现再营销和自定义受众。

  • 属性报告功能实现广告展示和转化的测量。


此外,Chrome 还支持 Storage Access API (SAA),用于用户交互框架中的使用。SAA 已经在 Edge,Firefox 和 Safari 上得到支持。我们认为它在保持用户隐私的同时,仍然能够实现关键的跨站点功能,并具有跨浏览器的兼容性。


请注意,Storage Access API (SAA) 将向用户显示浏览器权限提示。为了提供最佳的用户体验,只有在调用 requestStorageAccess() 的站点与嵌入页面进行交互并之前在顶级上下文中访问过第三方站点时,才会提示用户。成功授权将允许该站点在 30 天内跨站点访问 Cookie。可能的用例包括认证的跨站点嵌入,如社交网络评论小部件、支付提供商、订阅视频服务。


如果您仍然有未被这些选项覆盖的第三方 Cookie 用例,您应该向 Chrome 团队报告该问题,并考虑是否有不依赖于启用跨站点跟踪的功能的替代实现。


作者:FED实验室
来源:juejin.cn/post/7302330573381156876
收起阅读 »

程序员必看几大定律(2),你中招了吗?

1 洛克定律 确定目标,专注行动! 这条定律,我相信做过几年的程序员感受都会特别深! 除非你写的代码没什么难度,否则思考或者说设计才是程序员最经常要做的事,只有想清楚了,动手写代码才会顺畅! 不然你会发现自己经常在写一会->想一会->改一会 中...
继续阅读 »

1 洛克定律



确定目标,专注行动!



这条定律,我相信做过几年的程序员感受都会特别深!


除非你写的代码没什么难度,否则思考或者说设计才是程序员最经常要做的事,只有想清楚了,动手写代码才会顺畅! 不然你会发现自己经常在写一会->想一会->改一会 中循环往复,浪费不少时间和精力。


我自己总结了一下程序员的几个阶段:




  1. 初期: 以学习技术为主,写代码过程中花费时间最多的是:


    很多语法不懂要查、被一些低级错误耽误,调试很久可能发现只是单词拼错、完全没思路,只能疯狂搜索或者求助前辈。




  2. 中期: 常用技术已经熟悉,业务也逐渐深入,花费时间最多的是:


    某些代码因为经常copy,虽然已经写了几十次了,每次用的时候总是想不起来,还是要经常查。


    花更多的时间思考业务逻辑、思考代码结构。




  3. 后期: 我也还没达到,只能自己脑补一下:


    代码敲的更少了,主要负责项目管理,系统设计,系统架构。


    百科全书级的人物,能够解决绝大部分问题,当新人向你询问自己也曾犯过的错误时,也会感慨:时光不再了啊!!




2 相关定律



条条大路通罗马,万事万物皆有联系!



不知道你们是不是跟我曾经一样,有某一段时期,痴迷于各种技术栈。


我之前在刚接触前端,学习vue的时候,对前端充满了兴趣,不仅要会element-ui,也要会ant-design,不管是less,scss我都想要会,各种实用的工具库我也要会,vue2还不过瘾,vue3也得会,还看了react,nodejs,express,nuxt,next,ts,webpack,vite,rollup......


花了不少时间看了各种技术栈的文档,也实际动手搞了点小东西。


不过说真的,收获其实不多,即使当时感觉收获很多知识,但是因为这些额外的知识在工作中不常使用,过一段时间就忘了差不多了。


后来我是想通了,与其这样囫囵吞枣,不如深入现有经常接触的知识,我就不信了,如果我把vue2彻底搞懂了,学vue3还不是轻轻松松,学react应该也只是分分钟的事。


于是我就从各种源码开始看起,从axios到vuex,vue-router,vue2的源码也看了一部分了,不过最近工作忙,已经停了几周没看了,但是收获还是很多的,确实更让我坚信了:


条条大路通罗马!!


感兴趣的小伙伴也可以进入我的主页,里面有不少源码的阅读解析。


3 奥卡姆剃刀定律



把握关键,化繁为简



不知不觉,已经从当初的菜鸟逐渐变成现在的老鸟,工作也从繁复的coding中挣脱出来了一些些。


现在越来越觉得设计才是软件开发的精髓


而一个好的设计,一定不能太过复杂!


听一位领导讲过:公司推广过不少,不乏设计精细,十分深入业务理解的系统,但是真正能够推广让用户爱用的系统,一定不能太复杂,必要的时候也需要为用户的便利做出一些牺牲。


4 墨菲定律



如果事情有变坏的可能,不管这种可能性有多小,它总会发生。



不知道是谁瞎传的,我一直以为墨菲定律说的是:你越害怕的事情,越容易发生!


直到我看完墨菲定律才发现自己真的是被这句话给耽误了好久。




墨菲定律对于程序员来说,我觉得意义是很重大的。


因为如果代码存在bug,那么就一点有变坏的可能,不管这种可能性有多小,它总会发生!


也就是说,只要存在bug,它早晚都是会被触发的!!


不知道有多少人跟我曾经一样,抱着侥幸的心理,有时候即使知道某段代码存在bug,但是因为触发条件十分苛刻,就不去管它!!


只是我的心里还是会隐隐担心着某天会不会被人发现。




作为程序员,一定要保持代码的严谨性,对自己的代码负责。


任何人都无法保证自己不出错。


但是至少,如果已经有我们知道的问题,不要因为麻烦,不要因为侥幸,而去逃避它!


或许你们不相信,当我抱着这种消除自己所有已知隐患的态度写代码之后,反而觉得整个人都轻松了不少。


脑袋里没有那么多要惦记的事情,事情做完就真的等于事情做完了!如果真的出现自己也没想到的问题,那我也已经尽力了,再解决就是了。


5 酝酿效应



灵感来自偶然!



不知道你们是否也有遇到过这样的场景:


一整天都被某个问题困扰,百思不得其解!!


回去睡一觉,第二天再一想这个问题,我去,答案怎么就直接有了!!


那我昨天为什么会困扰这么久呢?




这其实就跟酝酿效应有关系!


当我们百思不得其解的时候,往往代表着脑袋的思考方向就进入了死胡同,这时候再怎么往里投入时间,投入精力都进展甚微。


而当我们放下这件事,去做其他事情的时候,我们的潜意识并没有忘记这件事,还是在帮助我们思考,只不过不是继续往死胡同里走了,用一个词来形容,那就是酝酿


所以当我们回过头再思考的时候,常常发现有心栽花花不开,无心插柳柳成荫柳暗花明又一村这样的现象!


所以如果你们看到程序员老哥们正在喝茶发呆,一定要小心,他们一定不是表面看过去的这么简单,他们的脑海可能正在疯狂酝酿着一段绝佳的代码!!


作者:林劭敏
来源:juejin.cn/post/7302249949215408167
收起阅读 »

浅谈一下滴滴实习

在租房躺尸好几天了,自从周一从滴滴离职就一直待在租房打游戏,瞬间没有工作的负担是真的彻底让我释放了心中的欲望,实际上游戏纯属发泄欲望和转移注意力的工具,我是一个喜欢瞎想的人(这可能就是我胖不起来的主要原因),放纵完实在是太无聊了,想写点什么。 说实话我这文章写...
继续阅读 »

在租房躺尸好几天了,自从周一从滴滴离职就一直待在租房打游戏,瞬间没有工作的负担是真的彻底让我释放了心中的欲望,实际上游戏纯属发泄欲望和转移注意力的工具,我是一个喜欢瞎想的人(这可能就是我胖不起来的主要原因),放纵完实在是太无聊了,想写点什么。
说实话我这文章写得毫无章法,完全是想到哪里写到哪里,也不想去分门别类了,我觉得真实的想法最重要,如果有语义错误就略过吧哈哈。


说说业务


对这段实习做一个小小的总结,先说一说业务吧,我所在的是滴滴商旅的一个技术部门,主要负责企业版这块的业务,我去的第一天上午看团队规范文档,下午跑项目看代码,第二天接需求,当然是比较简单的需求,后面陆陆续续做了滴滴企业版的小部分 pc 端官网和大部分移动端官网,如果你现在用手机搜滴滴企业版,那么你看到的页面大概率就是我做的,除此以外还有一个经典后台管理项目,其实项目用的技术栈都还好,没有说很有难度的,对于业务来说我觉得最难的应该就是项目的搭建和部署,然后就是技术方案,开发代码确实是最基础的事情了,这几个月完成的代码量并不大,这也完全在意料之中,实习生嘛,能确保自学到东西就行,当自学到一定的程度会很迷茫,不知道下一个进阶的领域是什么,但是在这段时间我逐渐感觉前端的一个瓶颈就在前端工程化,其实早就在学了,但是没有实际的项目经验加上网上教程比较匮乏,大多是讲解 webpack 的基本使用甚至一度让大部分人认为 webpack 就是前端工程化,如果有后端基础我觉得理解工程化那就太简单了,只不过可惜的是参与前端开发的大多是后端经验为 0 的同学,因此对于常年在浏览器玩 js 的我们很难理解在编译阶段能做的一些工作的意义所在,不管是现在的 Node 或是 Go 和 Rust,其实都可以作为一个深入挖掘的方向,至少我感觉业务是真的很无聊,偶尔当玩具写写还行,每天写真的没意思。


除了业务以外认知到一些原来不知道的职场"内幕"。


第一点:面试冷知识

走之前组内一直在招社招的员工,当面试官的兄弟和我说了我才知道,原来面试通一个部门甚至是同一个面试官可能真的会因为面试官心情或者其余外在因素决定你面试是否通过,比如最近部门比较忙,那可能面试也就比较水一点,大概考察没问题就直接过了没有那么多的时间去做横向比较(那我面的部门基本都还是比较闲啊哈哈),又或者是面试官看你比较顺眼性格也比较符合他的要求,大概率会给一些比较简单的题,这些都会影响面试官的判断从而决定你是否能通过面试最终拿 offer,所以经过这件事之后看开了很多,如果原来你一直不理解平时技术没你好的同学最后能拿到同公司或者同部门 offer,现在应该慢慢也就看开了,一旦挂了及时投递下一个部门,这不一定是自己的原因。


第二点:大厂其实不全是 996

不要被危言耸听,这其实大概率取决于你的部门而非公司,我在的部门经过这两年的形势成功变得小而精,小组的氛围很好,平时开发大家都合作得很开心,不管是导师还是 leader,休息了也会偶尔一起打打游戏,在这个部门我感觉挺好至少没有看到所谓的大厂 996,基本上大家 10 点来,最晚 8 点也都走了。离职的前一天刚好赶上了部门团建于是狠狠地去蹭了一只烤全羊,leader 把商旅的大 leader 请过来了,我对大领导的刻板印象是电视里那种懂得都得,但是没想到和我想的完全相反,大家举杯畅饮吹牛逼,欢声笑语,挺好,,后来想想有可能是因为大家都是技术出身很多时候也都很讨厌那一套,这也是我对互联网最满意的一点,凭本事吃饭,对于出身不是那么地依赖,也不是尔诈我虞,阿谀奉承。


第三点:学会装菜,不要没事找事

作为实习生,懂的都懂,其实在哪里都一样,如果你太着急表现自己,别人就会觉得你过分刺毛,能装菜的地方千万别装逼,艹,我感觉我就是傻逼了,这也许也是我离开的原因之一,作为实习生老老实实完成自己的工作就好,能够保质保量完成任务对于导师来说基本就差不多了,至于一些 pua 话术里面说的额外价值,我觉得对于没有转正的实习生来说毫无意义,反而会自找麻烦,因为并不会因为你原本安排 2 天切完的图你一天切完导师就给你放松自学,很多时候你做的事情是否有意义完全取决于你的导师是否愿意安排有意义的工作给你,所幸我在滴滴完整地参与了项目的技术方案到代码编写直至最后部署上线,里面沉淀了我自己的思考,经过这段实习确实让我受益匪浅。


最后一点是软实力

我觉得这也是我在这段实习中收获到的最重要的东西之一:"学会总结,及时复盘",每次周会给导师和兄弟们讲方案总是要准备很久,会去看很多的自己不知道的东西,以此来让我写的东西显得足够的高大上,记得有一次上线官网出问题了,意料之中做了一个复盘,倒不是说学到了什么代码层面的东西,更多的是让我了解了整个项目从开发到部署上线的流程,这个远远比写代码有意义,不得不说这极大地培养了我的能力,包括新技术的敏感程度,技术的深度以及口才,总结出来的东西一方面加深了自己的记忆和理解,往小了说,让我可以在以后的技术面试中就这段经历侃侃而谈,往大了说,这个让我学会从更高的视角去看问题,不再是盯着代码的一亩三分地,更多的是学会从项目的技术架构层面去看问题,第二是学会表现自己并且及时纠正自己的错误,没错,就是给别人看,自己瞎学总结是没有意义的,你是一个无比努力的人,可是大家不知道那也毫无意义,他能知道的仅仅是你能写上简历的东西,只有向别人更好的展示自己,下一次面试官看到你才会觉得你是一个善于总结和反思的人,程序员这一行也是这样,其实参与一个开源项目远远比你基础扎实更让人刮目相看,尽管你只是为一个看起来无比高大上的开源切了图,对我自己来说我只是把曾经在 wps 或者 typora 的写作 转移到了掘金或者 github,内容并没有太大的变化,这样的事情何乐而不为呢。


最后做一个收尾。


这两天想去北京附近转转,今天跑到了天安门,还是想吐槽地铁站一些人地素质问题,经典的钱包鼓起来了素质教育没跟上来,或者换句话说富起来很多并非接受过良好教育的一批人。


马上快开学了,要回学校拿保研名额,说实话我到现在都不确定哪条路是对了,大厂?还是保研?还是国企公务员?谁知道呢,每个人有每个人的说法,老员工会劝你保研进编制,新员工会劝你尽早进大厂捞钱,每个人追求的往往都是目前最缺失的,也许正是因为未来充满未知所以才无限期待,不然像我这样躺尸一周那该多无聊,脑袋都睡麻木了,这两周陆陆续续也面试了四五家公司,不得不说有大厂背书投简历就是好使,曾经拒绝过我的那些公司都拿到了 offer 然后全给拒了哈哈,不为别的就是解气,基本都是一些 b 格还比较高的独角兽公司,比如教育,云服务器,游戏行业等等,大厂我肯定还是没这个底气的哈哈我依然是大部分大厂的舔狗,不过结果不算坏,下一站是老铁厂了。


不知道是否会有之前一起工作的兄弟看到这篇文章,如果认为我有说得不恰当的地方欢迎指正。


作者:雨赎
来源:juejin.cn/post/7268289776867934266
收起阅读 »

华为鸿蒙app开发,真的遥遥领先?

前言 最近刷头条,刷到很多开始鸿蒙系统app开发者说 鸿蒙系统要崛起了 属于国家意志。于是我也在周五空闲时间去华为官网学习一下,体验一下遥遥领先的感觉。 developer.huawei.com/ 官网下载下载DevEco Studio 下载流程就不用细说了 ...
继续阅读 »

前言


最近刷头条,刷到很多开始鸿蒙系统app开发者说 鸿蒙系统要崛起了 属于国家意志。于是我也在周五空闲时间去华为官网学习一下,体验一下遥遥领先的感觉。
developer.huawei.com/ 官网下载下载DevEco Studio


下载流程就不用细说了 借鉴一下别人的文章,主要核心在于按照官网学习了一个ToDo的例子


鸿蒙OS应用开发初体验


启动页面


image.png


Setup


image.png


image.png



HarmonyOS-SDK:鸿蒙操作系统软件开发工具包



  • Previewer:预览器

  • Toolchains:工具链



OpenHarmony-SDK:开源鸿蒙操作系统软件开发工具包




  • ArkTS:鸿蒙生态的应用开发语言。

  • JS:JavaScript

  • Previewer:预览器

  • Toolchains:工具链



image.png


Create Project


image.png image.png


配置工程


image.png 项目名称、包名、存储路径、编译SDK版本、模型,语言、设备类型等。


工程目录结构


image.png



  • AppScope:存放应用全局所需要的资源文件。

  • entry:应用主模块,存放HarmonyOS应用的代码、资源等。

  • on_modules:工程依赖包,存放工程依赖的源文件。

  • build-profile.json5是工程级配置信息,包括签名、产品配置等。

  • hvigorfile.ts是工程级编译构建任务脚本,hvigor是基于任务管理机制实现的一款全新的自动化构建工具,主要提供任务注册编排,工程模型管理、配置管理等核心能力。

  • oh-package.json5是工程级依赖配置文件,用于记录引入包的配置信息


TODO例子


这里我我干掉了初始代码 实现了一个TODO例子 源码贴出来了


image.png



import ArrayList from '@ohos.util.ArrayList'
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
private taskList:Array<String>=[
'吃饭',
'睡觉',
'遛娃',
'学习'
]
build() {
Column() {
Text('待办')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.align(Alignment.Start)
ForEach(this.taskList,(item)=>{
ToDoItem({ content: item })
})

}.height('100%')
.width('100%')
.backgroundColor('#e6e6e6')

}
}

@Component
struct ToDoItem {
private content: string;
@State isComplete: boolean = false;
@State isClicked: boolean = false;
build() {
Row() {
Image($r('app.media.app_icon'))
.width(20)
.margin(10)
Text(this.content)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.opacity(this.isComplete ? 0.4 : 1)
.decoration({ type: this.isComplete ? TextDecorationType.Underline : TextDecorationType. })
}.borderRadius(24)
.width('100%')
.padding(20)
.backgroundColor(this.isClicked ? Color.Gray : Color.White)
.margin(10)
.onClick(
()=>{
this.isClicked = true; // 设置点击状态为true
setTimeout(() => {
this.isClicked = false; // 0.5秒后恢复点击状态为false
}, 500);
this.isComplete=!this.isComplete
}
)
}
}

总结


在模拟器上 点击啥的效果还好 但是在我华为p40上的真机运行效果真的点击效果响应太慢了吧。本人也是华为手机的爱好者,但这一次真的不敢苟同谁敢用这样的平台开发app。有深入学习的大佬指点一下,望花粉勿喷。


作者:阡陌昏晨
来源:juejin.cn/post/7302070112639385651
收起阅读 »

鸿蒙OS应用开发初体验

什么是HarmonyOS? HarmonyOS(鸿蒙操作系统)是华为公司开发的一款基于微内核的分布式操作系统。它是一个面向物联网(IoT)时代的全场景操作系统,旨在为各种类型的设备提供统一的操作系统平台和开发框架。HarmonyOS 的目标是实现跨设备的无缝协...
继续阅读 »

什么是HarmonyOS?


HarmonyOS(鸿蒙操作系统)是华为公司开发的一款基于微内核的分布式操作系统。它是一个面向物联网(IoT)时代的全场景操作系统,旨在为各种类型的设备提供统一的操作系统平台和开发框架。HarmonyOS 的目标是实现跨设备的无缝协同和高性能。


DevEco Studio



对标Android Studio,开发鸿蒙OS应用的IDE。



启动页面


image.png


Setup


image.png


image.png



HarmonyOS-SDK:鸿蒙操作系统软件开发工具包



  • Previewer:预览器

  • Toolchains:工具链



OpenHarmony-SDK:开源鸿蒙操作系统软件开发工具包




  • ArkTS:鸿蒙生态的应用开发语言。

  • JS:JavaScript

  • Previewer:预览器

  • Toolchains:工具链



image.png


Create Project


image.png
image.png


配置工程


image.png
项目名称、包名、存储路径、编译SDK版本、模型,语言、设备类型等。


工程目录结构


image.png



  • AppScope:存放应用全局所需要的资源文件。

  • entry:应用主模块,存放HarmonyOS应用的代码、资源等。

  • on_modules:工程依赖包,存放工程依赖的源文件。

  • build-profile.json5是工程级配置信息,包括签名、产品配置等。

  • hvigorfile.ts是工程级编译构建任务脚本,hvigor是基于任务管理机制实现的一款全新的自动化构建工具,主要提供任务注册编排,工程模型管理、配置管理等核心能力。

  • oh-package.json5是工程级依赖配置文件,用于记录引入包的配置信息。


Device Manager


image.png


创建好的模拟器会出现在这里。
image.png


启动模拟器之后,会在设备列表中出现。


image.png


编译运行


image.png
编译运行,可以从通知栏看到输出的文件并不是apk,而是hap(Harmony Application Package的缩写)。是鸿蒙操作系统设计的应用程序包格式。


image.png
.hap 文件包含了应用程序的代码、资源和元数据等信息,用于在 HarmonyOS 设备上安装和运行应用程序。


image.png


整体开发流程跟Android基本无差,所以熟悉Android开发的同学上手基本没啥难度。


ArkTS



ArkTS是鸿蒙生态的应用开发语言。它在保持TypeScript(简称TS)基本语法风格的基础上,对TS的动态类型特性施加更严格的约束,引入静态类型。同时,提供了声明式UI、状态管理等相应的能力,让开发者可以以更简洁、更自然的方式开发高性能应用。
developer.harmonyos.com/cn/develop/…



最简单例子:


@Entry
@Component
struct Index {
@State message: string = 'Hello World'

build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}

看起来非常简洁,采用的是声明式UI,写过Flutter的同学对声明式UI应该不会陌生。从最简单的例子初步了解下基本语法:



  • 装饰器,用于装饰类、结构、方法以及变量,并赋予其特殊的含义。如@Entry、@Component、@State都是装饰器。

  • 自定义组件:可复用的UI单元,可组合其他组件,如上述被@Component装饰的stuct Index。

  • UI 描述:以声明式的方式来描述UI的结构,如上述的build()方法中的代码块。

  • 系统组件:ArkUI框架中默认内置的基础和容器组件,可直接被开发者调用,比如示例中的Row、Column、Text。

  • 属性方法:组件可以通过链式调用配置多项属性,如fontSize()、width()、height()、backgroundColor()等。

  • 事件方法:组件可以通过链式调用设置多个事件的响应逻辑,本例代码不涉及,可以进一步学习文档。


这里就不是Android熟悉的java或kotlin语言了,编程语言变成了类JavaScript的前端语言,这意味着我们需要适应用前端的思想去开发鸿蒙应用,比如状态管理。


总结


本文纯初体验遥遥领先背后的鸿蒙操作系统,基于开发者平台提供的IDE、鸿蒙生态的开发语言ArkTS,通过模拟器运行起来了鸿蒙OS版HelloWorld。对于已经有移动开发经验的同学来说上手可以说非常快,官方文档也非常详尽,ArkTS语法也非常简洁易学,如果大家对华为生态的应用开发感兴趣或者想深入学习借鉴华为做OS和物联网的思路,鸿蒙系统就是一个标杆。


作者:巫山老妖
来源:juejin.cn/post/7295576148363886631
收起阅读 »

数据库入门:掌握MySQL数据库的五大基本操作,轻松驾驭数据世界!

对数据库进行查询和修改操作的语言叫做 SQL(Structured Query Language,结构化查询语言)。SQL 语言是目前广泛使用的关系数据库标准语言,是各种数据库交互方式的基础。在之前的文章中,我们已经掌握了SQL语言的基本概念以及常用的DDL(...
继续阅读 »

对数据库进行查询和修改操作的语言叫做 SQL(Structured Query Language,结构化查询语言)。SQL 语言是目前广泛使用的关系数据库标准语言,是各种数据库交互方式的基础。

在之前的文章中,我们已经掌握了SQL语言的基本概念以及常用的DDL(数据定义)和DML(数据操作)语句。接下来,我们将探讨如何运用这些知识进行MySQL数据库的操作。在本篇文章中,我们将详细介绍基本的增、删、改、查等操作方法。

Description

首先我们来回顾一下标识符命名规则:

  • 数据库名、表名不得超过30个字符,变量名限制为29个。

  • 必须只能包含 A–Z, a–z, 0–9, _共63个字符。

  • 数据库名、表名、字段名等对象名中间不要包含空格;

  • 同一个MySQL软件中,数据库不能同名;同一个库中,表不能重名;同一个表中,字段不能重名。

  • 必须保证你的字段没有和保留字、数据库系统或常用方法冲突。如果坚持使用,请在SQL语句中使用`(着重号)引起来。

  • 保持字段名和类型的一致性:在命名字段并为其指定数据类型的时候一定要保证一致性,假如数据类型在一个表里是整数,那在另一个表里可就别变成字符型了。

下面我们来看数据库的基本操作。

一、创建数据库

在 MySQL 中,可以使用 CREATE DATABASE 语句创建数据库,下面我们介绍三种创建数据库的方式:
方式1:创建数据库 ( 数据库名存在时,会报错 )

CREATE DATABASE 数据库名;

方式2:创建数据库并指定字符集 ( 数据库名存在时,会报错 )

CREATE DATABASE 数据库名 CHARACTER SET 字符集;

('不指定字符集会选择默认字符集')    

方式3:判断数据库是否已经存在,不存在则创建数据库 (推荐)

CREATE DATABASE IF NOT EXISTS 数据库名;

注意: DATABASE 不能改名。一些可视化工具可以改名,它是建新库,把所有表复制到新库,再删旧库完成的。

编程学习,从云端源想开始,课程视频、在线书籍、在线编程、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

实例:最简单的创建MySQL数据库的语句
在 MySQL 中创建一个名为 test_db 的数据库。在 MySQL 命令行客户端输入 SQL 语句CREATE DATABASE test_db;即可创建一个数据库,输入的 SQL 语句与执行结果如下。

Description

“Query OK, 1 row affected (0.12 sec);”提示中,“Query OK”表示上面的命令执行成功,“1 row affected”表示操作只影响了数据库中一行的记录,“0.12 sec”则记录了操作执行的时间。

若再次输入CREATE DATABASE test_db;语句,则系统会给出错误提示信息,如下所示:

Description

提示不能创建“test_db”数据库,数据库已存在。MySQL 不允许在同一系统下创建两个相同名称的数据库。

可以加上IF NOT EXISTS从句,就可以避免类似错误,如下所示:

Description

二、查看数据库

在 MySQL 中,可使用 SHOW DATABASES 语句来查看或显示当前用户权限范围以内的数据库。语法格式如下:

查看当前所有的数据库

SHOW DATABASES; #有一个S,代表多个数据库

查看当前正在使用的数据库

SELECT DATABASE(); #使用的一个 mysql 中的全局函数

查看指定库下所有的表

SHOW TABLES FROM 数据库名;

查看数据库的创建信息

SHOW CREATE DATABASE 数据库名;
或者:
SHOW CREATE DATABASE 数据库名\G

注意: 要操作表格和数据之前必须先说明是对哪个数据库进行操作,否则就要对所有对象加上“数据库名.”。

实例1:查看所有数据库

列出当前用户可查看的所有数据库:

Description

可以发现,在上面的列表中有 6 个数据库,它们都是安装 MySQL 时系统自动创建的,其各自功能如下:

information_schema: 主要存储了系统中的一些数据库对象信息,比如用户表信息、列信息、权限信息、字符集信息和分区信息等。


mysql: MySQL 的核心数据库,类似于 SQL Server 中的 master 表,主要负责存储数据库用户、用户访问权限等 MySQL 自己需要使用的控制和管理信息。常用的比如在 mysql 数据库的 user 表中修改 root 用户密码。

performance_schema: 主要用于收集数据库服务器性能参数。

sakila: MySQL 提供的样例数据库,该数据库共有 16 张表,这些数据表都是比较常见的,在设计数据库时,可以参照这些样例数据表来快速完成所需的数据表。

sys: MySQL 5.7 安装完成后会多一个 sys 数据库。sys 数据库主要提供了一些视图,数据都来自于 performation_schema,主要是让开发者和使用者更方便地查看性能问题。

world: world 数据库是 MySQL 自动创建的数据库,该数据库中只包括 3 张数据表,分别保存城市,国家和国家使用的语言等内容。

实例2:创建并查看数据库

先创建一个名为 test_db 的数据库:

CREATE DATABASE test_db;

Query OK, 1 row affected (0.12 sec)

再使用 SHOW DATABASES 语句显示权限范围内的所有数据库名,如下所示:
Description

你看,刚才创建的数据库已经被显示出来了。

三、修改数据库

在 MySQL 数据库中只能对数据库使用的字符集和校对规则进行修改,数据库的这些特性都储存在 db.opt 文件中。下面我们来介绍一下修改数据库的基本操作。

在 MySQL 中,可以使用 ALTER DATABASE 来修改已经被创建或者存在的数据库的相关参数。修改数据库的语法格式为:

语法说明如下:

  • ALTER DATABASE 用于更改数据库的全局特性。

  • 使用 ALTER DATABASE 需要获得数据库 ALTER 权限。

  • 数据库名称可以忽略,此时语句对应于默认数据库。

  • CHARACTER SET 子句用于更改默认的数据库字符集。

四、删除数据库

当数据库不再使用时应该将其删除,以确保数据库存储空间中存放的是有效数据。删除数据库是将已经存在的数据库从磁盘空间上清除,清除之后,数据库中的所有数据也将一同被删除。

在 MySQL 中,需要删除已创建的数据库时,可以使用DROP DATABASE 语句。其语法格式为:

sql复制代码DROP DATABASE [ IF EXISTS ] <数据库名>

语法说明如下:

  • <数据库名>:指定要删除的数据库名。

  • IF EXISTS:用于防止当数据库不存在时发生错误。

  • DROP DATABASE:删除数据库中的所有表格并同时删除数据库。
    使用此语句时要非常小心,以免错误删除。如果要使用 DROP DATABASE,需要获得数据库 DROP 权限。

注意: MySQL 安装后,系统会自动创建名为 information_schema 和 mysql 的两个系统数据库,系统数据库存放一些和数据库相关的信息,如果删除了这两个数据库,MySQL 将不能正常工作。

还有一点值得注意的是: 在进行删除操作的时候一定要谨慎,在执行DROP DATABASE命令后,MySQL 不会给出任何提示确认信息。并且删除数据库后,数据库中存储的所有数据表和数据也将一同被删除,而且不能恢复,因此最好在删除数据库之前先将数据库进行备份。

实例 :在 MySQL 中创建一个测试数据库 test_db_del

Description

使用命令行工具将数据库 test_db_del 从数据库列表中删除,输入的 SQL 语句与执行结果如下所示:

Description

此时数据库 test_db_del 不存在。再次执行相同的命令,直接使用 DROP DATABASE test_db_del,系统会报错,如下所示:

Description

如果使用IF EXISTS从句,可以防止系统报此类错误,如下所示:

Description

五、选择数据库

在 MySQL 中就有很多系统自带的数据库,那么在操作数据库之前就必须要确定是哪一个数据库。在 MySQL 中,USE 语句用来完成一个数据库到另一个数据库的跳转。

当用 CREATE DATABASE 语句创建数据库之后,该数据库不会自动成为当前数据库,需要用 USE 来指定当前数据库。其语法格式为:

xml复制代码USE <数据库名>

该语句可以通知 MySQL 把<数据库名>所指示的数据库作为当前数据库。该数据库保持为默认数据库,直到语段的结尾,或者直到遇见一个不同的 USE 语句。 只有使用 USE 语句来指定某个数据库作为当前数据库之后,才能对该数据库及其存储的数据对象执行操作。

总结:

本篇文章详细介绍了MySQL数据库的新增、查看、修改、删除和选择等操作,希望对你的数据库入门学习有那么一点点的帮助。

收起阅读 »

穷鬼,是如何炼成的?

开门见山,我是西安的一名女性穷屌丝。以前的我,喜欢写一些网文,感慨一下人生,抱怨一下社会,但是,现在我不愿意写了。原因是什么呢?很简单,没钱。我再也不愿意免费为任何人提供情绪价值,同时我也不需要任何人的共鸣、欣赏或者鄙视。廉价的欣赏与我没有任何意义,有偿的即便...
继续阅读 »


开门见山,我是西安的一名女性穷屌丝。


以前的我,喜欢写一些网文,感慨一下人生,抱怨一下社会,但是,现在我不愿意写了。


原因是什么呢?很简单,没钱。我再也不愿意免费为任何人提供情绪价值,同时我也不需要任何人的共鸣、欣赏或者鄙视。


廉价的欣赏与我没有任何意义,有偿的即便是鄙视我也双手欢迎。对于不能变现的付出,我一概归为不必要的消耗。转换不成金钱的兴趣,一概一文不值!


为什么我会发生如此大的转变?是什么扼杀了我的理想主义?我想,任谁试试失业找不到工作,大半年没收入,家门口的包子都快吃不起了,给娃买衣服要买大两号的滋味,他都会瞬间明白的:万物皆可抛,钱才是我爹!


我总结了自己行走社会这10多年的教训,想想自己如今能沦为一个穷屌丝,也确确实实不是凭运气,而是凭实力来的。各位如果不想重蹈我的覆辙,就听听吧!


1

依靠别人就等于害了自己

有句老话说:行走江湖,多个朋友多条路。可经验告诉我,没有所谓的朋友,更没有别人为你铺好的路。当你一文不值的时候,也别妄想有人能给你提供任何价值。


弱者无法为弱者带来价值,而强者则不屑于为弱者带来价值。


这个世界的运行是有规律的,千万不要相信强者和弱者之间会有“交情”,世界不相信“交情”,只相信“交换”。别人即便对你有一时的同情,也丝毫改变不了你的处境。你要做的是让自己有用,这才是永恒的法则。


能靠能力就别靠关系,这样你才能走得更稳、更远。当然,如果你的家庭是达官显贵甚至皇亲国戚,那不在我的讨论范围。可如果你来自一个底层草根家庭,就别想着削尖脑袋攀关系的事了,否则你活了半辈子,到头来身无长物,一定会死得很惨!


你要清醒,攀来的关系是不牢靠的,并且随时可能消失;而你想靠攀来的关系改变命运无异于痴人说梦。抱有这种想法的人,混一辈子最后大抵还是个在底层挣扎的穷鬼。


攀关系就相当于把自己的命运依附到了别人身上,想想这是一件多么不靠谱的事情啊!且不说别人给能不能给你靠,就是给你靠了,他能保你一辈子吗?


所以今天,不管你是20岁、30岁还是40岁的底层屌丝,如果你想摆脱这辈子做穷鬼的命运,那就勇敢一点,从现在开始为自己负责!


2

没有无缘无故的爱,也没有无缘无故的恨


初到一个单位,你最先要警惕的一定是那些最早对你热情,无缘无故对你献殷勤的人,你大概率最后会死在这些人手里。


你想想,初来乍到,他既不知道你的能力,又不了解你的人品,凭啥要对你好啊?他的这种好一定是有目的的!或者是想通过对你好去拉拢你,以此来压制他的敌人;或者是想通过对你好来换取你日后在工作中的顺从和超额回报;并且这种好的前提,是你对他不会产生任何威胁。而你一旦中了圈套,误把他认做你的朋友,深谙人情世故的他会借机迅速掌握你的弱点。 


在这种不对等的关系中,一旦你的行事无法满足他的预期,或是有一天你在某一方面对他产生了威胁,他会毫不犹豫地抛弃你,甚至把你放在他的对立面。他会故伎重演地换一拨人去拉拢,以此来打压你。这种人擅长拉帮结派搞小团体,你大概率不是他的对手,最后会被他整得很惨。


当你有天被某些人针对感到摸不着头脑、百思不得其解的时候,你要静下来好好想想自己是不是在哪些方面有点优秀了。你一定是在哪里挡了别人的财路,要不就是在哪里动了别人的奶酪。


善于周旋人际关系、拉帮结派的人,往往是心眼非常小、非常善妒又自私自利的,所以为了避免被伤害,你一定要多留个心眼子,早早地远离这类人。


3

不要高看你和任何人的关系


穷鬼在日子过得还不那么落魄的时候,往往自我感觉良好,总觉得自己朋友一大把,最喜欢跟人交心。可殊不知自己在别人心里,连根葱都不是。


穷鬼甚至会傻到怀着交朋友的心态去职场,真是滑天下之大稽。穷鬼每天用义气、意气等词汇来自我标榜,自导自演着一幕幕内心戏。在他沉浸其中自我感动的时候,殊不知那是对别人的一次次无耻意淫。


职场是什么地方?职场如战场啊!真正得体的职场人,一定是客观、冷酷、谨慎和中立的。他们清醒独立,早学会了远离是非的漩涡。他们最会明哲保身,保住饭碗要紧,谁会和你这个意气用事的傻屌扯感情?


所以穷鬼们落难后,还在为被附赠的情感伤害而痛苦不已。我只能劝你省省,棒子都打不醒你吗?你还不明白,职场里怎么可能有朋友?快收起你的幼稚和真心,学着冷酷一些吧,这样你或许还有一条活路。


4

醒醒吧,把自己当个屁放了


一个穷屌丝为什么自命不凡,总感觉他生下来就背负着了不起的使命,今生必定会干出一番轰轰烈烈的事业呢?答案是:他还活在梦里!


如果一个人在学生时代做梦,那还值得被理解;如果一个人在刚出社会10年内还做梦,那我们姑且可以谅解他;但当一个人都在社会里混了20、30年了却依旧是个穷屌丝,那我们大可以唾弃和鄙视他了。因为基本上可以断定,这个穷鬼不是疯子就是傻子,这辈子大抵就是这个鬼样子了,所以,谁都不要理他!


一个最没有社会地位的穷鬼,却往往是一个最自命不凡、坚持自我的人。他们会以常人难以理解的方式存在于这个社会的角落。这些人普遍拥有一些特质:比如不爱说假话、不喜随大流、不爱占便宜、较真、自尊心强……这些不合时宜的特质让这个穷鬼最终只能沦为组织的边缘人物,甚至被清理出局。


而这个穷鬼都被人弃如敝履了,还在心里暗暗觉得自己形象伟岸、卓尔不群、是别人想成为而不能成为的人。这种自恋的人,你就是浇一万盆凉水到他脸上,恐怕也叫不醒他。


所以针对这种人,就得让别人给他挖一个又一个大坑,得让生活给他一个又一个大包,得让社会给他一次又一次教训……除非他自己觉醒,否则真是没救了!


5

暴力解决不了任何问题


一个穷屌丝一旦沦落到连家门口的包子都吃不起的地步,难免会对这个社会产生很大的怨恨。


当他在下雨天被开豪车的无良商人飞驰而过的轮子溅了一身泥的时候,当他在想买几个苹果却发现太贵于是说出“我再看看”这几个字,却恰好被往昔的仇人看见的时候……每当这种时候,往往是他内心最抓狂,最想找个地缝钻下去重新投胎的时候。


穷屌丝在忆往昔的时候也是心理最阴暗的时刻,他会有一万个理由来解释他今天的田地:从投胎开始的一次次选择错误、从他毕业开始的一遍遍处事失误、从他工作开始的一回回识人不慧……他终于找到了他成为穷鬼的原因。于是,他怨天怨地怨爹娘,怨家怨国怨社会,他怨每一任有眼无珠的领导,他恨每一位虚情假意的同事,他唯独不愿意怪罪他自己。


穷屌丝会想,我一个“德智体美劳”、“仁义礼智信”和“真善美”全面发展的人,如今何以被你们这群学习不好、道德败坏的缺德鬼踩在脚下?这可真是人间荒唐,世风日下啊!如今我连给孩子报补习班的钱都要勒紧裤腰带去凑,这个社会真他妈的对不起我!


一旦这种怨气日积月累而无法弥散,就会让穷鬼们彻底从一个“文疯子”变成一个“武疯子”。他变成了一只危险的、行走的火药桶,一引即爆、一触即发。这时候,大家切记远离这种垃圾人,因为他们随时有可能伤及身边的每一位无辜。


你如果是一个因为穷而逐渐变得暴力的屌丝,那我劝你一定要清醒:你的失败与这个社会无关,与这个国家无关,与你身边的一切人和事物都无关。你要从自己身上找原因,因为只有敢于承认自己的今天,才不会错过一个又一个可能的明天。


可如果你一意孤行,则只会在错误的道路上越走越远,甚至会受到法律的制裁,牵连到你的家人和孩子,永世不得翻身。


6

你需要善良吗?


穷鬼们从小就受到正统的、正面的社会教育,善良一直被当做一种优点被肯定和传承。


那么善良到底是一种美德吗?如果是20岁之前,我会毫不犹豫地点头称是;而在30岁到40岁这10年,我对善良的是非判断尚处在一个灰色地带;可到了近一两年,我会肯定的告诉你,善良绝对算不上什么优良品质。


如果你不想你的孩子成为像你一样的穷鬼,我劝你千万不要教孩子善良。不同的年代要用不同的方法,而现在的社会已经乱成马了。生存如此多艰,你再用你父母教育你的老方法来教育孩子,那就是刻舟求剑,早已不合时宜了。你想想,你都已经是个loser了,社会实践已然失败,你再让孩子去相信你那一套,那你的孩子将来大概率也是个废物。


其实在我的大孩子上幼儿园的时候,我就发现孩子的同龄人大多数都伶牙俐齿,遇好事积极表现,有好处争先恐后,荣誉面前从不谦让,遇到麻烦和责任赶紧把包袱甩给老实人。而不幸我的孩子就是那个善良老实,却常常被老师忽略的异类。


孩子就是社会和家庭的缩影,你孩子在幼儿园这个小小的丛林里成为了边缘人物,其根源就来自于你。你就看看你自己混成啥样了吧!所以,还不赶快改变思路。善良得分对谁,小心你的孩子都被别的孩子骑到头上撒尿了,还不知道怎么还击呢。


更正一下善良的观念吧:利己而不损人、有原则不越界、自尊且尊重别人、能捍卫住自己的一亩三分地,这些才是真正的善良。


所以,教好下一代,很可能就是穷鬼翻身的机会。


7

你需要诚信吗?


即便是世风日下到现今这种程度,我仍然相信诚信是一个人立足社会的基本品质。坑蒙拐骗如许家印,即便是已经成为中国首屈一指的富人,也总会有被撕下面具打入十八层地狱的一天。他欠下的成千上万普通家庭的血债,会将他和他的家族牢牢地钉在耻辱柱上,永世不得超生。


你或许会说,那还有千千万万的李家印王家印正活得逍遥自在呢!我告诉你,欠下的债总是要还的,三十年河东,三十年河西。报应来的很快的,靠让别人妻离子散、家破人亡所建立起来的声望、财富和地位危如累卵,会在顷刻间灰飞烟灭。


马云凭借过人的智商和超前的眼光搞出了一套电子商务系统,B to C模式在不到20年的时间搞垮了一众中间商、商场、甚至超市。如果不是国家及时出手反制,恐怕今天的菜市场都没有小商贩的一席之地了。


人人都是贪小便宜和短视的,由于当年的屌丝们普遍难以抵抗2毛钱差价所带来的诱惑,所以断送了今天一大片下沉市场的繁荣。如今放眼望去,大多数商场濒临倒闭,逛超市都冷冷清清,人们再也无法感受10年前那样的繁荣,再也无法体验到消费所带来的快乐。


马云的电子商务模式干倒了一批人,却也造就了一批人。互联网的工厂面对个人模式巩固和强化了沿海地区的工业优势,成就了一批沿海地区的电商新贵。内陆的企业再也竞争不过而纷纷选择丢盔弃甲,要么倒闭,要么转而去发展农产品、旅游业和文化产业。


地区之间的分工更加固化,地区贫富悬殊呈几何式扩大,现如今内地还有几个有竞争力的城市呢?真所谓先富的带动不了后富的,先富的把后富的摁死在摇篮里。于是内地的人才纷纷外流,只剩下一些拖家带口,跑也跑不掉,死也不敢死,活又活不好的中年穷鬼。


当然社会之所以走到今天责任并不在马云,可我为什么想把马云放这里来讲?虽然以我的智商无法指出马云哪里不诚信,但我总觉得这个人在信仰上是有瑕疵的。


8

谁的问题?


我承认,我自己的窘境是自己造成的。但我相信,在当今的社会里,像我一样还挣扎在温饱线上的穷鬼不在少数。而他们,反而是善良、淳朴、诚实、踏实、肯干、友善的。


如此多优秀的品质,反而沦为社会的最底层,到底是我们的问题,还是这个社会的问题呢?

作者:柯视角
来源:mp.weixin.qq.com/s/6cMUv_n3AuHwIPuiNH1NjA
e>

收起阅读 »

面试官:写了个 insert into select,就被开了?

血一般的教训,请慎用insert int0 select。同事应用之后,导致公司损失了近10w元,最终被公司开除。1事情的起因公司的交易量比较大,使用的数据库是mysql,每天的增量差不多在百万左右,公司并没有分库分表,所以想维持这个表的性能只能考虑做数据迁移...
继续阅读 »

血一般的教训,请慎用insert int0 select。同事应用之后,导致公司损失了近10w元,最终被公司开除。

1事情的起因

公司的交易量比较大,使用的数据库是mysql,每天的增量差不多在百万左右,公司并没有分库分表,所以想维持这个表的性能只能考虑做数据迁移

同事李某接到了这个任务,于是他想出了这两个方案

  • 先通过程序查询出来,然后插入历史表,再删除原表
  • 使用insert int0 select让数据库IO来完成所有操作

第一个方案使用的时候发现一次性全部加载,系统直接就OOM了,但是分批次做就过多io和时间长,于是选用了第二种方案,测试的时候没有任何问题,开开心心上线,然后被开除。

2到底发生了啥,我们复盘一下

先来看第一个方案,先看伪代码
// 1、查询对应需要迁移的数据
List list = selectData();

// 2、将数据插入历史表
insertData(list);

// 3、删除原表数据
deleteByIds(ids);

我们可以从这段代码中看到,OOM的原因很简单,我们直接将数据全部加载内存,内存不爆才怪。

再来看看第二个方案,到底发生了啥

为了维持表的性能,同时保留有效数据,经过商量定了一个量,保留10天的数据,差不多要在表里面保留1kw的数据。所以同事就做了一个时间筛选的操作,直接insert int0 select ... dateTime < (Ten days ago),爽极了,直接就避免了要去分页查询数据,这样就不存在OOM啦。还简化了很多的代码操作,减少了网络问题。

为了测试,还特意建了1kw的数据来模拟,测试环境当然是没有问题啦,顺利通过。考虑到这个表是一个支付流水表,于是将这个任务做成定时任务,并且定在晚上8点执行。

晚上量也不是很大,自然是没有什么问题,但是第二天公司财务上班,开始对账,发现资金对不上,很多流水都没有入库。最终排查发现晚上8点之后,陆陆续续开始出现支付流水插入失败的问题,很多数据因此丢失。

最终定位到了是迁移任务引起的问题,刚开始还不明所以,白天没有问题,然后想到晚上出现这样的情况可能是晚上的任务出现了影响,最后停掉该任务的第二次上线,发现没有了这样的情况。

3复盘

问题在哪里?

为什么停掉迁移的任务之后就好了呢?这个insert int0 select操作到底做了什么?我们来看看这个语句的explain

我们不难从图中看出,这个查询语句直接走了全表扫描。这个时候,我们不难猜想到一点点问题。如果全表扫描,我们这个表这么大,是不是意味着迁移的时间会很长?假若我们这个迁移时间为一个小时,那是不是意味着就解释了我们白天没有出现这样问题的原因了。但是全表扫描是最根本的原因吗?

我们不妨试试,一边迁移,一边做些的操作,还原现场。最终还是会出现这样的问题。这个时候,我们可以调整一下,大胆假设,如果不全表扫描,是不是就不会出现这样的问题。当我们将条件修改之后,果然发现没有走了全表扫描了。最终再次还原现场,问题解决了

得出结论:全表扫描导致了这次事故的发生。

这样做就解决了发生的问题,但是做为陆陆续续开始失败这个就不好解释了。

原因

在默认的事务隔离级别下:insert int0 a select b的操作a表示直接锁表,b表是逐条加锁。这也就解释了为什么出现陆续的失败的原因。在逐条加锁的时候,流水表由于多数是复合记录,所以最终部分在扫描的时候被锁定,部分拿不到锁,最终导致超时或者直接失败,还有一些在这加锁的过成功成功了。

为什么测试没有问题?

在测试的时候充分的使用了正式环境的数据来测试,但是别忽视一个问题,那就是测试环境毕竟是测试环境,在测试的时候,数据量真实并不代表就是真实的业务场景。比方说,这个情况里面就少了一个迁移的时候,大量数据的插入这样的情况。最终导致线上bug

4解决办法

既然我们避免全表扫描就可以解决,我们避免它就行了。想要避免全表扫描,对where后面的条件做索引,让我们的select查询都走索引即可。

5insert int0还能用吗?

可以

6总结

使用insert int0 select的时候请慎重,一定要做好索引。


作者:面试专栏
来源:mp.weixin.qq.com/s/EIOrrsJNmIigTOoZXuTOS

收起阅读 »

AI 辅助程序员日常,ES 分页查询 3 分钟搞定!

大家好,我是伍六七。 程序猿们在日常的工作中,经常会碰到一些数据处理的工作,比如日志查询、日志过滤等需求。 以前我们可能会自己写一个功能性代码来跑数据,现在,你只需要把你要解决的问题发给 GPT,它生成的代码,你就可以直接用了。 前段时间,阿七就接到一个小的日...
继续阅读 »

大家好,我是伍六七。


程序猿们在日常的工作中,经常会碰到一些数据处理的工作,比如日志查询、日志过滤等需求。


以前我们可能会自己写一个功能性代码来跑数据,现在,你只需要把你要解决的问题发给 GPT,它生成的代码,你就可以直接用了。


前段时间,阿七就接到一个小的日常问题,让我将所有符合条件的日志数据都导出来,日志数据存储在 ES 中。


但是阿七已经很久没写过 ES 了,完全忘记了呀。怎么办,第一想法是找有没有其他项目中有类似代码,发现找不到。马上,想到可以把问题抛给 GPT,结果我 3 分钟就搞定了!


我们把需要 GPT 做的事情和限制条件发给 GPT,prompt 如下:



帮我编写一段java代码,用来查询es数据库中,索引:index1,包含字段 keyword1、keyword2,日期是 date1 的所有内容,并导出到文件 index1_search.log。

要求如下:

1、使用 elasticsearch-rest-high-level-client-7.7.0中的 org.elasticsearch.client.RestHighLevelClient 这个类作为client



以下是一个简单的 Java 代码示例,用于查询 Elasticsearch 中的索引index1,包含字段 keyword1、keyword2,日期是 date1 的所有内容,并将结果导出到文件 index1_search.log 中。


该代码使用了 elasticsearch-rest-high-level-client-7.7.0 中的 org.elasticsearch.client.RestHighLevelClient 类作为 client。


import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class ElasticsearchQuery {

public static void main(String[] args) throws IOException {
// 创建RestHighLevelClient对象
RestHighLevelClient client = new RestHighLevelClient();

// 构建查询条件
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("keyword1", "value1"))
.must(QueryBuilders.termQuery("keyword2", "value2"))
.must(QueryBuilders.rangeQuery("date1").gte("2021-01-01").lte("2021-12-31"));

// 构建查询请求
SearchRequest searchRequest = new SearchRequest("index1");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(queryBuilder);
searchSourceBuilder.size(1000);
searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
searchRequest.source(searchSourceBuilder);

// 执行查询请求
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

// 处理查询结果
SearchHit[] searchHits = searchResponse.getHits().getHits();
BufferedWriter writer = new BufferedWriter(new FileWriter("index1_search.log"));
for (SearchHit hit : searchHits) {
String source = hit.getSourceAsString();
writer.write(source);
writer.newLine();
}
writer.close();

// 关闭RestHighLevelClient对象
client.close();
}
}

该代码中,首先创建了一个 RestHighLevelClient 对象,然后使用 BoolQueryBuilder 构建了查询条件,包括 keyword1、keyword2 和 date1 三个字段的查询条件。


接着,使用 SearchRequest 和 SearchSourceBuilder 构建了查询请求,并执行了查询请求。最后,将查询结果导出到文件 index1_search.log 中,并关闭了 RestHighLevelClient 对象。


然后我发现数据量太大了,怎么办呢?可以分页解决,就跟谷歌浏览器的搜索分页一样,但是不会写,怎么办?继续扔问题给 GPT。


我们在原来的提示词中补充新的分页的需求,prompt 如下:



帮我编写一段java代码,用来查询es数据库中,索引:index1,包含字段keyword1、keyword2,日期是 date1 的所有内容,并导出到文件 index1_search.log。

要求如下:

1、使用 elasticsearch-rest-high-level-client-7.7.0中的 org.elasticsearch.client.RestHighLevelClient 这个类作为client

2、日志比较大,使用 scroll 循环查询,每次查询100条



以下是一个 Java 代码示例,用于查询 Elasticsearch 中的索引 index1,包含字段 keyword1、keyword2,日期是 date1 的所有内容,并将结果导出到文件 index1_search.log中。


该代码使用了 elasticsearch-rest-high-level-client-7.7.0 中的 org.elasticsearch.client.RestHighLevelClient类作为client,并使用 scroll 循环查询,每次查询 100 条。


import org.elasticsearch.action.search.ClearScrollRequest;
import org.elasticsearch.action.search.ClearScrollResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchScrollRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class ElasticsearchQuery {

public static void main(String[] args) throws IOException {
// 创建RestHighLevelClient对象
RestHighLevelClient client = new RestHighLevelClient();

// 构建查询条件
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("keyword1", "value1"))
.must(QueryBuilders.termQuery("keyword2", "value2"))
.must(QueryBuilders.rangeQuery("date1").gte("2021-01-01").lte("2021-12-31"));

// 构建查询请求
SearchRequest searchRequest = new SearchRequest("index1");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(queryBuilder);
searchSourceBuilder.size(100);
searchRequest.scroll(new TimeValue(1, TimeUnit.MINUTES));
searchRequest.source(searchSourceBuilder);

// 执行查询请求
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
String scrollId = searchResponse.getScrollId();

// 处理查询结果
BufferedWriter writer = new BufferedWriter(new FileWriter("index1_search.log"));
while (true) {
SearchHit[] searchHits = searchResponse.getHits().getHits();
for (SearchHit hit : searchHits) {
String source = hit.getSourceAsString();
writer.write(source);
writer.newLine();
}

SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(new TimeValue(1, TimeUnit.MINUTES));
searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);
if (searchResponse.getHits().getHits().length == 0) {
break;
}
}
writer.close();

// 清除scroll上下文
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(scrollId);
ClearScrollResponse clearScrollResponse = client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);

// 关闭RestHighLevelClient对象
client.close();
}
}

该代码中,首先创建了一个 RestHighLevelClient 对象,然后使用 BoolQueryBuilder 构建了查询条件,包括 keyword1、keyword2 和 date1 三个字段的查询条件。


接着,使用 SearchRequest 和 SearchSourceBuilder 构建了查询请求,并执行了查询请求。由于日志比较大,使用 scroll 循环查询,每次查询 100 条,并将查询结果导出到文件 index1_search.log 中。


最后,清除 scroll 上下文,并关闭了 RestHighLevelClient 对象。


自从有了 GPT,写代码如此简单,原来可能需要半个小时甚至 1 个小时的工作,3 分钟就搞定了,赶紧去试用吧!




关注我,送你全套我整理的 Java 岗位面试资料。这是我自己之前整理的面试题,靠着这份面试题,我从 30 人的小公司,进了 2000 人+的央企子公司,之后又进了互联网大厂。


作者:伍六七AI编程
来源:juejin.cn/post/7301910992311582720
收起阅读 »

支付宝二面:使用 try-catch 捕获异常会影响性能吗?大部分人都会答错!

文章来源:blog.csdn.net/bokerr/article/details/122655795# 前言不知道从何时起,传出了这么一句话:Java中使用try catch 会严重影响性能。然而,事实真的如此么?我们对try catch 应该畏之如猛虎么?...
继续阅读 »

文章来源:blog.csdn.net/bokerr/article/details/122655795


# 前言

不知道从何时起,传出了这么一句话:Java中使用try catch 会严重影响性能。

然而,事实真的如此么?我们对try catch 应该畏之如猛虎么?
# JVM 异常处理逻辑

Java 程序中显式抛出异常由athrow指令支持,除了通过 throw 主动抛出异常外,JVM规范中还规定了许多运行时异常会在检测到异常状况时自动抛出(效果等同athrow), 例如除数为0时就会自动抛出异常,以及大名鼎鼎的 NullPointerException 。

还需要注意的是,JVM 中 异常处理的catch语句不再由字节码指令来实现(很早之前通过 jsr和 ret指令来完成,它们在很早之前的版本里就被舍弃了),现在的JVM通过异常表(Exception table 方法体中能找到其内容)来完成 catch 语句;很多人说try catch 影响性能可能就是因为认识还停留于上古时代。

我们编写如下的类,add 方法中计算 ++x; 并捕获异常。
public class TestClass {    private static int len = 779;    public int add(int x){        try {            // 若运行时检测到 x = 0,那么 jvm会自动抛出异常,(可以理解成由jvm自己负责 athrow 指令调用)            x = 100/x;        } catch (Exception e) {            x = 100;        }        return x;    }}

使用javap 工具查看上述类的编译后的class文件
 # 编译 javac TestClass.java # 使用javap 查看 add 方法被编译后的机器指令 javap -verbose TestClass.class

忽略常量池等其他信息,下边贴出add 方法编译后的 机器指令集:
  public int add(int);    descriptor: (I)I    flags: ACC_PUBLIC    Code:      stack=2, locals=3, args_size=2         0: bipush        100   //  加载参数100         2: iload_1             //  将一个int型变量推至栈顶         3: idiv                //  相除         4: istore_1            //  除的结果值压入本地变量         5: goto          11    //  跳转到指令:11         8: astore_2            //  将引用类型值压入本地变量         9: bipush        100   //  将单字节常量推送栈顶<这里与数值100有关,可以尝试修改100后的编译结果:iconst、bipush、ldc>         10: istore_1            //  将int类型值压入本地变量        11: iload_1             //  int 型变量推栈顶        12: ireturn             //  返回      // 注意看 from  to 以及 targer,然后对照着去看上述指令      Exception table:         from    to  target type             0     5     8   Class java/lang/Exception      LineNumberTable:        line 6: 0        line 9: 5        line 7: 8        line 8: 9        line 10: 11      StackMapTable: number_of_entries = 2        frame_type = 72 /* same_locals_1_stack_item */          stack = [ class java/lang/Exception ]        frame_type = 2 /* same */


再来看 Exception table:


from=0, to=5。指令 0~5 对应的就是 try 语句包含的内容,而targer = 8 正好对应 catch 语句块内部操作。

个人理解,from 和 to 相当于划分区间,只要在这个区间内抛出了type 所对应的,“java/lang/Exception” 异常(主动athrow 或者 由jvm运行时检测到异常自动抛出),那么就跳转到target 所代表的第八行。

若执行过程中,没有异常,直接从第5条指令跳转到第11条指令后返回,由此可见未发生异常时,所谓的性能损耗几乎不存在;

如果硬是要说的话,用了try catch 编译后指令篇幅变长了;goto 语句跳转会耗费性能,当你写个数百行代码的方法的时候,编译出来成百上千条指令,这时候这句goto的带来的影响显得微乎其微。

如图所示为去掉try catch 后的指令篇幅,几乎等同上述指令的前五条。

综上所述:“Java中使用try catch 会严重影响性能” 是民间说法,它并不成立。如果不信,接着看下面的测试吧。

# 关于JVM的编译优化

其实写出测试用例并不是很难,这里我们需要重点考虑的是编译器的自动优化,是否会因此得到不同的测试结果?

本节会粗略的介绍一些jvm编译器相关的概念,讲它只为更精确的测试结果,通过它我们可以窥探 try catch 是否会影响JVM的编译优化。

前端编译与优化:我们最常见的前端编译器是 javac,它的优化更偏向于代码结构上的优化,它主要是为了提高程序员的编码效率,不怎么关注执行效率优化;例如,数据流和控制流分析、解语法糖等等。

后端编译与优化:后端编译包括 “即时编译[JIT]” 和 “提前编译[AOT]”,区别于前端编译器,它们最终作用体现于运行期,致力于优化从字节码生成本地机器码的过程(它们优化的是代码的执行效率)。

1. 分层编译

PS * JVM 自己根据宿主机决定自己的运行模式, “JVM 运行模式”;[客户端模式-Client、服务端模式-Server],它们代表的是两个不同的即时编译器,C1(Client Compiler) 和 C2 (Server Compiler)。

PS * 分层编译分为:“解释模式”、“编译模式”、“混合模式”;

解释模式下运行时,编译器不介入工作;

编译模式模式下运行,会使用即时编译器优化热点代码,有可选的即时编译器[C1 或 C2];

混合模式为:解释模式和编译模式搭配使用。

如图,我的环境里JVM 运行于 Server 模式,如果使用即时编译,那么就是使用的:C2 即时编译器。

2. 即时编译器

了解如下的几个 概念:

1. 解释模式

它不使用即时编译器进行后端优化

强制虚拟机运行于 “解释模式” -Xint

禁用后台编译 -XX:-BackgroundCompilation

2. 编译模式

即时编译器会在运行时,对生成的本地机器码进行优化,其中重点关照热点代码。
    # 强制虚拟机运行于 "编译模式"    -Xcomp    # 方法调用次数计数器阈值,它是基于计数器热点代码探测依据[Client模式=1500,Server模式=10000]    -XX:CompileThreshold=10    # 关闭方法调用次数热度衰减,使用方法调用计数的绝对值,它搭配上一配置项使用    -XX:-UseCounterDecay    # 除了热点方法,还有热点回边代码[循环],热点回边代码的阈值计算参考如下:    -XX:BackEdgeThreshold  = 方法计数器阈值[-XX:CompileThreshold] * OSR比率[-XX:OnStackReplacePercentage]    # OSR比率默认值:Client模式=933,Server模式=140    -XX:OnStackReplacePercentag=100

所谓 “即时”,它是在运行过程中发生的,所以它的缺点也也明显:在运行期间需要耗费资源去做性能分析,也不太适合在运行期间去大刀阔斧的去做一些耗费资源的重负载优化操作。

3. 提前编译器:jaotc

它是后端编译的另一个主角,它有两个发展路线,基于Graal [新时代的主角] 编译器开发,因为本文用的是 C2 编译器,所以只对它做一个了解;

第一条路线:与传统的C、C++编译做的事情类似,在程序运行之前就把程序代码编译成机器码;好处是够快,不占用运行时系统资源,缺点是"启动过程" 会很缓慢;

第二条路线:已知即时编译运行时做性能统计分析占用资源,那么,我们可以把其中一些耗费资源的编译工作,放到提前编译阶段来完成啊,最后在运行时即时编译器再去使用,那么可以大大节省即时编译的开销;这个分支可以把它看作是即时编译缓存;

遗憾的是它只支持 G1 或者 Parallel 垃圾收集器,且只存在JDK 9 以后的版本,暂不需要去关注它;JDK 9 以后的版本可以使用这个参数打印相关信息:[-XX:PrintAOT]。

# 关于测试的约束

执行用时统计

System.naoTime() 输出的是过了多少时间[微秒:10的负9次方秒],并不是完全精确的方法执行用时的合计,为了保证结果准确性,测试的运算次数将拉长到百万甚至千万次。

编译器优化的因素

上一节花了一定的篇幅介绍编译器优化,这里我要做的是:对比完全不使用任何编译优化,与使用即时编译时,try catch 对的性能影响。

通过指令禁用 JVM 的编译优化,让它以最原始的状态运行,然后看有无 try catch 的影响。

通过指令使用即时编译,尽量做到把后端优化拉满,看看 try catch 十有会影响到 jvm的编译优化。

关于指令重排序

目前尚未可知 try catch 的使用影响指令重排序;

我们这里的讨论有一个前提,当 try catch 的使用无法避免时,我们应该如何使用 try catch 以应对它可能存在的对指令重排序的影响。

指令重排序发生在多线程并发场景,这么做是为了更好的利用CPU资源,在单线程测试时不需要考虑。不论如何指令重排序,都会保证最终执行结果,与单线程下的执行结果相同;

虽然我们不去测试它,但是也可以进行一些推断,参考 volatile 关键字禁止指令重排序的做法:插入内存屏障;

假定 try catch 存在屏障,导致前后的代码分割;那么最少的try catch代表最少的分割。

所以,是不是会有这样的结论呢:我们把方法体内的 多个 try catch 合并为一个 try catch 是不是反而能减少屏障呢?这么做势必造成 try catch 的范围变大。

当然,上述关于指令重排序讨论内容都是基于个人的猜想,犹未可知 try catch 是否影响指令重排序;本文重点讨论的也只是单线程环境下的 try catch 使用影响性能。


# 测试代码

循环次数为100W ,循环内10次预算[给编译器优化预留优化的可能,这些指令可能被合并];

每个方法都会到达千万次浮点计算。

同样每个方法外层再循环跑多次,最后取其中的众数更有说服力。
public class ExecuteTryCatch {    // 100W     private static final int TIMES = 1000000;    private static final float STEP_NUM = 1f;    private static final float START_NUM = Float.MIN_VALUE;    public static void main(String[] args){        int times = 50;        ExecuteTryCatch executeTryCatch = new ExecuteTryCatch();        // 每个方法执行 50 次        while (--times >= 0){            System.out.println("times=".concat(String.valueOf(times)));            executeTryCatch.executeMillionsEveryTryWithFinally();            executeTryCatch.executeMillionsEveryTry();            executeTryCatch.executeMillionsOneTry();            executeTryCatch.executeMillionsNoneTry();            executeTryCatch.executeMillionsTestReOrder();        }    }    /**     * 千万次浮点运算不使用 try catch     * */    public void executeMillionsNoneTry(){        float num = START_NUM;        long start = System.nanoTime();        for (int i = 0; i < TIMES; ++i){            num = num + STEP_NUM + 1f;            num = num + STEP_NUM + 2f;            num = num + STEP_NUM + 3f;            num = num + STEP_NUM + 4f;            num = num + STEP_NUM + 5f;            num = num + STEP_NUM + 1f;            num = num + STEP_NUM + 2f;            num = num + STEP_NUM + 3f;            num = num + STEP_NUM + 4f;            num = num + STEP_NUM + 5f;        }        long nao = System.nanoTime() - start;        long million = nao / 1000000;        System.out.println("noneTry   sum:" + num + "  million:" + million + "  nao: " + nao);    }    /**     * 千万次浮点运算最外层使用 try catch     * */    public void executeMillionsOneTry(){        float num = START_NUM;        long start = System.nanoTime();        try {            for (int i = 0; i < TIMES; ++i){                num = num + STEP_NUM + 1f;                num = num + STEP_NUM + 2f;                num = num + STEP_NUM + 3f;                num = num + STEP_NUM + 4f;                num = num + STEP_NUM + 5f;                num = num + STEP_NUM + 1f;                num = num + STEP_NUM + 2f;                num = num + STEP_NUM + 3f;                num = num + STEP_NUM + 4f;                num = num + STEP_NUM + 5f;            }        } catch (Exception e){        }        long nao = System.nanoTime() - start;        long million = nao / 1000000;        System.out.println("oneTry    sum:" + num + "  million:" + million + "  nao: " + nao);    }    /**     * 千万次浮点运算循环内使用 try catch     * */    public void executeMillionsEveryTry(){        float num = START_NUM;        long start = System.nanoTime();        for (int i = 0; i < TIMES; ++i){            try {                num = num + STEP_NUM + 1f;                num = num + STEP_NUM + 2f;                num = num + STEP_NUM + 3f;                num = num + STEP_NUM + 4f;                num = num + STEP_NUM + 5f;                num = num + STEP_NUM + 1f;                num = num + STEP_NUM + 2f;                num = num + STEP_NUM + 3f;                num = num + STEP_NUM + 4f;                num = num + STEP_NUM + 5f;            } catch (Exception e) {            }        }        long nao = System.nanoTime() - start;        long million = nao / 1000000;        System.out.println("evertTry  sum:" + num + "  million:" + million + "  nao: " + nao);    }    /**     * 千万次浮点运算循环内使用 try catch,并使用 finally     * */    public void executeMillionsEveryTryWithFinally(){        float num = START_NUM;        long start = System.nanoTime();        for (int i = 0; i < TIMES; ++i){            try {                num = num + STEP_NUM + 1f;                num = num + STEP_NUM + 2f;                num = num + STEP_NUM + 3f;                num = num + STEP_NUM + 4f;                num = num + STEP_NUM + 5f;            } catch (Exception e) {            } finally {                num = num + STEP_NUM + 1f;                num = num + STEP_NUM + 2f;                num = num + STEP_NUM + 3f;                num = num + STEP_NUM + 4f;                num = num + STEP_NUM + 5f;            }        }        long nao = System.nanoTime() - start;        long million = nao / 1000000;        System.out.println("finalTry  sum:" + num + "  million:" + million + "  nao: " + nao);    }    /**     * 千万次浮点运算,循环内使用多个 try catch     * */    public void executeMillionsTestReOrder(){        float num = START_NUM;        long start = System.nanoTime();        for (int i = 0; i < TIMES; ++i){            try {                num = num + STEP_NUM + 1f;                num = num + STEP_NUM + 2f;            } catch (Exception e) { }            try {                num = num + STEP_NUM + 3f;                num = num + STEP_NUM + 4f;                num = num + STEP_NUM + 5f;            } catch (Exception e){}            try {                num = num + STEP_NUM + 1f;                num = num + STEP_NUM + 2f;            } catch (Exception e) { }            try {                num = num + STEP_NUM + 3f;                num = num + STEP_NUM + 4f;                num = num + STEP_NUM + 5f;            } catch (Exception e) {}        }        long nao = System.nanoTime() - start;        long million = nao / 1000000;        System.out.println("orderTry  sum:" + num + "  million:" + million + "  nao: " + nao);    }}

# 解释模式下执行测试

设置如下JVM参数,禁用编译优化
  -Xint   -XX:-BackgroundCompilation

结合测试代码发现,即使百万次循环计算,每个循环内都使用了 try catch 也并没用对造成很大的影响。

唯一发现了一个问题,每个循环内都是使用 try catch 且使用多次。发现性能下降,千万次计算差值为:5~7 毫秒;4个 try 那么执行的指令最少4条goto ,前边阐述过,这里造成这个差异的主要原因是 goto 指令占比过大,放大了问题;当我们在几百行代码里使用少量try catch 时,goto所占比重就会很低,测试结果会更趋于合理。

# 编译模式测试

设置如下测试参数,执行10 次即为热点代码
-Xcomp -XX:CompileThreshold=10 -XX:-UseCounterDecay -XX:OnStackReplacePercentage=100 -XX:InterpreterProfilePercentage=33
执行结果如下图,难分胜负,波动只在微秒级别,执行速度也快了很多,编译效果拔群啊,甚至连 “解释模式” 运行时多个try catch 导致的,多个goto跳转带来的问题都给顺带优化了;由此也可以得到 try catch 并不会影响即时编译的结论。


我们可以再上升到亿级计算,依旧难分胜负,波动在毫秒级。

# 结论

try catch 不会造成巨大的性能影响,换句话说,我们平时写代码最优先考虑的是程序的健壮性,当然大佬们肯定都知道了怎么合理使用try catch了,但是对萌新来说,你如果不确定,那么你可以使用 try catch;

在未发生异常时,给代码外部包上 try catch,并不会造成影响。

举个栗子吧,我的代码中使用了:URLDecoder.decode,所以必须得捕获异常。
private int getThenAddNoJudge(JSONObject json, String key){        if (Objects.isNull(json))            throw new IllegalArgumentException("参数异常");        int num;        try {            // 不校验 key 是否未空值,直接调用 toString 每次触发空指针异常并被捕获            num = 100 + Integer.parseInt(URLDecoder.decode(json.get(key).toString(), "UTF-8"));        } catch (Exception e){            num = 100;        }        return num;    }    private int getThenAddWithJudge(JSONObject json, String key){        if (Objects.isNull(json))            throw new IllegalArgumentException("参数异常");        int num;        try {            // 校验 key 是否未空值            num = 100 + Integer.parseInt(URLDecoder.decode(Objects.toString(json.get(key), "0"), "UTF-8"));        } catch (Exception e){            num = 100;        }        return num;    }    public static void main(String[] args){        int times = 1000000;// 百万次        long nao1 = System.nanoTime();        ExecuteTryCatch executeTryCatch = new ExecuteTryCatch();        for (int i = 0; i < times; i++){            executeTryCatch.getThenAddWithJudge(new JSONObject(), "anyKey");        }        long end1 = System.nanoTime();        System.out.println("未抛出异常耗时:millions=" + (end1 - nao1) / 1000000 + "毫秒  nao=" + (end1 - nao1) + "微秒");        long nao2 = System.nanoTime();        for (int i = 0; i < times; i++){            executeTryCatch.getThenAddNoJudge(new JSONObject(), "anyKey");        }        long end2 = System.nanoTime();        System.out.println("每次必抛出异常:millions=" + (end2 - nao2) / 1000000 + "毫秒  nao=" + (end2 - nao2) + "微秒");    }
调用方法百万次,执行结果如下:


经过这个例子,我想你知道你该如何 编写你的代码了吧?可怕的不是 try catch 而是 搬砖业务不熟练啊。

收起阅读 »

新项目为什么决定用 JDK 17了

最近在调研 JDK 17,并且试着将之前的一个小项目升级了一下,在测试环境跑了一段时间。最终,决定了,新项目要采用 JDK 17 了。 JDK 1.8:“不是说好了,他发任他发,你用 Java 8 吗?” 不光是我呀,连 Spring Boot 都开始要拥护 ...
继续阅读 »

最近在调研 JDK 17,并且试着将之前的一个小项目升级了一下,在测试环境跑了一段时间。最终,决定了,新项目要采用 JDK 17 了。


JDK 1.8:“不是说好了,他发任他发,你用 Java 8 吗?”


不光是我呀,连 Spring Boot 都开始要拥护 JDK 17了,下面这一段是 Spring Boot 3.0 的更新日志。



Spring Boot 3.0 requires Java 17 as a minimum version. If you are currently using Java 8 or Java 11, you'll need to upgrade your JDK before you can develop Spring Boot 3.0 applications.



Spring Boot 3.0 需要 JDK 的最低版本就是 JDK 17,如果你想用 Spring Boot 开发应用,你需要将正在使用的 Java 8 或 Java 11升级到 Java 17。


选用 Java 17,概括起来主要有下面几个主要原因:


1、JDK 17 是 LTS (长期支持版),可以免费商用到 2029 年。而且将前面几个过渡版(JDK 9-JDK 16)去其糟粕,取其精华的版本;


2、JDK 17 性能提升不少,比如重写了底层 NIO,至少提升 10% 起步;


3、大多数第三方框架和库都已经支持,不会有什么大坑;


4、准备好了,来吧。


拿几个比较好玩儿的特性来说一下 JDK 17 对比 JDK 8 的改进。


密封类


密封类应用在接口或类上,对接口或类进行继承或实现的约束,约束哪些类型可以继承、实现。例如我们的项目中有个基础服务包,里面有一个父类,但是介于安全性考虑,值允许项目中的某些微服务模块继承使用,就可以用密封类了。


没有密封类之前呢,可以用 final关键字约束,但是这样一来,被修饰的类就变成完全封闭的状态了,所有类都没办法继承。


密封类用关键字 sealed修饰,并且在声明末尾用 permits表示要开放给哪些类型。


下面声明了一个叫做 SealedPlayer的密封类,然后用关键字 permits将集成权限开放给了 MarryPlayer类。


public sealed class SealedPlayer permits MarryPlayer {
public void play() {
System.out.println("玩儿吧");
}
}

之后 MarryPlayer 就可以继承 SealedPlayer了。


public non-sealed class MarryPlayer extends SealedPlayer{
@Override
public void play() {
System.out.println("不想玩儿了");
}
}

继承类也要加上密封限制。比如这个例子中是用的 non-sealed,表示不限制,任何类都可以继承,还可以是 sealed,或者 final


如果不是 permits 允许的类型,则没办法继承,比如下面这个,编译不过去,会给出提示 "java: 类不得扩展密封类:org.jdk17.SealedPlayer(因为它未列在其 'permits' 子句中)"


public non-sealed class TomPlayer extends SealedPlayer {

@Override
public void play() {

}
}

空指针异常


String s = null;
String s1 = s.toLowerCase();

JDK1.8 的版本下运行:


Exception in thread "main" java.lang.NullPointerException
at org.jdk8.App.main(App.java:10)

JDK17的版本(确切的说是14及以上版本)


Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toLowerCase()" because "s" is null
at org.jdk17.App.main(App.java:14)

出现异常的具体方法和原因都一目了然。如果你的一行代码中有多个方法、多个变量,可以快速定位问题所在,如果是 JDK1.8,有些情况下真的不太容易看出来。


yield关键字


public static int calc(int a,String operation){
var result = switch (operation) {
case "+" -> {
yield a + a;
}
case "*" -> {
yield a * a;
}
default -> a;
};
return result;
}

换行文本块


如果你用过 Python,一定知道Python 可以用 'hello world'"hello world"''' hello world '''""" hello world """ 四种方式表示一个字符串,其中后两种是可以直接支持换行的。


在 JDK 1.8 中,如果想声明一个字符串,如果字符串是带有格式的,比如回车、单引号、双引号,就只能用转义符号,例如下面这样的 JSON 字符串。


String json = "{\n" +
" \"name\": \"古时的风筝\",\n" +
" \"age\": 18\n" +
"}";

从 JDK 13开始,也像 Python 那样,支持三引号字符串了,所以再有上面的 JSON 字符串的时候,就可以直接这样声明了。


String json = """
{
"
name": "古时的风筝",
"
age": 18
}
"
"";

record记录类


类似于 Lombok 。


传统的Java应用程序通过创建一个类,通过该类的构造方法实例化类,并通过getter和setter方法访问成员变量或者设置成员变量的值。有了record关键字,你的代码会变得更加简洁。


之前声明一个实体类。


public class User {
private String name;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

使用 Record类之后,就像下面这样。


public record User(String name) {

}

调用的时候像下面这样


RecordUser recordUser = new RecordUser("古时的风筝");
System.out.println(recordUser.name());
System.out.println(recordUser.toString());

输出结果



Record 类更像是一个实体类,直接将构造方法加在类上,并且自动给字段加上了 getter 和 setter。如果一直在用 Lombok 或者觉得还是显式的写上 getter 和 setter 更清晰的话,完全可以不用它。


G1 垃圾收集器


JDK8可以启用G1作为垃圾收集器,JDK9到 JDK 17,G1 垃圾收集器是默认的垃圾收集器,G1是兼顾老年代和年轻代的收集器,并且其内存模型和其他垃圾收集器是不一样的。


G1垃圾收集器在大多数场景下,其性能都好于之前的垃圾收集器,比如CMS。


ZGC


从 JDk 15 开始正式启用 ZGC,并且在 JDK 16后对 ZGC 进行了增强,控制 stop the world 时间不超过10毫秒。但是默认的垃圾收集器仍然是 G1。


配置下面的参数来启用 ZGC 。


-XX:+UseZGC

可以用下面的方法查看当前所用的垃圾收集器


JDK 1.8 的方法


jmap -heap 8877

JDK 1.8以上的版本


jhsdb jmap --heap --pid 8877

例如下面的程序采用 ZGC 垃圾收集器。



其他一些小功能


1、支持 List.of()、Set.of()、Map.of()和Map.ofEntries()等工厂方法实例化对象;


2、Stream API 有一些改进,比如 .collect(Collectors.toList())可以直接写成 .toList()了,还增加了 Collectors.teeing(),这个挺好玩,有兴趣可以看一下;


3、HttpClient重写了,支持 HTTP2.0,不用再因为嫌弃 HttpClient 而使用第三方网络框架了,比如OKHTTP;


升级 JDK 和 IDEA


安装 JDK 17,这个其实不用说,只是推荐一个网站,这个网站可以下载各种系统、各种版本的 JDK 。地址是 adoptium.net/


还有,如果你想在 IDEA 上使用 JDK 17,可能要升级一下了,只有在 2021.02版本之后才支持 JDK 17。



作者:古时的风筝
来源:juejin.cn/post/7177550894316126269
收起阅读 »

女朋友要我讲解@Controller注解的原理,真是难为我了

背景 女朋友被公司裁员一个月了,和我一样作为后端工程师,最近一直在找工作,面试了很多家还是没有找到工作,面试官问@Controller的原理,她表示一脸懵,希望我能给她讲清楚。之前我也没有好好整理这块知识,这次借助这个机会把它彻底搞清楚。 我们知道Contr...
继续阅读 »

背景


女朋友被公司裁员一个月了,和我一样作为后端工程师,最近一直在找工作,面试了很多家还是没有找到工作,面试官问@Controller的原理,她表示一脸懵,希望我能给她讲清楚。之前我也没有好好整理这块知识,这次借助这个机会把它彻底搞清楚。
太难了.jpeg


我们知道Controller注解的类能够实现接收并处理Http请求,其实在我看Spring mvc模块的源码之前也和我女朋友目前的状态一样,很疑惑,Spring框架是底层是如何实现的,通过使用Controller注解就简单的完成了http请求的接收与处理。


image.png


有疑问就好啊,因为兴趣是最好的老师,如果有兴趣才有动力去弄懂这个技术点。


看过前面的文章的同学就会知道,学习Spring的所有组件,脑袋里要有一个思路,那就是解析组件和运用组件两个流程,这是Spring团队实现组件的统一套路,大家可以回忆一下是不是这么回事。


image.png


一、Spring解析Controller注解


首先我们看看Spring是如何解析Controller注解的,打开源码看看他长啥样??


@Target({ElementType.TYPE})
@Component
public @interface Controller {
String value() default "";
}

发现Controller注解打上了Component的注解,这样Spring做类扫描的时候,发现了@Controller标记的类也会当作Bean解析并注册到Spring容器。
我们可以看到Spring的类扫描器,第一个就注册了Component注解的扫描


//org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
protected void registerDefaultFilters() {
this.includeFilters.add(new AnnotationTypeFilter(Component.class));
}

这样Spring容器启动完成之后,bean容器中就有了被Controller注解标记的bean实例了。
到这里只是单纯的把Controller标注的类实例化注册到Spring容器,和Http请求接收处理没半毛钱关系,那么他们是怎么关联起来的呢?


二、Spring解析Controller注解标注的类方法


这个时候Springmvc组件中的另外一个组件就闪亮登场了



RequestMappingHandlerMapping



RequestMappingHandlerMapping 看这个名就可以知道他的意思,请求映射处理映射器。
这里就是重点了,该类间接实现了InitializingBean方法,bean初始化后执行回调afterPropertiesSet方法,里面调用initHandlerMethods方法进行初始化handlermapping。



//类有没有加Controller的注解
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

protected void initHandlerMethods() {
//所有的bean
String[] beanNames= applicationContext().getBeanNamesForType(Object.class);

for (String beanName : beanNames) {
Class<?> beanType = obtainApplicationContext().getType(beanName);
//有Controller注解的bean
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}

这里把标注了Controller注解的实例全部找到了,然后调用detectHandlerMethods方法,检测handler方法,也就是解析Controller标注类的方法。



private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

protected void detectHandlerMethods(final Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());

if (handlerType != null) {
final Class<?> userType = ClassUtils.getUserClass(handlerType);
//查找Controller的方法
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));

methods.forEach((method, mapping) -> {
//注册
this.registry.put(mapping,new MappingRegistration<>(mapping,method));

});
}


到这里为止,Spring将Controller标注的类和类方法已经解析完成。现在再来看RequestMappingHandlerMapping这个类的作用,他就是用来注册所有Controller类的方法。


三、Spring调用Controller注解标注的方法


接着还有一个重要的组件RequestMappingHandlerAdapter
它就是用来调用我们写的Controller方法,完成请求处理的流程。
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter


@Override
public boolean supports(Object handler) {
return handler instanceof HandlerMethod;
}

protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod)
throws Exception {
//请求check
checkRequest(request);
//调用handler方法
mav = invokeHandlerMethod(request, response, handlerMethod);
//返回
return mav;
}

看到这里,就知道http请求是如何被处理的了,我们找到DispatcherServlet的doDispatch方法看看,确实是如此!!


四、DispatcherServlet调度Controller方法完成http请求


protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 从注册表查找handler
HandlerExecutionChain mappedHandler = getHandler(request);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 底层调用Controller
ModelAndView m = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 处理请求结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

DispatcherServlet是Spring mvc的总入口,看到doDispatch方法后,全部都联系起来了。。。
最后我们看看http请求在Spring mvc中的流转流程。


image.png


第一次总结SpringMvc模块,理解不到位的麻烦各位大佬指正。


作者:服务端技术栈
来源:juejin.cn/post/7222186286564311095
收起阅读 »

阁下,您的表单校验规则还维护的动吗?

web
表单校验是前端项目广泛存在的一个功能,因为Ant Design的引入,所谓的表单校验功能其实已经被抽象成了一个又一个的表单校验规则。以前并未注意到表单校验规则的可维护性,哪个组件用到,就在组件内完成即可。直到PM问了我2个问题: “我们的账号名字符数限制区间是...
继续阅读 »

表单校验是前端项目广泛存在的一个功能,因为Ant Design的引入,所谓的表单校验功能其实已经被抽象成了一个又一个的表单校验规则。以前并未注意到表单校验规则的可维护性,哪个组件用到,就在组件内完成即可。直到PM问了我2个问题:


“我们的账号名字符数限制区间是多少?”


“项目中所有涉及命名的字符数都要限制在3-26个,啥时候能改好?”


“好的”,我习惯性的打开了编辑器全局搜索。。。


后来的日子以上对话又重复了好几轮,每次内容略有不同啊!终于无法忍受的我开始动手对这部分内容专门做了重构,经过几个版本的迭代,似乎已经找到了一个好的方案将前端项目中的众多表单校验规则维护得体,便记下此文与各位前端大佬分享探讨。


影响维护性的几个问题



  1. 校验规则靠近并耦合业务组件,散落在项目各处

  2. 校验规则的复用

  3. 校验规则难以理解和可读

  4. 需要传参的校验规则

  5. 异步校验的规则


可维护的方案


image.png



该方案将所有的表单校验规则整理在src/formRules目录下统一管理,带来的收益是明显的:统一管理本身解决了问题1;./rules.ts & ./rulesHooks.tsx 作为所有业务组件消费校验规则的统一输出口,针对了问题1、2;./baseRuleCreator.tsx 针对了问题4;./baseSemiRule.tsx & ./utils.ts 针对了问题5;问题3么写注释就好了!



我们来看具体的内容:


./baseRules.tsx


// 必填的
export const requiredRule: RuleObject = {
required: true,
}

// 输入必须以字母开头
export const startLetterRule: RuleObject = {
pattern: /^[a-zA-Z]/,
message: 'Must start with a letter',
}

可以看到我们将表单校验规则拆成了原子级别,相同的规则我们只会写1遍,意外的收益在于每个校验报错信息也将是精准的。


./rules.ts


// Email
export const emailRules: RuleObject[] = [requiredRule, emailPatternRule]

// 密码
export const passwordRules: RuleObject[] = [requiredRule, length8_32Rule, passwordBanRule]

我们在该文件下自由组合复用原子化的校验规则,并导出给业务组件消费,达到了复用的目的。如果业务校验规则有修改,我们也可以仅在该文件统一做修改,完成了业务逻辑与校验规则的解耦。


./baseRuleCreator.tsx


// 校验是否与目标值匹配
export function createMatchTargetValueRule(targetValue: any): RuleObject {
return {
message: 'Does not match',
validator(rule: Rule, value: string) {
return value === targetValue ? Promise.resolve() : Promise.reject()
},
}
}

很明显该文件放的规则都是需要有入参的,不再赘述。


./baseSemiRule.tsx


// 异步校验邮箱是否重复
export const duplicatingAccountEmailSemiRule: TSemiRule = {
message: 'Email already exists',
callbackValidator: (value, resolve, reject) => {
return fetch(value).then(res => (res.data.exists ? reject() : resolve(undefined)))
},
}

显然该文件放了需要网络异步校验的规则,但为什么被命名为Semi?难道阁下忘了防抖?虽然我们还需要一个防抖函数来统一加工这些校验规则才可实用。但非常棒的地方在于,这些规则在逻辑上已经自洽了,报错信息和校验逻辑都已经完整了。所以我们并不关心防抖函数的具体实现和校验规则在业务组件中的具体应用,如有修改则我们在这里维护即可,做到了关注点的分离。


./utils.ts


export function createDebounceRule(semiRule: TSemiRule): RuleObject {
const { message, callbackValidator, delayTime = 500 } = semiRule
let timeId: NodeJS.Timeout = null

return {
message,
validator(rule, value) {
return new Promise((resolve, reject) => {
clearTimeout(timeId)
timeId = setTimeout(() => {
callbackValidator(value, resolve, reject)
}, delayTime)
})
},
}
}

防抖函数的具体实现,不再赘述。


./rulesHooks.tsx


export function useVpcConnectionNameRules(target: string) {
const rules = useMemo(() => [
requiredRule,
startLetterRule,
createMatchTargetValueRule(target),
createDebounceRule(duplicatingAccountEmailSemiRule),
], [target])

return rules
}

需要入参的校验规则和异步校验规则利用hooks做抽象,实现复用。


千里之堤溃于蚁穴,勿以事小而不为!表单校验规则虽然是代码维护性一个鲜有前端boy关注的微小领域,但如果我们能关注到项目中一砖一瓦的维护性,则可以相信整个项目大厦也将是健壮无比的。



以上方案仅个人在项目中的实践,维护性的提升无可止境,各位前端大佬如有更好的方案,希望留言不吝赐教!



作者:阿佛加德奔
来源:juejin.cn/post/7259638617546637349
收起阅读 »

扒一扒抖音是如何做线程优化的

背景 最近在对一些大厂App进行研究学习,在对某音App进行研究时,发现其在线程方面做了一些优化工作,并且其解决的问题也是之前我在做线上卡顿优化时遇到的,因此对其具体实现方案做了深入分析。本文是对其相关源码的研究加上个人理解的一个小结。 问题 创建线程卡顿 在...
继续阅读 »

背景


最近在对一些大厂App进行研究学习,在对某音App进行研究时,发现其在线程方面做了一些优化工作,并且其解决的问题也是之前我在做线上卡顿优化时遇到的,因此对其具体实现方案做了深入分析。本文是对其相关源码的研究加上个人理解的一个小结。


问题


创建线程卡顿


在Java中,真正的内核线程被创建是在执行 start函数的时候, nativeCreate的具体流程可以参考我之前的一篇分析文章 Android虚拟机线程启动过程解析 。这里假设你已经了解了,我们可以可以知道 start()函数底层涉及到一系列的操作,包括 栈内存空间分配、内核线程创建 等操作,这些操作在某些情况下可能出现长耗时现象,比如由于linux系统中,所有系统线程的创建在内核层是由一个专门的线程排队实现,那么是否可能由于队列较长同时内核调度出现问题而出现长耗时问题? 具体的原因因为没有在线下复现过此类问题,因此只能大胆猜测,不过在线上确实收集到一些case, 以下是线上收集到一个阻塞现场样本:



那么是不是不要直接在主线程创建其他线程,而是直接使用线程池调度任务就没有问题? 让我们看下 ThreadPoolExecutor.execute(Runnable command)的源码实现



从文档中可以知道,execute函数的执行在很多情况下会创建(JavaThread)线程,并且跟踪其内部实现后可以发现创建Java线程对象后,也会立即在当前线程执行start函数。



来看一下线上收集到的一个在主线程使用线程池调度任务依旧发生卡顿的现场。



线程数过多的问题


在ART虚拟机中,每创建一个线程都需要为其分配独立的Java栈空间,当Java层未显示设置栈空间大小时,native层会在FixStackSize函数会分配默认的栈空间大小.



从这个实现中,可以看出每个线程至少会占用1M的虚拟内存大小,而在32位系统上,由于每个进程可分配的用户用户空间虚拟内存大小只有3G,如果一个应用的线程数过多,而当进程虚拟内存空间不足时,创建线程的动作就可能导致OOM问题.



另一个问题是某些厂商的应用所能创建的线程数相比原生Android系统有更严格的限制,比如某些华为的机型限制了每个进程所能创建的线程数为500, 因此即使是64位机型,线程数不做控制也可能出现因为线程数过多导致的OOM问题。


优化思路


线程收敛


首先在一个Android App中存在以下几种情况会使用到线程



  • 通过 Thread类 直接创建使用线程

  • 通过 ThreadPoolExecutor 使用线程

  • 通过 ThreadTimer 使用线程

  • 通过 AsyncTask 使用线程

  • 通过 HandlerThread 使用线程


线程收敛的大致思路是, 我们会预先创建上述几个类的实现类,并在自己的实现类中做修改, 之后通过编译期的字节码修改,将App中上述使用线程的地方都替换为我们的实现类。


使用以上线程相关类一般有几种方式:



  1. 直接通过 new 原生类 创建相关实例

  2. 继承原生类,之后在代码中 使用 new 指令创建自己的继承类实例


因此这里的替换包括:



  • 修改类的继承关系,比如 将所有 继承 Thread类的地方,替换为 我们实现 的 PThread

  • 修改上述几种类直接创建实例的地方,比如将代码中存在 new ThreadPoolExecutor(..) 调用的地方替换为 我们实现的 PThreadPoolExecutor


通过字码码修改,将代码中所有使用线程的地方替换为我们的实现类后,就可以在我们的实现类做一些线程收敛的操作。


Thread类 线程收敛


在Java虚拟机中,每个Java Thread 都对应一个内核线程,并且线程的创建实际上是在调用 start()函数才开始创建的,那么我们其实可以修改start()函数的实现,将其任务调度到指定的一个线程池做执行, 示例代码如下


class ThreadProxy : Thread() {
override fun start() {
SuperThreadPoolExecutor.execute({
this@ThreadProxy.run()
}, priority = priority)
}
}

线程池 线程收敛


由于每个ThreadPoolExecutor实例内部都有独立的线程缓存池,不同ThreadPoolExecutor实例之间的缓存互不干扰,在一个大型App中可能存在非常多的线程池,所有的线程池加起来导致应用的最低线程数不容小视。


另外也因为线程池是独立的,线程的创建和回收也都是独立的,不能从整个App的任务角度来调度。举个例子: 比如A线程池因为空闲正在释放某个线程,同时B线程池确可能正因为可工作线程数不足正在创建线程,如果可以把所有的线程池合并成 一个统一的大线程池,就可以避免类似的场景。


核心的实现思路为:



  1. 首先将所有直接继承 ThreadPoolExecutor的类替换为 继承 ThreadPoolExecutorProxy,以及代码中所有new ThreadPoolExecutor(..)类 替换为 new ThreadPoolExecutorProxy(...)

  2. ThreadPoolExecutorProxy 持有一个 大线程池实例 BigThreadPool ,该线程池实例为应用中所有线程池共用,因此其核心线程数可以根据应用当前实际情况做调整,比如如果你的应用当前线程数平均是200,你可以将BigThreadPool 核心线程设置为150后,再观察其调度情况。

  3. 在 ThreadPoolExecutorProxy 的 addWorker 函数中,将任务调度到 BigThreadPool中执行



AsyncTask 线程收敛


对于AsyncTask也可以用同样的方式实现,在execute1函数中调度到一个统一的线程池执行



public abstract class AsyncTaskProxy<Params,Progress,Result> extends AsyncTask<Params,Progress,Result>{

private static final Executor THREAD_POOL_EXECUTOR = new PThreadPoolExecutor(0,20,
3, TimeUnit.MILLISECONDS,
new SynchronousQueue<>(),new DefaultThreadFactory("PThreadAsyncTask"));


public static void execute(Runnable runnable){
THREAD_POOL_EXECUTOR.execute(runnable);
}

/**
* TODO 使用插桩 将所有 execute 函数调用替换为 execute1
* @param params The parameters of the task.
* @return This instance of AsyncTask.
*/

public AsyncTask execute1(Params... params) {
return executeOnExecutor(THREAD_POOL_EXECUTOR,params);
}


}

Timer类


Timer类一般项目中使用的地方并不多,并且由于Timer一般对任务间隔准确性有比较高的要求,如果收敛到线程池执行,如果某些Timer类执行的task比较耗时,可能会影响原业务,因此暂不做收敛。


卡顿优化


针对在主线程执行线程创建可能会出现的阻塞问题,可以判断下当前线程,如果是主线程则调度到一个专门负责创建线程的线程进行工作。


    private val asyncExecuteHandler  by lazy {
val worker = HandlerThread("asyncExecuteWorker")
worker.start()
return@lazy Handler(worker.looper)
}


fun execute(runnable: Runnable, priority: Int) {
if (Looper.getMainLooper().thread == Thread.currentThread() && asyncExecute
){
//异步执行
asyncExecuteHandler.post {
mExecutor.execute(runnable,priority)
}
}else{
mExecutor.execute(runnable, priority)
}

}

32位系统线程栈空间优化


在问题分析中的环节中,我们已经知道 每个线程至少需要占用 1M的虚拟内存,而32位应用的虚拟内存空间又有限,如果希望在线程这里挤出一点虚拟内存空间来,可以参考微信的一个方案, 其利用PLT hook需改了创建线程时的栈空间大小。


而在另一篇 juejin.cn/post/720930… 技术文章中,也介绍了另一个取巧的方案 :在Java层直接配置一个 负值,从而起到一样的效果



OOM了? 我还能再抢救下!


针对在创建线程时由于内存空间不足或线程数限制抛出的OOM问题,可以做一些兜底处理, 比如将任务调度到一个预先创建的线程池进行排队处理, 而这个线程池核心线程和最大线程是一致的 因此不会出现创建线程的动作,也就不会出现OOM异常了。



另外由于一个应用可能会存在非常多的线程池,每个线程池都会设置一些核心线程数,要知道默认情况下核心线程是不会被回收的,即使一直处于空闲状态,该特性是由线程池的 allowCoreThreadTimeOut控制。



该参数值可通过 allowCoreThreadTimeOut(value) 函数修改



从具体实现中可以看出,当value值和当前值不同 且 value 为true时 会触发 interruptIdleWorkers()函数, 在该函数中,会对空闲Worker 调用 interrupt来中断对应线程



因此当创建线程出现OOM时,可以尝试通过调用线程池的 allowCoreThreadTimeOut 来触发 interruptIdleWorkers 实现空闲线程的回收。 具体实现代码如下:



因此我们可以在每个线程池创建后,将这些线程池用弱引用队列保存起来,当线程start 或者某个线程池execute 出现OOM异常时,通过这种方式来实现线程回收。


线程定位


线程定位 主要是指在进行问题分析时,希望直接从线程名中定位到创建该线程的业务,关于此类优化的文章网上已经介绍的比较多了,基本实现是通过ASM 修改调用函数,将当前类的类名或类名+函数名作为兜底线程名设置。这里就不详细介绍了,感兴趣的可以看 booster 中的实现



字节码修改工具


前文讲了一些优化方式,其中涉及到一个必要的操作是进行字节码修改,这些需求可以概括为如下



  • 替换类的继承关系,比如将 所有继承于 java.lang.Thread的类,替换为我们自己实现的 ProxyThread

  • 替换 new 指令的实例类型,比如将代码中 所有 new Thread(..) 的调用替换为 new ProxyThread(...)


针对这些通用的修改,没必要每次遇到类似需求时都 进行插件的单独开发,因此我将这种修改能力集成到开源库 LanceX插件中:github.com/Knight-ZXW/… ,我们可以通过以下 注解方便实现上述功能。


替换 new 指令


@Weaver
@Gr0up("threadOptimize")
public class ThreadOptimize {

@ReplaceNewInvoke(beforeType = "java.lang.Thread",
afterType = "com.knightboost.lancetx.ProxyThread")
public static void replaceNewThread(){
}

}

这里的 beforeType表示原类型,afterType 表示替换后的类型,使用该插件在项目编译后,项目中的如下源码



会被自动替换为



替换类的继承关系


@Weaver
@Gr0up("threadOptimize")
public class ThreadOptimize {

@ChangeClassExtends(
beforeExtends = "java.lang.Thread",
afterExtends = "com.knightboost.lancetx.ProxyThread"
)
public void changeExtendThread(){};



}

这里的beforeExtends表示 原继承父类,afterExtends表示修改后的继承父类,在项目编译后,如下源码



会被自动替换为



总结


本文主要介绍了有关线程的几个方面的优化



  • 主线程创建线程耗时优化

  • 线程数收敛优化

  • 线程默认虚拟空间优化

  • OOM优化


这些不同的优化手段需要根据项目的实际情况进行选择,比如主线程创建线程优化的实现方面比较简单、影响面也比较低,可以优先实施。 而线程数收敛需要涉及到字节码插桩、各种对象代理 复杂度会高一些,可以根据当前项目的实际线程数情况再考虑是否需要优化。


线程OOM问题主要出现在低端设备 或一些特定厂商的机型上,可能对于某些大厂的用户基数来说有一定的收益,如果你的App日活并没有那么大,这个优化的优先级也是较低的。

参考资料



1.某音App


2.内核线程创建流程


3.juejin.cn/post/720930… 虚拟内存优化: 线程 + 多进程优化


4.github.com/didi/booste…



作者:卓修武K
来源:juejin.cn/post/7212446354920407096
收起阅读 »

听别人说Vue的拖拽库都断代了,我第一个不服

web
vue-draggable-plus 前言 前段时间偶然翻掘金的过程中发现有人宣传 vue 的拖拽库断代了,那么我也来蹭一下热度。 Sortablejs Sortablejs 是一个功能强大的 JavaScript 拖拽库,并且提供了 vue 相关的组件 vu...
继续阅读 »

vue-draggable-plus


前言


前段时间偶然翻掘金的过程中发现有人宣传 vue 的拖拽库断代了,那么我也来蹭一下热度。


Sortablejs


Sortablejs 是一个功能强大的 JavaScript 拖拽库,并且提供了 vue 相关的组件 vue-draggable,并且在 vue3 的前期,提供了 vue-draggable-next,但是可能由于作者的生活过于繁忙的原因,这个库已经两年没有更新了,在当前 vue3 的版本中并不适用,于是乎本人突发奇想,写了一个 vue-draggable-plus(其实很久之前就写好了,没有可以宣传),它用于兼容 vue2.7vue3 以上的版本,下面我们来介绍一下它。


vue-draggable-plus


vue-draggable-plus 它用于延续 vue-draggable 核心理念,提供 vue 组件用于双向绑定数据列表,实现拖拽排序、克隆等功能,同时它还支持函数式、指令式的使用方式,让你使用起来更加方便,废话不多生活,我们先上图


2023-11-09 18.47.24.gif


更多演示请参考:demo


安装


npm install vue-draggable-plus

使用


vue-draggable-plus 支持三种使用方式:组件使用、函数式使用、指令式使用


下面我们来一一介绍:



  1. 组件式使用:


它和传统的vue组件的使用方式一样,支持双向绑定数据:


<template>
<VueDraggable
v-model="list"
:animation="150"
ghostClass="ghost"
@start="onStart"
@update="onUpdate"
>

<div
v-for="item in list"
:key="item.id"
>

{{ item.name }}
</div>
</VueDraggable>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { type UseDraggableReturn, VueDraggable } from 'vue-draggable-plus'
const list = ref([
{
name: 'Joao',
id: 1
},
{
name: 'Jean',
id: 2
},
{
name: 'Johanna',
id: 3
},
{
name: 'Juan',
id: 4
}
])

</script>

<style scoped>
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>



  1. 函数式使用:


<template>
<div
ref="el"
>

<div
v-for="item in list"
:key="item.id"
>

{{ item.name }}
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useDraggable } from 'vue-draggable-plus'
const list = ref([
{
name: 'Joao',
id: 1
},
{
name: 'Jean',
id: 2
},
{
name: 'Johanna',
id: 3
},
{
name: 'Juan',
id: 4
}
])
const el = ref()

const { start } = useDraggable(el, list, {
animation: 150,
ghostClass: 'ghost',
onStart() {
console.log('start')
},
onUpdate() {
console.log('update')
}
})
</script>

<style scoped>
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>

它就像你使用 vueuse 一样接受一个 element 的引用,和一个响应式列表数据



  1. 指令式使用


由于指令的特殊性,指令只能绑定您在 setup 中绑定的数据,它并不能支持异步绑定数据,如果您的数据来自于异步获取,那么请您使用组件或者函数式实现



<template>
<ul
v-draggable="[
list,
{
animation: 150,
ghostClass: 'ghost',
onUpdate,
onStart
}
]"
>
<li
v-for="item in list"
:key="item.id"
>
{{ item.name }}
</li>
</ul>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { vDraggable } from 'vue-draggable-plus'
const list = ref([
{
name: 'Joao',
id: 1
},
{
name: 'Jean',
id: 2
},
{
name: 'Johanna',
id: 3
},
{
name: 'Juan',
id: 4
}
])

function onStart() {
console.log('start')
}

function onUpdate() {
console.log('update')
}
</script>

<style scoped>
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>


指定目标容器


在 Sortablejs 官方以往的 Vue 组件中,都是通过使用组件作为列表的直接子元素来实现拖拽列表,当我们使用一些组件库时,如果组件库中没有提供列表根元素的插槽,我们很难实现拖拽列表,vue-draggable-plus 完美解决了这个问题,它可以让你在任何元素上使用拖拽列表,我们可以使用指定元素的选择器,来获取到列表根元素,然后将列表根元素作为 Sortablejs 的 container,我们来看一下用法:



  • Table.vue


<template>
<table>
<thead>
<tr>
<th>Id</th>
<th>Name</th>
</tr>
</thead>
<tbody class="el-table"> <-- 我们会将 .el-table 的选择器传递给 vue-draggable-plus -->
<tr v-for="item in list" :key="item.name" class="cursor-move">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
interface Props {
list: Record<'name' | 'id', string>[]
}
defineProps<Props>()
</script>


  • App.vue


<template>
<section>
<div>
<-- 传递 .el-table 作为根元素,将 .el-table 的子元素作为拖拽项 -->
<VueDraggable v-model="list" animation="150" target=".el-table">
<Table :list="list"></Table>
</VueDraggable>
</div>
<div class="flex justify-between">
<preview-list :list="list" />
</div>
</section>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { VueDraggable } from 'vue-draggable-plus'
import Table from './Table.vue'

const list = ref([
{
name: 'Joao',
id: '1'
},
{
name: 'Jean',
id: '2'
},
{
name: 'Johanna',
id: '3'
},
{
name: 'Juan',
id: '4'
}
])
</script>


来看效果:


2023-11-09 19.11.29.gif


结尾


如果您有使用需求,请参考文档:vue-draggable-plus,当然如果您不需要高度定制化,使用 vueuse 中的 useSortable 也是一样的。


如果它对您有用,请帮忙点个star:GitHub


友情链接:svelte-draggable-plus


作者:丶远方
来源:juejin.cn/post/7299353745506615347
收起阅读 »

一个 React 简易网页端音乐播放器

web
前言 这是一个轻量级的 react 音乐播放器,前端使用 UmiJS,后端采用网易云音乐 NODEJS API 制作。项目的 TS 声明写的比较乱,后续有空的话会发布重写 TS 的版本或者直接重构该播放器。 后续计划将右侧播放器抽离为一个单独的组件,可供页面直...
继续阅读 »

图片


前言


这是一个轻量级的 react 音乐播放器,前端使用 UmiJS,后端采用网易云音乐 NODEJS API 制作。项目的 TS 声明写的比较乱,后续有空的话会发布重写 TS 的版本或者直接重构该播放器。


后续计划将右侧播放器抽离为一个单独的组件,可供页面直接使用。


功能


现有功能



  1. 登陆 / 退出个人网易云账号

  2. 获取私人雷达歌单

  3. 播放歌曲

  4. 播放自己已有的网易云音乐歌单 / 订阅的歌单

  5. 单曲播放 / 全部循环 / 随机播放

  6. 搜索歌曲

  7. 背景图切换


计划中功能(先把饼画着)



  1. 音质选择

  2. 歌曲切换 -> 背景图变化

  3. 保存播放列表并同步到网易云

  4. 双语歌词对照

  5. 歌词自定义字体大小

  6. 歌曲查看评论 / 点赞 / 留言

  7. 详情页相似歌曲推荐

  8. 无版权歌曲或加载出错歌曲增加标记

  9. 将右侧播放器抽离成独立组件


比较有特色的地方


图片


1、右侧全局播放栏


播放栏可以清空播放列表,查看当前歌曲歌词,对播放列表的歌曲可以使用拖拽进行顺序调整。


2、主页左上角频谱图的实现


开始构建使用


1.安装项目


npm install

2.设置后台接口地址


第一个:网易云 NODEJS 服务器,到 src/utils/request.ts 将其设置为你的网易云后台 API 地址。


switch (process.env.NODE_ENV) {
case 'production':
// 你的生产环境地址 / Your production mode api
axios.defaults.baseURL = '';
break;

default:
// development
axios.defaults.baseURL = 'http://localhost:3000';
break;
}

第二个:天气地址,到 src/constant/api/weather.ts 进行设置。


然后到 src/redux/modules/Weather/action.ts 下根据你设置的天气接口改变传入数据结构,文件内均有注释。


      const info = {
// 空气质量
airQuailty: dewPt,
// 当前气温
currentTemp: temp,
// 体感气温
feelTemp: feels,
// 湿度
humidity: rh,
// 气压
baro,
// 天气描述,如晴或多云
weatherDescription: cap,
}

如果想高度自定义样式或内容的话可以到 src/pages/IndexPage/topRightWeather 进行调整。


打包发布


npm run build

部分功能预览


图片


图片


图片


作者:程序员Winn
来源:juejin.cn/post/7291960625462198324
收起阅读 »

炫酷的高亮卡片效果

web
前言 无意中在Nuxt官网发现一组高亮卡片元素的效果,发现还挺好看的,就自己试着写了一下,下面是Nuxt官网效果图,边框会随着鼠标移动,并且周围的卡片也会“染上”。 我实现的效果如下 实现过程 写好六个卡片 下面看代码,先用HTML写六个div元素,并...
继续阅读 »

前言



无意中在Nuxt官网发现一组高亮卡片元素的效果,发现还挺好看的,就自己试着写了一下,下面是Nuxt官网效果图,边框会随着鼠标移动,并且周围的卡片也会“染上”。



Video_2023-11-14_180220.gif


我实现的效果如下


Video_2023-11-14_175549-Trim.gif


实现过程


写好六个卡片


下面看代码,先用HTML写六个div元素,并设置好基础样式。


    <div class="box">
<div class="col">
<div class="element">
<div class="mask">
div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
<div class="col">
<div class="element">
<div class="mask">div>
div>
div>
div>

body {
margin: 0;
padding: 0;
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
background-color: #0D1428;
}

.box {
width: 1200px;
display: flex;
flex-wrap: wrap;
}

.col {
width: calc((100% - 4 * 20px) / 4);
height: 180px;
padding: 10px;
}
.element {
background: #172033;
height: 100%;
position: relative;
border-radius: 10px;
}


image.png


JS获取卡片坐标距离鼠标坐标的距离


使用JS获取每一个卡片坐标距离鼠标坐标的距离,并将这个值设置到元素的style中作为一个变量。


var elements = document.getElementsByClassName("element");
// 添加鼠标移动事件监听器
document.addEventListener("mousemove", function (event) {
// 获取鼠标位置
var mouseX = event.pageX;
var mouseY = event.pageY;

// 遍历元素并输出距离鼠标的坐标
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
var rect = element.getBoundingClientRect();
var elementX = rect.left + window.pageXOffset;
var elementY = rect.top + window.pageYOffset;

var distanceX = mouseX - elementX;
var distanceY = mouseY - elementY;

// 将距离值设置到每一个卡片元素上面
element.style.setProperty('--x', distanceX + 'px');
element.style.setProperty('--y', distanceY + 'px');
}
});

我们检查控制台可以看到,值已经设置上去了,并且随着鼠标的移动,这个值是在不断变化的


image.png


给元素设置径向渐变


随后我们在element这个伪元素上设置一个径向渐变的CSS效果, 径向渐变的圆心坐标为当前元素距离当前鼠标坐标的距离。再使用mask遮罩,只留出3px的距离作为渐变效果展示。


.element::before {
content: '';
position: absolute;
width: calc(100% + 3px);
height: calc(100% + 3px);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 10px;
background: radial-gradient(250px circle at var(--x) var(--y),#00DC82 0,transparent 100%);;
}
.element .mask {
position: absolute;
inset: 3px;
background: #172033;
border-radius: 10px;

}

至此,效果就完全实现啦


image.png



作者:林黛玉倒拔垂杨柳
来源:juejin.cn/post/7301266090750115877
收起阅读 »

深入了解 JavaScript 中 Object 的重要属性

web
JavaScript 中的 Object 是一种非常灵活且强大的数据类型,它允许我们创建和操作键值对。在本文中,我们将深入探讨 Object 的一些重要属性,以便更好地理解和利用这个关键的数据结构。 1. Object.keys() Object.keys()...
继续阅读 »

JavaScript 中的 Object 是一种非常灵活且强大的数据类型,它允许我们创建和操作键值对。在本文中,我们将深入探讨 Object 的一些重要属性,以便更好地理解和利用这个关键的数据结构。


1. Object.keys()


Object.keys() 方法返回一个包含给定对象的所有可枚举属性的字符串数组。这对于获取对象的所有键是非常有用的。


示例:


const myObject = {
name: 'John',
age: 30,
job: 'Developer'
};

const keys = Object.keys(myObject);
console.log(keys); // ['name', 'age', 'job']

2. Object.values()


Object.values() 方法是 JavaScript 中用于获取对象所有可枚举属性值的一个非常便捷的工具。通过调用该方法,我们可以轻松地将对象的值提取为一个数组,而无需手动遍历对象的属性。这样一来,我们能够更加高效地对对象的值进行处理和操作。这一特性对于处理对象数据非常有用,例如在需要对对象的值进行计算、过滤或展示时,可以直接利用 Object.values() 方法获取到对象的所有值,然后进行进一步的处理。这样不仅能简化代码逻辑,还能提升代码的可读性和可维护性。


示例:


const myObject = {
name: 'John',
age: 30,
job: 'Developer'
};

const values = Object.values(myObject);
console.log(values); // ['John', 30, 'Developer']

3. Object.entries()


Object.entries() 方法返回一个给定对象自己的所有可枚举属性的键值对数组。这对于遍历对象的键值对非常有用。


示例:


const myObject = {
name: 'John',
age: 30,
job: 'Developer'
};

const entries = Object.entries(myObject);
console.log(entries);
// [['name', 'John'], ['age', 30], ['job', 'Developer']]

4. Object.assign()


Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。这对于对象的浅拷贝非常有用。


示例:


const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const result = Object.assign({}, target, source);
console.log(result); // { a: 1, b: 4, c: 5 }

5. Object.freeze()


Object.freeze() 方法冻结一个对象,防止添加新属性,删除现有属性或修改属性的值。这对于创建不可变对象非常有用。


示例:


const myObject = {
name: 'John',
age: 30
};

Object.freeze(myObject);

// 下面的操作将无效
myObject.age = 31;
delete myObject.name;
myObject.job = 'Developer';

console.log(myObject); // { name: 'John', age: 30 }

6. Object.defineProperty()


Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性。这对于定义属性的特性非常有用。


示例:


const myObject = {};

Object.defineProperty(myObject, 'name', {
value: 'John',
writable: false, // 不能被修改
enumerable: true, // 可以被枚举
configurable: true // 可以被删除
});

console.log(myObject.name); // 'John'
myObject.name = 'Jane'; // 这里会被忽略,因为属性是不可写的

结论


Object 是 JavaScript 中一个关键的数据类型,通过深入了解其中的一些重要属性,我们可以更灵活地操作和管理对象。以上介绍的方法只是 Object 提供的众多功能之一,掌握这些属性将有助于更好地利用 JavaScript 中的对象。希望本文能够帮助你更深入地理解和使用 Object


作者:_XU
来源:juejin.cn/post/7301976895913951269
收起阅读 »

当你穿越到道诡异仙的世界,如何利用密码学知识区分幻想和现实?

《道诡异仙》是一部流行的网络小说。 其中,剧情讲述了男主角李火旺穿越到诡异世界,但意识时不时会回到原来的现代社会中。两个世界时不时交错,男主角陷入到了混乱当中,一直在疑惑到底哪边世界是真实的,也因此发展出了精彩的故事。 那么,作为一个程序员,如果面临这样的处境...
继续阅读 »

《道诡异仙》是一部流行的网络小说。


其中,剧情讲述了男主角李火旺穿越到诡异世界,但意识时不时会回到原来的现代社会中。两个世界时不时交错,男主角陷入到了混乱当中,一直在疑惑到底哪边世界是真实的,也因此发展出了精彩的故事。


那么,作为一个程序员,如果面临这样的处境,有没有办法利用专业知识区分世界是否是真实的呢?


其实不论什么样的异世界,数学始终不变,我们可以利用密码学背后的数学原理,来检查一个世界是否是真实世界。


在剧情中,男主角李火旺一直怀疑他所处的“现代世界”是幻觉,那么,我们很容易想到,幻觉没办法伪造算力,只要我们构造一个需要一定算力的数学问题,再交给“现代世界”的女主角杨娜去找计算机计算就可以了。


但是考虑到书中"诡异世界"并没有关于计算的神通,其数学发展水平也有限,所以我们构造出的问题应该是难以计算,但是又易于检验的。这样的问题与密码学所需的数学原理非常相似,我们可以利用一个简单的事实:



计算两个大质数的乘积非常简单,但是把两个大质数的乘积质因数分解却非常困难。



所以我们可以设计这样一个方案:



  1. 首先教会"诡异世界”一侧的女主角白灵淼学会基本算术(只要到整数乘法就可以了)。接下来,指挥白灵淼生成两个大质数,并且把它们的乘积告诉男主。

  2. 待男主穿越回“现代世界”,把这个乘积告诉"现代世界"女主角杨娜,请她去找计算机计算它的质因数分解,之后再告诉男主。

  3. 男主回到诡异世界,检查"现代世界"给出的质因数分解结果是否正确,如果正确,那么"现代世界"必定是真实的。


那么,如何在基础算术之内,生成较大的质数呢?我们可以利用费马小定理:



如果p是一个质数,而整数a不是p的倍数,则a^(p-1) 除以p余1 。



实际上,取a为偶数,ap1×p+1a^{p-1} \times p+1在多数情况下都是质数。在不那么严格的情况下,我们完全可以把这些伪质数当作质数来使用。


针对验证世界是否存在算力的场景,我们只需要选择两个大约几十万的整数就可以了,比如:


12515+1=1036816717+1=32659312^{5-1} * 5+1 = 103681\\
6^{7-1} * 7+1 = 326593

如果怕踩到坑,可以拿一些小质数试验一下。


之后我们计算它们的乘积,得到了 3386148883333861488833


这些计算量稍微有点大,但是应该还在小白的能力范围内,最多花上一个小时,足够完成计算了。


注意,为了防止幻觉作弊,小白只告诉李火旺最终的乘积,不需要告诉李火旺两个质因数。


接下来,让我们的主角回到"现代世界",把3386148883333861488833交给"现代世界"女主角杨娜,要求她找计算机和程序员对33861488833做因式分解。


接下来杨娜大约要花一点钱,比如她找到了winter,因式分解的代码这样写:


let p = new Array(Math.ceil(Math.sqrt(33861488833))).fill(1)

p[0] = 0;
p[1] = 0;
for(let i = 2; i < p.length; i++) {
if(i === 0)
continue;
if(33861488833 % i === 0)
console.log(i);
for(let j = i * 2; j < p.length; j += i)
p[j] = 0;
}

//运行结果:103681

用计算机计算这个循环只需要几秒,但是如果是人肉计算,这个工作量几乎是不可完成的。


幻觉再怎么厉害,也不可能帮助李火旺超越数学,算出这个因式分解的结果。


如果在"现代世界"中,算出了正确的因式分解结果,因为李火旺本人并不知道质因数,所以可以确定不可能是李火旺的幻觉。


这样就可以验证"现代世界"的真实性了。


换句话说,即使"现代世界"是幻觉,那也是一个有巨大算力的幻觉系统,那么《道诡异仙》的故事可能就变成另一种风格了。


作者:winter
来源:juejin.cn/post/7250718023815528485
收起阅读 »

实现异步编程,这个工具类你得掌握!

前言 最近看公司代码,多线程编程用的比较多,其中有对CompletableFuture的使用,所以想写篇文章总结下 在日常的Java8项目开发中,CompletableFuture是很强大的并行开发工具,其语法贴近java8的语法风格,与stream一起使用也...
继续阅读 »

前言


最近看公司代码,多线程编程用的比较多,其中有对CompletableFuture的使用,所以想写篇文章总结下


在日常的Java8项目开发中,CompletableFuture是很强大的并行开发工具,其语法贴近java8的语法风格,与stream一起使用也能大大增加代码的简洁性


大家可以多应用到工作中,提升接口性能,优化代码!


觉得有收获,希望帮忙点赞,转发下哈,谢谢,谢谢


基本介绍


CompletableFuture是Java 8新增的一个类,用于异步编程,继承了Future和CompletionStage


这个Future主要具备对请求结果独立处理的功能,CompletionStage用于实现流式处理,实现异步请求的各个阶段组合或链式处理,因此completableFuture能实现整个异步调用接口的扁平化和流式处理,解决原有Future处理一系列链式异步请求时的复杂编码


图片


Future的局限性


1、Future 的结果在非阻塞的情况下,不能执行更进一步的操作


我们知道,使用Future时只能通过isDone()方法判断任务是否完成,或者通过get()方法阻塞线程等待结果返回,它不能非阻塞的情况下,执行更进一步的操作。


2、不能组合多个Future的结果


假设你有多个Future异步任务,你希望最快的任务执行完时,或者所有任务都执行完后,进行一些其他操作


3、多个Future不能组成链式调用


当异步任务之间有依赖关系时,Future不能将一个任务的结果传给另一个异步任务,多个Future无法创建链式的工作流。


4、没有异常处理


现在使用CompletableFuture能帮助我们完成上面的事情,让我们编写更强大、更优雅的异步程序


基本使用


创建异步任务


通常可以使用下面几个CompletableFuture的静态方法创建一个异步任务


public static CompletableFuture runAsync(Runnable runnable);              //创建无返回值的异步任务
public static CompletableFuture runAsync(Runnable runnable, Executor executor);     //无返回值,可指定线程池(默认使用ForkJoinPool.commonPool)
public static CompletableFuture supplyAsync(Supplier supplier);           //创建有返回值的异步任务
public static CompletableFuture supplyAsync(Supplier supplier, Executor executor); //有返回值,可指定线程池


使用示例:



Executor executor = Executors.newFixedThreadPool(10);
CompletableFuture future = CompletableFuture.runAsync(() -> {
   //do something
}, executor);
int poiId = 111;
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
PoiDTO poi = poiService.loadById(poiId);
 return poi.getName();
});
// Block and get the result of the Future
String poiName = future.get();

使用回调方法


通过future.get()方法获取异步任务的结果,还是会阻塞的等待任务完成


CompletableFuture提供了几个回调方法,可以不阻塞主线程,在异步任务完成后自动执行回调方法中的代码


public CompletableFuture thenRun(Runnable runnable);            //无参数、无返回值
public CompletableFuture thenAccept(Consumersuper T> action);         //接受参数,无返回值
public CompletableFuture thenApply(Functionsuper T,? extends U> fn); //接受参数T,有返回值U


使用示例:



CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello")
                          .thenRun(() -> System.out.println("do other things. 比如异步打印日志或发送消息"));
//如果只想在一个CompletableFuture任务执行完后,进行一些后续的处理,不需要返回值,那么可以用thenRun回调方法来完成。
//如果主线程不依赖thenRun中的代码执行完成,也不需要使用get()方法阻塞主线程。
CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello")
                          .thenAccept((s) -> System.out.println(s + " world"));
//输出:Hello world
//回调方法希望使用异步任务的结果,并不需要返回值,那么可以使用thenAccept方法
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
 PoiDTO poi = poiService.loadById(poiId);
 return poi.getMainCategory();
}).thenApply((s) -> isMainPoi(s));   // boolean isMainPoi(int poiId);

future.get();
//希望将异步任务的结果做进一步处理,并需要返回值,则使用thenApply方法。
//如果主线程要获取回调方法的返回,还是要用get()方法阻塞得到

组合两个异步任务


//thenCompose方法中的异步任务依赖调用该方法的异步任务
public CompletableFuture thenCompose(Functionsuper T, ? extends CompletionStage> fn);
//用于两个独立的异步任务都完成的时候
public CompletableFuture thenCombine(CompletionStage other,
                                             BiFunctionsuper
T,? super U,? extends V> fn);


使用示例:



CompletableFuture> poiFuture = CompletableFuture.supplyAsync(
() -> poiService.queryPoiIds(cityId, poiId)
);
//第二个任务是返回CompletableFuture的异步方法
CompletableFuture> getDeal(List poiIds){
 return CompletableFuture.supplyAsync(() ->  poiService.queryPoiIds(poiIds));
}
//thenCompose
CompletableFuture> resultFuture = poiFuture.thenCompose(poiIds -> getDeal(poiIds));
resultFuture.get();

thenCompose和thenApply的功能类似,两者区别在于thenCompose接受一个返回CompletableFuture的Function,当想从回调方法返回的CompletableFuture中直接获取结果U时,就用thenCompose


如果使用thenApply,返回结果resultFuture的类型是CompletableFuture>>,而不是CompletableFuture>


CompletableFuture future = CompletableFuture.supplyAsync(() -> "Hello")
.thenCombine(CompletableFuture.supplyAsync(() -> "world"), (s1, s2) -> s1 + s2);
//future.get()

组合多个CompletableFuture


当需要多个异步任务都完成时,再进行后续处理,可以使用allOf方法


CompletableFuture poiIDTOFuture = CompletableFuture
.supplyAsync(() -> poiService.loadPoi(poiId))
.thenAccept(poi -> {
   model.setModelTitle(poi.getShopName());
   //do more thing
});

CompletableFuture productFuture = CompletableFuture
.supplyAsync(() -> productService.findAllByPoiIdOrderByUpdateTimeDesc(poiId))
.thenAccept(list -> {
   model.setDefaultCount(list.size());
   model.setMoreDesc("more");
});
//future3等更多异步任务,这里就不一一写出来了

CompletableFuture.allOf(poiIDTOFuture, productFuture, future3, ...).join();  //allOf组合所有异步任务,并使用join获取结果

该方法挺适合C端的业务,比如通过poiId异步的从多个服务拿门店信息,然后组装成自己需要的模型,最后所有门店信息都填充完后返回


这里使用了join方法获取结果,它和get方法一样阻塞的等待任务完成


多个异步任务有任意一个完成时就返回结果,可以使用anyOf方法


CompletableFuture future1 = CompletableFuture.supplyAsync(() -> {
   try {
       TimeUnit.SECONDS.sleep(2);
  } catch (InterruptedException e) {
      throw new IllegalStateException(e);
  }
   return "Result of Future 1";
});

CompletableFuture future2 = CompletableFuture.supplyAsync(() -> {
   try {
       TimeUnit.SECONDS.sleep(1);
  } catch (InterruptedException e) {
      throw new IllegalStateException(e);
  }
   return "Result of Future 2";
});

CompletableFuture future3 = CompletableFuture.supplyAsync(() -> {
   try {
       TimeUnit.SECONDS.sleep(3);
  } catch (InterruptedException e) {
      throw new IllegalStateException(e);
     return "Result of Future 3";
});

CompletableFuture anyOfFuture = CompletableFuture.anyOf(future1, future2, future3);

System.out.println(anyOfFuture.get()); // Result of Future 2

异常处理


Integer age = -1;

CompletableFuture maturityFuture = CompletableFuture.supplyAsync(() -> {
 if(age < 0) {
   throw new IllegalArgumentException("Age can not be negative");
}
 if(age > 18) {
   return "Adult";
} else {
   return "Child";
}
}).exceptionally(ex -> {
 System.out.println("Oops! We have an exception - " + ex.getMessage());
 return "Unknown!";
}).thenAccept(s -> System.out.print(s));
//Unkown!

exceptionally方法可以处理异步任务的异常,在出现异常时,给异步任务链一个从错误中恢复的机会,可以在这里记录异常或返回一个默认值


使用handler方法也可以处理异常,并且无论是否发生异常它都会被调用


Integer age = -1;

CompletableFuture maturityFuture = CompletableFuture.supplyAsync(() -> {
   if(age < 0) {
       throw new IllegalArgumentException("Age can not be negative");
  }
   if(age > 18) {
       return "Adult";
  } else {
       return "Child";
  }
}).handle((res, ex) -> {
   if(ex != null) {
       System.out.println("Oops! We have an exception - " + ex.getMessage());
       return "Unknown!";
  }
   return res;
});

分片处理


分片和并行处理:分片借助stream实现,然后通过CompletableFuture实现并行执行,最后做数据聚合(其实也是stream的方法)


CompletableFuture并不提供单独的分片api,但可以借助stream的分片聚合功能实现


举个例子:


//请求商品数量过多时,做分批异步处理
List> skuBaseIdsList = ListUtils.partition(skuIdList, 10);//分片
//并行
List>> futureList = Lists.newArrayList();
for (List skuId : skuBaseIdsList) {
 CompletableFuture> tmpFuture = getSkuSales(skuId);
 futureList.add(tmpFuture);
}
//聚合
futureList.stream().map(CompletalbleFuture::join).collent(Collectors.toList());

举个例子


带大家领略下CompletableFuture异步编程的优势


这里我们用CompletableFuture实现水泡茶程序


首先还是需要先完成分工方案,在下面的程序中,我们分了3个任务:



  • 任务1负责洗水壶、烧开水

  • 任务2负责洗茶壶、洗茶杯和拿茶叶

  • 任务3负责泡茶。其中任务3要等待任务1和任务2都完成后才能开始


图片


下面是代码实现,你先略过runAsync()、supplyAsync()、thenCombine()这些不太熟悉的方法,从大局上看,你会发现:



  1. 无需手工维护线程,没有繁琐的手工维护线程的工作,给任务分配线程的工作也不需要我们关注;

  2. 语义更清晰,例如 f3 = f1.thenCombine(f2, ()->{}) 能够清晰地表述任务3要等待任务1和任务2都完成后才能开始

  3. 代码更简练并且专注于业务逻辑,几乎所有代码都是业务逻辑相关的


//任务1:洗水壶->烧开水
CompletableFuture f1 =
 CompletableFuture.runAsync(()->{
 System.out.println("T1:洗水壶...");
 sleep(1, TimeUnit.SECONDS);

 System.out.println("T1:烧开水...");
 sleep(15, TimeUnit.SECONDS);
});
//任务2:洗茶壶->洗茶杯->拿茶叶
CompletableFuture f2 =
 CompletableFuture.supplyAsync(()->{
 System.out.println("T2:洗茶壶...");
 sleep(1, TimeUnit.SECONDS);

 System.out.println("T2:洗茶杯...");
 sleep(2, TimeUnit.SECONDS);

 System.out.println("T2:拿茶叶...");
 sleep(1, TimeUnit.SECONDS);
 return "龙井";
});
//任务3:任务1和任务2完成后执行:泡茶
CompletableFuture f3 =
 f1.thenCombine(f2, (__, tf)->{
   System.out.println("T1:拿到茶叶:" + tf);
   System.out.println("T1:泡茶...");
   return "上茶:" + tf;
});
//等待任务3执行结果
System.out.println(f3.join());

void sleep(int t, TimeUnit u) {
 try {
   u.sleep(t);
}catch(InterruptedException e){}
}
// 一次执行结果:
T1:洗水壶...
T2:洗茶壶...
T1:烧开水...
T2:洗茶杯...
T2:拿茶叶...
T1:拿到茶叶:龙井
T1:泡茶...
上茶:龙井

注意事项


1.CompletableFuture默认线程池是否满足使用


前面提到创建CompletableFuture异步任务的静态方法runAsync和supplyAsync等,可以指定使用的线程池,不指定则用CompletableFuture的默认线程池


private static final Executor asyncPool = useCommonPool ?
       ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

可以看到,CompletableFuture默认线程池是调用ForkJoinPool的commonPool()方法创建,这个默认线程池的核心线程数量根据CPU核数而定,公式为Runtime.getRuntime().availableProcessors() - 1,以4核双槽CPU为例,核心线程数量就是4*2-1=7


这样的设置满足CPU密集型的应用,但对于业务都是IO密集型的应用来说,是有风险的,当qps较高时,线程数量可能就设的太少了,会导致线上故障


所以可以根据业务情况自定义线程池使用


2.get设置超时时间不能串行get,不然会导致接口延时线程数量*超时时间


作者:程序员清风
来源:juejin.cn/post/7301909438586683433
收起阅读 »

Vue 中使用 Lottie 动画库详解

web
Lottie 是一个由 Airbnb 开源的动画库,它允许你在 Web、iOS、Android 等平台上使用体积小、高性能的体验丰富的矢量动画。本文将详细介绍在 Vue 项目中如何集成和使用 Lottie。 步骤一:安装 Lottie 首先,需要安装 Lott...
继续阅读 »

Lottie 是一个由 Airbnb 开源的动画库,它允许你在 Web、iOS、Android 等平台上使用体积小、高性能的体验丰富的矢量动画。本文将详细介绍在 Vue 项目中如何集成和使用 Lottie。


步骤一:安装 Lottie


首先,需要安装 Lottie 包。在 Vue 项目中,可以使用 npm 或 yarn 进行安装:


npm install lottie-web
# 或
yarn add lottie-web

步骤二:引入 Lottie


在需要使用 Lottie 的组件中引入 Lottie 包:


// HelloWorld.vue

<template>
<div>
<lottie
:options="lottieOptions"
:width="400"
:height="400"
/>

</div>

</template>

<script>
import Lottie from 'lottie-web';
import animationData from './path/to/your/animation.json';

export default {
data() {
return {
lottieOptions: {
loop: true,
autoplay: true,
animationData: animationData,
},
};
},
mounted() {
this.$nextTick(() => {
// 初始化 Lottie 动画
const lottieInstance = Lottie.loadAnimation(this.lottieOptions);
});
},
};
</script>


<style>
/* 可以添加样式以调整动画的位置和大小 */
</style>


在上述代码中,animationData 是你的动画 JSON 数据,可以使用 Bodymovin 插件将 After Effects 动画导出为 JSON。


步骤三:调整参数和样式


lottieOptions 中,你可以设置各种参数来控制动画的行为,比如是否循环、是否自动播放等。同时,你可以通过样式表中的 CSS 来调整动画的位置和大小,以适应你的页面布局。


/* HelloWorld.vue */

<style>
.lottie {
margin: 20px auto; /* 调整动画的位置 */
}
</style>

四 Lottie 的主要配置参数


Lottie 提供了一系列配置参数,以便你能够定制化和控制动画的行为。以下是 Lottie 的主要配置参数以及它们的使用方法:


1. container


container 参数用于指定动画将被插入到页面中的容器元素。可以是 DOM 元素,也可以是一个用于选择元素的 CSS 选择器字符串。


示例:


// 使用 DOM 元素作为容器
const container = document.getElementById('animation-container');

// 或者使用 CSS 选择器字符串
const container = '#animation-container';

// 初始化 Lottie 动画
const animation = lottie.loadAnimation({
container: container,
/* 其他配置参数... */
});

2. renderer


renderer 参数用于指定渲染器的类型,常用的有 "svg" 和 "canvas"。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
renderer: 'svg', // 或 'canvas'
/* 其他配置参数... */
});

3. loop


loop 参数用于指定动画是否循环播放。设置为 true 时,动画将一直循环播放。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
loop: true,
/* 其他配置参数... */
});

4. autoplay


autoplay 参数用于指定是否在加载完成后自动播放动画。设置为 true 时,动画将在加载完成后立即开始播放。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
autoplay: true,
/* 其他配置参数... */
});

5. path


path 参数用于指定动画 JSON 文件的路径或 URL。可以是相对路径或绝对路径。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
path: 'path/to/animation.json',
/* 其他配置参数... */
});

6. rendererSettings


rendererSettings 参数用于包含特定渲染器的设置选项。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
rendererSettings: {
clearCanvas: true, // 在每一帧上清除画布
},
/* 其他配置参数... */
});

7. animationData


animationData 参数允许你直接将动画数据作为 JavaScript 对象传递给 Lottie。可以用于直接内嵌动画数据而不是从文件加载。


示例:


const animationData = {
/* 动画数据的具体内容 */
};

const animation = lottie.loadAnimation({
container: '#animation-container',
animationData: animationData,
/* 其他配置参数... */
});

8. name


name 参数用于为动画指定一个名称。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
name: 'myAnimation',
/* 其他配置参数... */
});

9. speed


speed 参数用于控制动画的播放速度。1 表示正常速度,0.5 表示一半速度,2 表示两倍速度。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
speed: 1.5, // 播放速度为原来的1.5倍
/* 其他配置参数... */
});

10. 事件回调


Lottie 还支持通过事件回调来执行一些自定义操作,如 onCompleteonLoopCompleteonEnterFrame 等。


示例:


const animation = lottie.loadAnimation({
container: '#animation-container',
loop: true,
onComplete: () => {
console.log('动画完成!');
},
/* 其他配置参数... */
});

通过灵活使用这些参数,你可以定制化你的动画,使其更好地满足项目的需求。


步骤五:运行项目


最后,确保你的 Vue 项目是运行在支持 Lottie 的环境中。启动项目,并在浏览器中查看效果:


npm run serve
# 或
yarn serve

访问 http://localhost:8080(具体端口可能会有所不同),你应该能够看到嵌入的 Lottie 动画正常播放。


结论


通过这些步骤,我们为 Vue 项目增添了一种引人注目的交互方式,提升了用户体验。Lottie 的强大功能和易用性使得在项目中集成动画变得轻而易举。希望本文对你在 Vue 项目中使用 Lottie 有所帮助。在应用中巧妙地使用动画,让用户感受到更加愉悦的交互体验。


作者:_XU
来源:juejin.cn/post/7301976895913623589
收起阅读 »

惊讶,Vite 原来也可以跑在浏览器

web
为大家介绍一个 vite 的一个终端插件,使之可以运行在浏览器中。它就是# vite-plugin-terminal。 Git 地址:github.com/patak-dev/v… vite-plugin-terminal 这个插件使用起来很简单,首先安装: ...
继续阅读 »


为大家介绍一个 vite 的一个终端插件,使之可以运行在浏览器中。它就是# vite-plugin-terminal


Git 地址:github.com/patak-dev/v…


vite-plugin-terminal


这个插件使用起来很简单,首先安装:


npm i -D vite-plugin-terminal

然后将插件添加到您的 vite.config.ts 配置中:


// vite.config.ts
import Terminal from 'vite-plugin-terminal'

export default {
plugins: [
Terminal()
]
}

最后,你可以在源代码中像使用 console.log 一样使用它。


import terminal from 'virtual:terminal';
import './module.js';

terminal.log('Hey terminal! A message from the browser');

const json = { foo: 'bar' };

terminal.log({ json });

terminal.assert(true, 'Assertion pass');
terminal.assert(false, 'Assertion fails');

terminal.info('Some info from the app');

terminal.table(['vite', 'plugin', 'terminal']);

看看效果。



体验地址:stackblitz.com/edit/github…


将日志导入终端


如果您希望标准 console 日志出现在终端中,您可以使用以下 console: 'terminal' 选项 vite.config.ts:


// vite.config.ts
import Terminal from 'vite-plugin-terminal'

export default {
plugins: [
Terminal({
console: 'terminal'
})
]
}

在这种情况下,就不需要导入虚拟终端来使用该插件。


console.log('Hey terminal! A message from the browser')

如果想要更多控制,也可以手动在脑海中覆盖它。


  <script type="module">
// Redirect console logs to the terminal
import terminal from 'virtual:terminal'
globalThis.console = terminal
</script>

双端控制台


如果希望同时控制登录终端和控制台,可以使用 output 选项来定义 terminal 应记录日志的位置。接受 terminal、console 或同时包含两者的数组。


// vite.config.ts
import Terminal from 'vite-plugin-terminal'

export default {
plugins: [
Terminal({
output: ['terminal', 'console']
})
]
}


其他


这个插件方法非常多,基本和 console 一样。


terminal.log(obj1 [, obj2, ..., objN])
terminal.info(obj1 [, obj2, ..., objN])
terminal.warn(obj1 [, obj2, ..., objN])
terminal.error(obj1 [, obj2, ..., objN])
terminal.assert(assertion, obj1 [, obj2, ..., objN])
terminal.group()
terminal.groupCollapsed()
terminal.groupEnd()
terminal.table(obj)
terminal.time(id)
terminal.timeLog(id, obj1 [, obj2, ..., objN])
terminal.timeEnd(id)
terminal.clear()
terminal.count(label)
terminal.countReset(label)
terminal.dir(obj)
terminal.dirxml(obj)

也可以定制一些配置。
例如上面介绍到的 console,设置为 'terminal' 使其 globalThis.console 等于terminal 应用程序中的对象。设置 output,定义日志的输出位置。设置 strip,terminal.*()生产时捆扎时剥去。还可以设置 includeexclude 用来指定插件在删除生产调用时应在构建中操作的文件和指定插件在删除生产调用时应忽略的构建中的文件。


小结


# vite-plugin-terminal 换种方式颠覆了现在大多人本地开发的模式,如果用来快速做演示 demo,是一个非常不错的选择。但是当前这个插件还是存在不少的问题,不过真的要用在大型商业项目里面时候,就要考虑跟 Devops系统的集成,希望# vite-plugin-terminal完全成熟开源后,能给开发者带来更多的便利。


参考



作者:拜小白
来源:juejin.cn/post/7301909438540333067
收起阅读 »

结构思考力-透过结构看思考

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈 目标:思考清晰,表达有力,解决问题 0. 理念:透过结构看世界 结构化思考力的核心理念是应用结构化思维底层逻辑进行思考、表达和解决问题。  ...
继续阅读 »

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈



目标:思考清晰,表达有力,解决问题


0. 理念:透过结构看世界


结构化思考力的核心理念是应用结构化思维底层逻辑进行思考、表达和解决问题


  高效管理者应当具备三种基本技能:技术性技能、人际性技能和概念性技能。概念性技能是指面对复杂情况进行抽象和概念化的技能。


  结构思考力是一种“先总后分”的思考和表达方式,强调先框架后细节,先总结后具体,先结论后原因,先重要后次要


0.1 透过结构看世界,洞悉事物本质


结构是万物之本


  内行和外行的差别在于,是否具备这个行业的思维结构。


  结构存在于每个整体和局部的无穷变化中,每个局部变现整体,而每个局部的意义又由整体来决定。


结构也是思维的根本


  由欲望产生需求,由需求产生动机,由动机产生行为等,这些构成了心理活动的结构。


  从物质基础看,记忆和思维的发生基于的是人类大脑物质层次的“实体结构”;从思维效果看,思考和表达的效率不同,则基于的是人类思考的“逻辑结构”。


    


0.2 三层次模型:结构思考力的核心理念


案例:如何把200ml的水装入100ml的杯子里?



image.png


解决问题的三个步骤,对应到结构思考力的三层次模型




  • 第一步,明确自己遇到了什么问题。

  • 第二步,用科学的方法重新梳理思路,自己想清楚、想全面,从而针对问题做出有效的决策。

  • 第三步,保证解决方案可以顺利实施。


总结:




  • 理解:隐性思维显性化。觉察现有的思维,并且判断它是否清晰。




  • 重构:显性思维结构化




  • 呈现:结构思维形象化




0.3 金字塔结构:结构思考力的训练工具


麦肯锡咨询公司:以事实为基础,以假设为导向,严格的结构化


巴巴拉·明托的金字塔原理是一项层次性、结构化的思考、沟通技术,可以用于结构化的说话与写作过程


有一些经典案例,请看视频。


1. 理解:隐性思维显性化


看似有一个统一的标准,但具体又说不清楚这个标准究竟是什么,是不能量化的,换句话说,只能意会不能言传。这是隐性的感受。


理解是结构思考力的基础,本质是拆分信息找结构


image.png


结构化接收信息的三个步骤:识别、判断和概括:




  • 第一步:识别信息中的事实和观点,确定理由和结论。找出哪些是观点类信息、哪些是事实类信息,观点类信息中哪些是结论、哪些是支撑结论的理由,并将它们区分。




  • 第二步:判断结论和理由的对应关系,并依据这些对应关系画出金字塔结构图
    要判断对方结论的合理性可以从两点出发:一是看对方提供的事实与数据是否真实。二是这些事实与数据是否可以得出相应的理由,相应的理由是否可以得出最终的结论。




  • 第三步:一句话概括所有内容
    在“序言”的基础上,从“ ”,“ ”,“ ”N个方面(一级目录),说明了“结论”。




1.2 案例


1.2.1 中国书法


中国书法是一门古老的艺术,它伴随着中华文明的发展而发展。世界上,拥有书法艺术的民族屈指可数。书法作为一种艺术创作,具有很深的玄妙。中国书法,具有悠久的历史,从甲骨文、金文演变而为大篆、小篆、隶书,到东汉、魏、晋时期,草书、楷书、行书、诸体基本定型,书法时刻散发着古老艺术的魅力。为一代又一代人们所喜爱。书法,是在洁白的纸上,靠毛笔运动的灵活多变和水墨的丰富性,留下斑斑迹相,在纸面上形成有意味的黑白构成,所以书法是构成艺术;书家的笔是他手指的延伸,笔的疾厉、徐缓飞动、顿挫,都受观的驱使成为他情感、情绪的发泄,所以,书法也是一种表现性的艺术;书法能够通过作品把书家个人的生活感受、学识、修养、个性等悄悄地折射出来,所以,通常有"字如其人”、"书为心画“的说法;书法还可以用于题辞、书写牌匾,因此,也是一种实用性的艺术


结构化:


一句话概括:

介绍世界上拥有书法艺术的民族屈指可数的基础上,从书法具有悠久的历史丰富艺术表现形式两个方面,说明了书法是一种古老而玄妙的艺术创作


2. 重构:显性思维结构化


2.1 结构思考力四个核心原则


案例:紧急事件的沟通


小李:李总,您好!我是小赵,有件事情非常紧急,今早七点我接到郑州交通管理局的电话,六点十分在郑州203国道上发生重大交通事故,我公司销售部的小马驾车与一辆大货车相撞,小马当场死亡,对方司机重伤,目前正在医院抢救,与小马同车的还有公司的销售员人员张三李四和王五、三人都不同程度受伤,但无生命危险。目前事故责任还不能确定,我准备立刻前往郑州处理相关事务,希望跟您商量一下应对措施。


李总:立即向主管总裁汇报;然后联系相关医院确保伤病员的全力敷治;再联系保险公司,协商理赔事宜;还有,联系伤亡员工家属;别忘了跟郑州交警部门确定事故责任,一定要全力维护公司利益跟销售部门说,让他们确保货物安全,做好工作交接,处理好与客户的关系,请他们理解;总之就是按照公司应急预案立即成立事故处理小组处理上述事宜。对了,别忘了做好伤亡员工家属前往郑州的准备。


如果李总这么表达,小李听的一脸懵逼~~


按照对内和对外分,为:



根据公司应急预案组成事故处理小组处理事故。


第一,跟总裁汇报情况并联系销售部做好善后处理;
第二,与医院、家属、交警和保险公司等多方协调维护员工和公司利益。



按照人员,事故,业务划分:



跟总裁汇报,根据预案成立事故小组处理事故。
第一,确保伤病员的全力救治并好家属安排;
第二,与各部门多方协调维护员工和公司利益;
第三,销售部做好货物和供应商的善后处理。



我们根据 对内和对外 画结构图,如下:


1239.jpg

重构“四核”:结构思考力四个核心原则


image.png

2.2 论:结论先行,一句话100%传达传达你的意思


案例:政府工作报告


image.png

案例:媒体的结论先行


image.png

案例:咨询公司的结构


image.png

案例:工作总结


修改前:

image.png


修改后:

image.png


2.3 证:以上统下,让你的观点经得住挑战质疑


image.png

  • 有结论

  • 有理由

  • 结论和理由相联系


2.4 类:归类分组,让你的表达清晰全面且容易记


案例:好友印象


image.png

分类:


image.png

结构化:


image.png

划分标准


**MECE原则**(Mutually Exclusive Collectively Exhaustive):中文意思是“相互独立,完全穷尽”,即对于一个重大的议题,能够做到不重叠,不遗漏的分类,而且能够借此有效把握问题的核心,并解决问题的方法。


“相互独立”意味着问题的细分是在同一纬度上并有明确区分、不可重叠的,“完全穷尽”则意味着全面、周密。



  • 例子:


  • image.png


2.5 比:逻辑递进,让你的观点逻辑严谨且有说服力


递进排列有三种顺序:




  • 时间顺序:当想要达成某个结果时,这个结果的达成必然有一系列行动或步骤来支撑,而这些行动或步骤就是按照时间顺序排列的一些要素。这些要素是对该组行动或步骤的概括,也是该组行动或步骤达成的结果。


  时间顺序适用于项目进展、阶段汇报。


 



  • 结构顺序:是指将一个整体划分为不同的部分,这个整体既可以是事物也可以是概念,或者从外到内、从上到下、从整体到局部来加以介绍。




  • 重要性顺序:是指具有某些共同特点和内容,按照重要程度进行排序。



3. 呈现: 结构思维形象化


3.1 结构罗盘,一张图说清所有工作内容


案例:隆中对,诸葛亮分析、汇报战略的结构


image.png

案例:一张图说清企业战略的前提是‘结构’


image.png

图表指南工具-结构罗盘


image.png


案例:停 缺 得


image.png

案例:整合-字母


image.png

3.2 配关系,四大模式十六种结构




  1. 形象表达是视觉化呈现结构的最佳方法。




  2. 结构罗盘:一站式形象表达的解决方案。




结构罗盘从内到外主要分成三个部分:“配”关系、“得”示图、“上” 包装




  • 流动模式:线性、流程、循环、关联

  • 作用模式:对立、合力、平衡、阻碍

  • 关系模式:并列、重叠、包含、分割

  • 比较模式:成分、排序、序列、关联


案例


image.png
image.png

基于信息定结构练习


image.png

3.3 得图示,关系匹配类图


3.3.1 好图胜千言


image.png

3.3.2 流动模式:整理流程的流动模式


image.png

线性和流程关系


image.png

关联和循坏


image.png

对应的图示


image.png

3.3.3 作用模式:动态变化的作用模式


分别是:阻碍,平衡,合力,对立


对立和合力


image.png

平衡和阻碍


image.png

图示


image.png

3.3.4 关系:要点清晰的联系模式


并列、重叠、包含、分割


并列和重叠


image.png

包含和分割


image.png

图示


image.png

比较模式:数据说话的比较模式


成分、排序、序列、关联


成分和排序


image.png

序列和关联


image.png

图示


image.png

3.4 上包装,让观点更吸引人更容易记


将已经搭建好的金字塔结构的一级目录通过简化、类比、整合和引用的方式进行包装,让对方更容易记忆并接受你的观点:
image.png


案例:简化


image.png

类比


类比-形象


image.png


类比-行为


image.png
image.png

类比-形象


image.png

整合


image.png
image.png
image.png

引用


image.png
image.png

4. 总结


image.png


作者:hsfxuebao
来源:juejin.cn/post/7301901927858880538
收起阅读 »

🔥🔥🔥“异步”是好还是坏?怎么灵活使用?看这边!🔥🔥🔥

web
前言     今天我们来聊一聊JS中的代码“异步”问题。先让我们简单看一看下面的代码: function a() { setTimeout(() => { console.log('写文章'); }, 1000) } f...
继续阅读 »

前言


    今天我们来聊一聊JS中的代码“异步”问题。先让我们简单看一看下面的代码:


function a() {
setTimeout(() => {
console.log('写文章');
}, 1000)
}

function b() {
setTimeout(() => {
console.log('发布');
}, 0)
}

a()
b()

    我们分别设置了ab两个函数;再分别设置一个计时器a函数设定为1秒,b函数设定为0秒;最后分别调用ab两函数。我们都知道JavaScript是从上往下单线程执行的,很明显此处我们想要的效果肯定是先“写文章”,1秒后再“发布”,让我们看看效果:


image.png


    很可惜事与愿违,我们连“写文章”都还没写呢就已经“发布”了。


    那么为什么会这样呢?当代码读取到调用a函数时,确实是是先执行了a函数,但同时浏览器引擎也不会傻傻等待a函数执行完再进行下一步,它会同时也执行b函数。而根据我们的设定,b函数的计算器设定为0秒并不需要等待,所以我们先得到的就是“发布”,而不是预期的“写文章”。这就是“异步”。


正文


异步问题


    在JavaScript中,异步编程是一种处理非阻塞操作的方式,使得代码可以在等待某些操作完成的同时继续执行其他任务。JavaScript是从上往下单线程执行的,但通过异步编程,可以实现在等待一些I/O操作、网络请求或定时器等时不阻塞整个程序的执行。同一时间干多步事情,让JS执行效率更高——这就是异步的优点。但异步有好处也有坏处,举个“栗子”:当b需要拿到a给出的结果才能执行的时候,异步会让还未拿到a结果的b也执行,这就会出问题,也就叫异步问题。就像我们前言中展示的那样,那碰到这种问题该怎么解决呢?


回调(Callback)


    有一种老的解决办法就是回调:把b的执行扔进a,等a执行完自然就轮到了b。让我们简单试一试:


function a() {
setTimeout(() => {
console.log('写文章');
b()
}, 1000)
}

function b() {
setTimeout(() => {
console.log('发布');
}, 0)
}

a()

再看看执行结果:


image.png


    我们在等待了一秒之后完成了“写文章”随后立即“发布”了。这样确实看起来解决了异步问题,但这也同时会带来新的问题。在回调中我们有一种情况叫做“回调地狱”:当回调的数量多起来的时候,执行的链就会非常长,类似于物理中的串联,有一个元件出了问题,整段代码就会崩掉,并且代码维护起来也会非常麻烦,得从头到尾查找问题。以下是一个简单的“回调地狱”例子,使用了多个嵌套的回调函数,模拟了异步操作的情况:


// 模拟异步操作1
function asyncOperation1(callback) {
setTimeout(function() {
console.log("Async Operation 1 completed");
callback();
}, 1000);
}

// 模拟异步操作2
function asyncOperation2(callback) {
setTimeout(function() {
console.log("Async Operation 2 completed");
callback();
}, 1000);
}

// 模拟异步操作3
function asyncOperation3(callback) {
setTimeout(function() {
console.log("Async Operation 3 completed");
callback();
}, 1000);
}

// 嵌套回调地狱
asyncOperation1(function() {
asyncOperation2(function() {
asyncOperation3(function() {
console.log("All async operations completed");
});
});
});

    在上述例子中,asyncOperation1asyncOperation2asyncOperation3 分别代表三个异步操作。它们的回调函数嵌套在彼此之内,形成了回调地狱。当异步操作数量增加时,这种嵌套结构会变得难以理解和维护。因此,使用Promise或更先进的异步处理方式通常更为推荐。这有助于避免回调地狱,提高代码的可读性和可维护性。


Promise


    在JavaScript中,Promise是一种用于处理异步操作的对象,它提供了更优雅的方式来组织和处理异步代码。Promise可以通过.then()链式调用,使得多个异步操作可以依次执行,而不是嵌套在回调中,使得异步代码更易于理解维护,避免了回调地狱(Callback Hell)。还可以通过.then()方法处理Promise成功状态,通过.catch()方法处理Promise失败状态。这种分离成功和失败的处理方式更加清晰。


    下面是一个简单的Promise示例:


// 创建一个Promise对象
let myPromise = new Promise(function(resolve, reject) {
// 异步操作
setTimeout(function() {
let success = true;

if (success) {
resolve("Promise resolved!");
} else {
reject("Promise rejected!");
}
}, 1000);
});

// 处理Promise成功状态
myPromise.then(function(result) {
console.log(result);
})
// 处理Promise失败状态
.catch(function(error) {
console.error(error);
});

    在这个例子中,myPromise表示一个异步操作,通过resolvereject函数表示成功和失败。.then()方法用于处理成功状态,.catch()方法用于处理失败状态。Promise的引入使得异步代码更为结构化,便于阅读维护


结语


    这次文章我们简单介绍了JavaScript中的“异步”、“回调”以及“Promise对象”。当然Promise身为一个对象肯定远不止这么几个方法!JavaScript的世界是那么的广阔,如果关于JS的内容对你有帮助的话,希望能给博主一个免费的小心心♡呀~


作者:Mio_02
来源:juejin.cn/post/7301914624140034083
收起阅读 »

面试官问,如何在十亿级别用户中检查用户名是否存在?

前言 不知道大家有没有留意过,在使用一些app注册的时候,提示你用户名已经被占用了,需要更换一个,这是如何实现的呢?你可能想这不是很简单吗,去数据库里查一下有没有不就行了吗,那么假如用户数量很多,达到数亿级别呢,这又该如何是好? 数据库方案 第一种方案就是查...
继续阅读 »

前言


不知道大家有没有留意过,在使用一些app注册的时候,提示你用户名已经被占用了,需要更换一个,这是如何实现的呢?你可能想这不是很简单吗,去数据库里查一下有没有不就行了吗,那么假如用户数量很多,达到数亿级别呢,这又该如何是好?


数据库方案



第一种方案就是查数据库的方案,大家都能够想到,代码如下:


public class UsernameUniquenessChecker {
private static final String DB_URL = "jdbc:mysql://localhost:3306/your_database";
private static final String DB_USER = "your_username";
private static final String DB_PASSWORD = "your_password";

public static boolean isUsernameUnique(String username) {
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD)) {
String sql = "SELECT COUNT(*) FROM users WHERE username = ?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, username);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
int count = rs.getInt(1);
return count == 0; // If count is 0, username is unique
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return false; // In case of an error, consider the username as non-unique
}

public static void main(String[] args) {
String desiredUsername = "new_user";
boolean isUnique = isUsernameUnique(desiredUsername);
if (isUnique) {
System.out.println("Username '" + desiredUsername + "' is unique. Proceed with registration.");
} else {
System.out.println("Username '" + desiredUsername + "' is already in use. Choose a different one.");
}
}
}

这种方法会带来如下问题:



  1. 性能问题,延迟高 如果数据量很大,查询速度慢。另外,数据库查询涉及应用程序服务器和数据库服务器之间的网络通信。建立连接、发送查询和接收响应所需的时间也会导致延迟。

  2. 数据库负载过高。频繁执行 SELECT 查询来检查用户名唯一性,每个查询需要数据库资源,包括CPU和I/O。



  1. 可扩展性差。数据库对并发连接和资源有限制。如果注册率继续增长,数据库服务器可能难以处理数量增加的传入请求。垂直扩展数据库(向单个服务器添加更多资源)可能成本高昂并且可能有限制。


缓存方案


为了解决数据库调用用户名唯一性检查的性能问题,引入了高效的Redis缓存。



public class UsernameCache {

private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private static final int CACHE_EXPIRATION_SECONDS = 3600;

private static JedisPool jedisPool;

// Initialize the Redis connection pool
static {
JedisPoolConfig poolConfig = new JedisPoolConfig();
jedisPool = new JedisPool(poolConfig, REDIS_HOST, REDIS_PORT);
}

// Method to check if a username is unique using the Redis cache
public static boolean isUsernameUnique(String username) {
try (Jedis jedis = jedisPool.getResource()) {
// Check if the username exists in the Redis cache
if (jedis.sismember("usernames", username)) {
return false; // Username is not unique
}
} catch (Exception e) {
e.printStackTrace();
// Handle exceptions or fallback to database query if Redis is unavailable
}
return true; // Username is unique (not found in cache)
}

// Method to add a username to the Redis cache
public static void addToCache(String username) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.sadd("usernames", username); // Add the username to the cache set
jedis.expire("usernames", CACHE_EXPIRATION_SECONDS); // Set expiration time for the cache
} catch (Exception e) {
e.printStackTrace();
// Handle exceptions if Redis cache update fails
}
}

// Cleanup and close the Redis connection pool
public static void close() {
jedisPool.close();
}
}

这个方案最大的问题就是内存占用过大,假如每个用户名需要大约 20 字节的内存。你想要存储10亿个用户名的话,就需要20G的内存。


总内存 = 每条记录的内存使用量 * 记录数 = 20 字节/记录 * 1,000,000,000 条记录 = 20,000,000,000 字节 = 20,000,000 KB = 20,000 MB = 20 GB


布隆过滤器方案


直接缓存判断内存占用过大,有没有什么更好的办法呢?布隆过滤器就是很好的一个选择。


那究竟什么布隆过滤器呢?


布隆过滤器Bloom Filter)是一种数据结构,用于快速检查一个元素是否存在于一个大型数据集中,通常用于在某些情况下快速过滤掉不可能存在的元素,以减少后续更昂贵的查询操作。布隆过滤器的主要优点是它可以提供快速的查找和插入操作,并且在内存占用方面非常高效。


具体的实现原理和数据结构如下图所示:



布隆过滤器的核心思想是使用一个位数组(bit array)和一组哈希函数。



  • 位数组(Bit Array) :布隆过滤器使用一个包含大量位的数组,通常初始化为全0。每个位可以存储两个值,通常是0或1。这些位被用来表示元素的存在或可能的存在。

  • 哈希函数(Hash Functions) :布隆过滤器使用多个哈希函数,每个哈希函数可以将输入元素映射到位数组的一个或多个位置。这些哈希函数必须是独立且具有均匀分布特性。


那么具体是怎么做的呢?



  • 添加元素:如上图所示,当将字符串“xuyang”,“alvin”插入布隆过滤器时,通过多个哈希函数将元素映射到位数组的多个位置,然后将这些位置的位设置为1。

  • 查询元素:当要检查一个元素是否存在于布隆过滤器中时,通过相同的哈希函数将元素映射到位数组的相应位置,然后检查这些位置的位是否都为1。如果有任何一个位为0,那么可以确定元素不存在于数据集中。但如果所有位都是1,元素可能存在于数据集中,但也可能是误判。


本身redis支持布隆过滤器的数据结构,我们用代码简单实现了解一下:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class BloomFilterExample {
public static void main(String[] args) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, "localhost", 6379);

try (Jedis jedis = jedisPool.getResource()) {
// 创建一个名为 "usernameFilter" 的布隆过滤器,需要指定预计的元素数量和期望的误差率
jedis.bfCreate("usernameFilter", 10000000, 0.01);

// 将用户名添加到布隆过滤器
jedis.bfAdd("usernameFilter", "alvin");

// 检查用户名是否已经存在
boolean exists = jedis.bfExists("usernameFilter", "alvin");
System.out.println("Username exists: " + exists);
}
}
}

在上述示例中,我们首先创建一个名为 "usernameFilter" 的布隆过滤器,然后使用 bfAdd 将用户名添加到布隆过滤器中。最后,使用 bfExists 检查用户名是否已经存在。


优点:



  • 节约内存空间,相比使用哈希表等数据结构,布隆过滤器通常需要更少的内存空间,因为它不存储实际元素,而只存储元素的哈希值。如果以 0.001 误差率存储 10 亿条记录,只需要 1.67 GB 内存,对比原来的20G,大大的减少了。

  • 高效的查找, 布隆过滤器可以在常数时间内(O(1))快速查找一个元素是否存在于集合中,无需遍历整个集合。


缺点:



  • 误判率存在:布隆过滤器在判断元素是否存在时,有一定的误判率。这意味着在某些情况下,它可能会错误地报告元素存在,但不会错误地报告元素不存在。

  • 不能删除元素:布隆过滤器通常不支持从集合中删除元素,因为删除一个元素会影响其他元素的哈希值,增加了误判率。


总结


Redis 布隆过滤器的方案为大数据量下唯一性验证提供了一种基于内存的高效解决方案,它需要在内存消耗和错误率之间取得一个平衡点。当然布隆过滤器还有更多应用场景,比如防止缓存穿透、防止恶意访问等。


作者:JAVA旭阳
来源:juejin.cn/post/7293786247655129129
收起阅读 »

初学后端,如何做好表结构设计?

前言 最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计? 大家关心的问题阳哥必须整理出来,希望对大家有帮助。 先说结论 这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从...
继续阅读 »

前言


最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计?


大家关心的问题阳哥必须整理出来,希望对大家有帮助。


先说结论


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:



  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)

  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度。


4个方面


设计数据库表结构需要考虑到以下4个方面:




  1. 数据库范式:通常情况下,我们希望表的数据符合某种范式,这可以保证数据的完整性和一致性。例如,第一范式要求表的每个属性都是原子性的,第二范式要求每个非主键属性完全依赖于主键,第三范式要求每个非主键属性不依赖于其他非主键属性。




  2. 实体关系模型(ER模型):我们需要先根据实际情况画出实体关系模型,然后再将其转化为数据库表结构。实体关系模型通常包括实体、属性、关系等要素,我们需要将它们转化为表的形式。




  3. 数据库性能:我们需要考虑到数据库的性能问题,包括表的大小、索引的使用、查询语句的优化等。




  4. 数据库安全:我们需要考虑到数据库的安全问题,包括表的权限、用户角色的设置等。




设计原则


在设计数据库表结构时,可以参考以下几个优雅的设计原则:




  1. 简单明了:表结构应该简单明了,避免过度复杂化。




  2. 一致性:表结构应该保持一致性,例如命名规范、数据类型等。




  3. 规范化:尽可能将表规范化,避免数据冗余和不一致性。




  4. 性能:表结构应该考虑到性能问题,例如使用适当的索引、避免全表扫描等。




  5. 安全:表结构应该考虑到安全问题,例如合理设置权限、避免SQL注入等。




  6. 扩展性:表结构应该具有一定的扩展性,例如预留字段、可扩展的关系等。




最后,需要提醒的是,优雅的数据库表结构需要在实践中不断迭代和优化,不断满足实际需求和新的挑战。



下面举个示例让大家更好的理解如何设计表结构,如何引入内存,有哪些优化思路:



问题描述



如上图所示,红框中的视频筛选标签,应该怎么设计数据库表结构?除了前台筛选,还想支持在管理后台灵活配置这些筛选标签。


这是一个很好的应用场景,大家可以先自己想一下。不要着急看我的方案。


需求分析



  1. 可以根据红框的标签筛选视频

  2. 其中综合标签比较特殊,和类型、地区、年份、演员等不一样



  • 综合是根据业务逻辑取值,并不需要入库

  • 类型、地区、年份、演员等需要入库



  1. 设计表结构时要考虑到:



  • 方便获取标签信息,方便把标签信息缓存处理

  • 方便根据标签筛选视频,方便我们写后续的业务逻辑


设计思路



  1. 综合标签可以写到配置文件中(或者写在前端),这些信息不需要灵活配置,所以不需要保存到数据库中

  2. 类型、地区、年份、演员都设计单独的表

  3. 视频表中设计标签表的外键,方便视频列表筛选取值

  4. 标签信息写入缓存,提高接口响应速度

  5. 类型、地区、年份、演员表也要支持对数据排序,方便后期管理维护


表结构设计


视频表


字段注释
id视频主键id
type_id类型表外键id
area_id地区表外键id
year_id年份外键id
actor_id演员外键id

其他和视频直接相关的字段(比如名称)我就省略不写了


类型表


字段注释
id类型主键id
name类型名称
sort排序字段

地区表


字段注释
id类型主键id
name类型名称
sort排序字段

年份表


字段注释
id类型主键id
name类型名称
sort排序字段

原以为年份字段不需要排序,要么是年份正序排列,要么是年份倒序排列,所以不需要sort字段。


仔细看了看需求,还有“10年代”还是需要灵活配置的呀~


演员表


字段注释
id类型主键id
name类型名称
sort排序字段

表结构设计完了,别忘了缓存


缓存策略


首先这些不会频繁更新的筛选条件建议使用缓存:




  1. 比较常用的就是redis缓存

  2. 再进阶一点,如果你使用docker,可以把这些配置信息写入docker容器所在物理机的内存中,而不用请求其他节点的redis,进一步降低网络传输带来的耗时损耗

  3. 筛选条件这类配置信息,客户端和服务端可以约定一个更新缓存的机制,客户端直接缓存配置信息,进一步提高性能


列表数据自动缓存


目前很多框架都是支持自动缓存处理的,比如goframe和go-zero


goframe


可以使用ORM链式操作-查询缓存


示例代码:


package main

import (
"time"

"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)

func main() {
var (
db = g.DB()
ctx = gctx.New()
)

// 开启调试模式,以便于记录所有执行的SQL
db.SetDebug(true)

// 写入测试数据
_, err := g.Model("user").Ctx(ctx).Data(g.Map{
"name": "xxx",
"site": "https://xxx.org",
}).Insert()

// 执行2次查询并将查询结果缓存1小时,并可执行缓存名称(可选)
for i := 0; i < 2; i++ {
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}

// 执行更新操作,并清理指定名称的查询缓存
_, err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: -1,
Name: "vip-user",
Force: false,
}).Data(gdb.Map{"name": "smith"}).Where("uid", 1).Update()
if err != nil {
g.Log().Fatal(ctx, err)
}

// 再次执行查询,启用查询缓存特性
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}

go-zero


DB缓存机制


go-zero缓存设计之持久层缓存


官方都做了详细的介绍,不作为本文的重点。


讨论


我的方案也在我的技术交流群里引起了大家的讨论,也和大家分享一下:


Q1 冗余设计和一致性问题



提问: 一个表里做了这么多外键,如果我要查各自的名称,势必要关联4张表,对于这种存在多外键关联的这种表,要不要做冗余呢(直接在主表里冗余各自的名称字段)?要是保证一致性的话,就势必会影响性能,如果做冗余的话,又无法保证一致性



回答:


你看文章的上下文应该知道,文章想解决的是视频列表筛选问题。


你提到的这个场景是在视频详情信息中,如果要展示这些外键的名称怎么设计更好。


我的建议是这样的:



  1. 根据需求可以做适当冗余,比如你的主表信息量不大,配置信息修改后同步修改冗余字段的成本并不高。

  2. 或者像我文章中写的不做冗余设计,但是会把外键信息缓存,业务查询从缓存中取值。

  3. 或者将视频详情的查询结果整体进行缓存


还是看具体需求,如果这些筛选信息不变化或者不需要手工管理,甚至不需要设计表,直接写死在代码的配置文件中也可以。进一步降低DB压力,提高性能。


Q2 why设计外键?



提问:为什么要设计外键关联?直接写到视频表中不就行了?这么设计的意义在哪里?



回答:



  1. 关键问题是想解决管理后台灵活配置

  2. 如果没有这个需求,我们可以直接把筛选条件以配置文件的方式写死在程序中,降低复杂度。

  3. 站在我的角度:这个功能的筛选条件变化并不会很大,所以很懂你的意思。也建议像我2.中的方案去做,去和产品经理拉扯喽~


总结


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:



  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)

  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度



本文抛砖引玉,欢迎大家留言交流。



一起学习


欢迎和我一起讨论交流:可以在掘金私信我


也欢迎关注我的公众号: 程序员升职加薪之旅


也欢迎大家关注我的掘金,点赞、留言、转发。你的支持,是我更文的最大动力!



作者:王中阳Go
来源:juejin.cn/post/7212828749128876092
收起阅读 »

React框架部署实战:打造高效现代化的Web应用

web
React框架部署实战 在前端开发领域,React框架因其高效的组件化架构和出色的性能而备受开发者喜爱。然而,要让React应用真正展现其魅力,一个关键的环节就是部署。 本文将带领你深入了解React框架的部署过程,为你的Web应用提供一个高效、稳定的运行...
继续阅读 »

React框架部署实战



在前端开发领域,React框架因其高效的组件化架构和出色的性能而备受开发者喜爱。然而,要让React应用真正展现其魅力,一个关键的环节就是部署。



本文将带领你深入了解React框架的部署过程,为你的Web应用提供一个高效、稳定的运行环境。


📍 准备工作:配置环境


在开始部署React应用之前,确保你的开发环境已经配置完善。首先,安装Node.js和npm,这是React应用的基础依赖。随后,使用以下命令安装Create React App,这是一个官方推荐的React应用脚手架,简化了项目的初始化和配置过程。


npx create-react-app my-react-app

📍 生产环境构建:优化性能


React应用在开发过程中使用的是开发环境配置,而在部署到生产环境时,我们需要进行一些优化以提升性能。使用以下命令进行生产环境构建:


npm run build

这将生成一个build文件夹,包含了优化后的、用于生产环境的代码。这一步骤将帮助你减小应用的体积,提高加载速度,使其更适合在生产环境中运行。


📍 选择合适的服务器:保障稳定性


选择一个合适的服务器对于React应用的部署至关重要。你可以选择使用传统的Web服务器,比如Nginx或Apache,也可以考虑使用专门为React应用设计的服务器,如Express或Firebase Hosting。确保服务器能够正确配置,以支持React路由和处理单页面应用的特殊需求。


📍 域名与SSL:提升安全性


为你的React应用配置域名,并考虑启用SSL证书以提高安全性。在大多数情况下,你可以通过云服务提供商或第三方SSL证书颁发机构获取免费的SSL证书。使用HTTPS协议不仅有助于提升安全性,还有可能对搜索引擎排名产生积极影响。


📍 自动化部署:提高效率


自动化部署是一个高效的实践,可以减少人为错误并提高开发团队的工作效率。你可以考虑使用持续集成/持续部署(CI/CD)工具,如Jenkins、Travis CI或GitHub Actions,将代码的自动构建和部署流程整合到你的开发工作流中。


📍 监控与日志:保障可维护性


部署完成后,监控和日志记录是必不可少的环节。使用工具如Sentry、New Relic等,实时监测应用的性能和错误,及时发现并解决潜在的问题。同时,记录应用的日志可以帮助你追踪和分析用户行为,为后续的优化提供有力支持。


📍 版本管理:确保灵活性


在生产环境中,灵活地管理React应用的版本是至关重要的。使用工具如Docker,可以打包你的应用及其依赖,确保在不同环境中的一致性。结合版本控制工具如Git,能够轻松地进行回滚和发布新版本。


📍 总结


通过本文的步骤,你可以更好地了解如何部署React应用,确保其在生产环境中高效、稳定地运行。部署不仅仅是一个技术问题,更是一个关乎用户体验和团队效率的重要环节。通过合理的部署流程,你的React应用将能够展现出其设计之美和高效性能,为用户提供卓越的使用体验。


作者:知识浅谈
来源:juejin.cn/post/7301976895913689125
收起阅读 »

历时一个月,6年前端降薪上岸了

web
6年前端,11.15号,拿到了这一个月以来的第一份offer,降薪,教育行业,正如之前在文章中所说的,我就是头铁。 这篇文章就水一下我这差不多一个月的面试旅程吧。 我是在7.14号左右从一家培训公司毕业,然后拿了N+1,我就回家了,一直待到国庆之后才回的北京。...
继续阅读 »

6年前端,11.15号,拿到了这一个月以来的第一份offer,降薪,教育行业,正如之前在文章中所说的,我就是头铁。
这篇文章就水一下我这差不多一个月的面试旅程吧。
我是在7.14号左右从一家培训公司毕业,然后拿了N+1,我就回家了,一直待到国庆之后才回的北京。


第一次面试


第一个面试是我的朋友给我内推的公司,智联招聘,时间大概是在10.08,当时我啥也没有准备,裸面,最后还是被pass了。面试官当时问了我这么几个问题:




  1. 说说你自己想做的技术方向(主要是针对招聘岗位看看你是否合适,他们想找一个做监控平台和业务的)

  2. 你认为什么是好的代码

  3. 对程序设计原则有了解吗

  4. 对于质量保障,前端职能该做什么(前端质检工作)

  5. 小程序从聊天记录选择文件,上传失败,你觉得应该如何去解决和排查这个问题

  6. node是如何处理高并发的



从整个面试来看,涉及到前端技术相关的问题其实也没几个,第1个问题,面试官就已经给我挖坑了,我当时巴拉巴拉说了一堆关于前端基建和自己想做前端底层相关的事情,很明显,他需要的不是这样的人,因为他们现在做的还是以业务为主,还有监控相关的事情正在推进,需要有人着手去做。然后就是第4个问题,这个问题就可以考察出我对于前端整个开发生命周期的质量保证了解多少,从而判断出我是不是适合这个岗位,很明显的,我的回答并不满意。第2个和第3个问题,我确实以前没有考虑过,后来我查了一下,如果对理论知识比较关注的话,这些东西应该都需要了解的。由此考察出我对这方面知识的欠缺。第5个问题,是之前他们在开发遇到的一个问题,然后问我如何解决这个问题,我当时脑子短路了,一直在寻思这个问题到底出在哪里,而他想知道的是,当遇到问题的时候如何排查解决的一个思路,很明显,我走错路了。整个面试过程也很清晰,考察三个点,第一点是不是匹配他们的招聘岗位,第二点是不是理论基础比较扎实,第三点解决问题的能力。


如何复习


经过这次面试,我把上面几个问题总结了,在后面的面试中2、3、4经常被问到,我也是对答如流。由此开始了我的面试旅程。那么接下来就讲讲我是如何来复习和做总结的。


首先,复习基础知识(八股文),一般就是css、js、ts、vue、react、webpack浏览器相关面试题,如何去找面试题呢?我最常用的方式是掘金去看优质的文章,这是最浪费时间也是最能能掌握的一种方式,可能很多面试题手册写的就是针对面试题的那么几句话,并没有讲的很清楚,知其然,不知其所以然,问的详细一点就会懵了。如果需要面试题手册的可以加我WX:xiumubai01。然后就是就是总结,我选择的是xmind写个思维导图,罗列大纲。这样看起来就非常清晰,哪些知识点我复习过了,对应的每个知识点有哪些细节,我都会做出标记,这样,在我的脑海中就形成了一套面试的话术,我面试的时候也会根据这样一个结构去讲,一来我心中有思路,二来面试官听的不迷糊。
大概差不多像下面的这样的:
image.png


以某个知识点为例:
image.png
我把整个浏览器相关的知识点都总结到了一起,这样既可以方便复习,也能让我系统的掌握相关浏览器的知识点。
想要获取思维导图的道友们可以可以加我WX:xiumubai01


关于项目问题,我也提以下。非常重要。一般面试官会根据你写的简历上面的项目问你,让你讲讲做的最拿手的一个项目,做了哪些事?遇到过什么问题?怎么解决的?当你讲项目的时候,面试官就能直观的感受到你平时工作中到底几斤几两。我的项目中写了一个业务(剧本直播),微前端平台,低代码平台。比如我微前端平台面试官会问到的问题:你如何选型?为什么选这个框架(qiankun)?那qiankun当中你使用的时候遇到过什么问题?你觉得这个框架有哪些不足的地方?它是如何实现js沙箱隔离的?(变态一点的直接让你实现一个)。所以项目你一定要吃透。当你讲的过程中,人家会提问各种场景,问你如何解决的,如果你提前没有想到,那只能当场退役。


除了自己复习相关的知识点以外,面试总结特别重要。我养成的习惯是每次面试完了立马总结问到的问题,然后进行复盘,这次面试哪里回答的不好,面试官想要考察的能力是什么?这次回答不好的问题下次我能不能应对?按照我的经验,每次总结完之后大概率后面的面试都会问到同样的问题。以下是我总结的:
image.png


如果你面试完了记不住面试官问的问题,我的做法是掏出纸和笔,在面试官提问的时候,记下问题关键词,这样方便面试完了以后回忆。


如何coding


加下来就是关于coding题,很多人都会恐惧,我也是,没有思路,前一天刚刷的代码第二天就忘了。没有他法,脑子笨,只能靠理解加强记忆,那最好的办法就是你要把这道题目吃透,研究明白,思路清晰,其实不管任何代码,你先得知道这道题怎么解啊,然后才能用代码实现。我这里总结了我面试以来手写的一些coding题,放在github了。大家可以打开链接自取。



github: github.com/xiumubai/co…


gitee: gitee.com/xiumubai/co…



如何消除焦虑


在面试的过程中,难免会有学不进去,面试遭受打击,长时间没有offer心中气垒等等情况的发生。尤其是当你面试3-4轮以后,眼见要拿到offer了,最后杳无音信,或者人事直接通知你pass了,这时候是最打击人的。整个人就像被掏空了一样。


面对以上这种情况,我后来想了一种排解的方式,拉了个微信群,把最近找工作的道友们一起拉进去,大家互相鼓励,讨论遇到的面试题,最近的市场行情,这样可以分解一下压力,转移注意力,有可能有的人比你更惨。有想进群的朋友可以关注一下我的公众号「白哥学前端」,进群,群里有我最新的一些面试题和xmind文件分享。


当然保持积极的心情也很重要,不要做一些沉迷动作(比如打游戏,晚上不睡,早上不起),把自己的时间调整成上班的时间,这个时间点你就做跟学习有关的事情。


这个过程是很痛苦和煎熬的,相信大家都能坚持住,最后,祝大家都能顺利找到满意的工作。


作者:白哥学前端
来源:juejin.cn/post/7301909438540267531
收起阅读 »

企业应该如何选择合适的api解决方案

当下有不少企业需要通过外部的api服务来进行公司内部的战略规划,那么在进行选择时,如何才能在满足公司需求的同时还能选择好合适自己的api解决方案呢?可以根据以下这几个标准来评估,以确保api方案能够实现企业利益最大化。服务性能为了满足企业对页面速度的期望,AP...
继续阅读 »


当下有不少企业需要通过外部的api服务来进行公司内部的战略规划,那么在进行选择时,如何才能在满足公司需求的同时还能选择好合适自己的api解决方案呢?可以根据以下这几个标准评估,以确保api方案能够实现企业利益最大化。

服务性能

为了满足企业对页面速度的期望,API响应时间不应超过 90 毫秒。保证API 快速运转其中一种方法是采用客户端将数据传送到 CDN 网络的边缘节点,而不是存储在数据中心某处的服务器上。

但是并非每个功能都需要相同级别的速度。例如,某电商产品描述内容大多是静态的,而商品库存则不是。因此,由企业 CMS API 提供的内容可能不需要与来自其他渠道的数据相同的速度。

虽然高性能对于企业使用的每个 API 都很重要,但每个 API 需要满足的实际性能基准却因功能和企业要解决的最终用户问题而异。

文档

对于企业采用的任何 API 解决方案来说,完整的文档是必不可少的。强大的文档让开发人员可以更有效地使用 API,并最终更快地将产品推向市场。

但即使在提供详尽文档的API提供商中,开发人员也无法直接有效上手使用。例如,我已经阅读了大量文档,这些文档是在已经了解 API 的工作原理的情况下编写的这使得新开发人员更难学习和理解 API 的功能。因此文档不仅需要完整还需要被人理解清楚。

安全

任何 API 提供商都应提供强大的安全性,以确保 API 不会被不良行为者恶意使用或被黑客入侵以暴露敏感信息。所以需要看API服务商是如何描述其安全性的,这关系到企业的信息安全问题。

灵活性和可扩展性

企业可以自由地使用 API 创建任何他们想要的东西,这种创建包括在客户体验方面、或者对组织有意义的方式去管理后端信息。

换句话说,API 解决方案应该定义明确

定价模式

最后要考虑的是 API 的定价模型,包括实际成本,还包括提供商使用其服务的收费方式。

例如,是否按订单收费?按带宽?按记录数?这种定价模式需要适用于企业的业务

API 解决方案应该使其易于增长和发展

如果企业 API 作为平台重构项目的一部分进行探索那么有可能会存在API无法满足不断变化的客户期望或者会限制团队创造力。因此企业选择的 API 应该满足内部需求,并且可以随着需求的发展而轻松做出更改例如将一个 API 提供者换成另一个。

数聚变基于以上几个标准打造了专门为企业提供标准化数据服务的API平台,通过API接口轻松接入精细化数据,帮助企业完成数字化转型,提高工作效率,更多信息点击了解:数聚变 | 数智技术加速数据要素安全流通,融合聚变 (goldwind.com)

收起阅读 »

用Kotlin通杀“一切”进率换算

用Kotlin通杀“一切”进率换算之存储容量 前言 在之前的文章《用Kotlin Duration来优化时间运算》 中,用Duration可以很方便的进行时间的单位换算和运算。我忽然想到平时的工作中经常用到的换算和运算。(人民币汇率;长度单位m,cm;质量单位...
继续阅读 »

用Kotlin通杀“一切”进率换算之存储容量


前言


在之前的文章《用Kotlin Duration来优化时间运算》
中,用Duration可以很方便的进行时间的单位换算和运算。我忽然想到平时的工作中经常用到的换算和运算。(人民币汇率;长度单位m,cm;质量单位 kg,g,lb;存储容量单位 mb,gb,tb 等等)


//进率为1024
val tenMegabytes = 10 * 1024 * 1024 //10mb
val tenGigabytes = 10 * 1024 * 1024 * 1024 //10gb

这样的业务代码加入了单位换算后阅读性就变差了,能否有像Duration一样的api实现下面这样的代码呢?


fun main() {
1.kg = 2.20462262.lb; 1.m = 100.cm

val fiftyMegabytes = 50.mb
val divValue = fiftyMegabytes - 30.mb
// 20mb
val timesValue = fiftyMegabytes * 2.4
// 120mb

// 1G文件 再增加2个50mb的数据空间
val fileSpace = fiftyMegabytes * 2 + 1.gb
RandomAccessFile("fileName","rw").use {
it.setLength(fileSpace.inWholeBytes)
it.write(...)
}
}

下面我们通过分析Duration源码了解原理,并且实现存储容量单位DataSize的换算和运算。


简单拆解Duration


kotlin没有提供,要做到上面的api那么我不会啊,但是我看到Duration可以做到,那我们来看看它的原理,进行仿写就行了。



  1. 枚举DurationUnit是用来定义时间不同单位,方便换算和转换的(详情看源码或上篇文)。

  2. Duration是如何做到不同单位的数据换算的,先看看Duration的创建函数和构造函数。toDuration把当前的值通过convertDurationUnit把时间换算成nanos或millis的值,再通过shl运算用来记录单位。
    //Long创建 Duration
    public fun Long.toDuration(unit: DurationUnit): Duration {
    //最大支持的 nanos值
    val maxNsInUnit = convertDurationUnitOverflow(MAX_NANOS, DurationUnit.NANOSECONDS, unit)
    //当前值如果在最大和最小值中间 表示不会溢出
    if (this in -maxNsInUnit..maxNsInUnit) {
    //创建 rawValue 是Nanos的 Duration
    return durationOfNanos(convertDurationUnitOverflow(this, unit, DurationUnit.NANOSECONDS))
    } else {
    //创建 rawValue 是millis的 Duration
    val millis = convertDurationUnit(this, unit, DurationUnit.MILLISECONDS)
    return durationOfMillis(millis.coerceIn(-MAX_MILLIS, MAX_MILLIS))
    }
    }
    // 用 nanos
    private fun durationOfNanos(normalNanos: Long) = Duration(normalNanos shl 1)
    // 用 millis
    private fun durationOfMillis(normalMillis: Long) = Duration((normalMillis shl 1) + 1)
    //不同os平台实现,肯定是 1小时60分 1分60秒那套算法
    internal expect fun convertDurationUnit(value: Long, sourceUnit: DurationUnit, targetUnit: DurationUnit): Long


  3. Duration是一个value class用来提升性能的,通过rawValue还原当前时间换算后的nanos或millis的数据value。为何不全部都用Nanos省去了这些计算呢,根据代码看应该是考虑了Nanos的计算会溢出。用一个long值可以还原构造对象前的所有参数,这代码设计真牛逼。
    @JvmInline
    public value class Duration internal constructor(private val rawValue: Long) : Comparable<Duration> {
    //原始最小单位数据
    private val value: Long get() = rawValue shr 1
    //单位鉴别器
    private inline val unitDiscriminator: Int get() = rawValue.toInt() and 1
    private fun isInNanos() = unitDiscriminator == 0
    private fun isInMillis() = unitDiscriminator == 1
    //还原的最小单位 DurationUnit对象
    private val storageUnit get() = if (isInNanos()) DurationUnit.NANOSECONDS else DurationUnit.MILLISECONDS


  4. Duration是如何做到算术运算的,是通过操作符重载实现的。不同单位Duration,持有的数据是同一个单位的那么是可以互相运算的,我们后面会着重介绍和仿写。

  5. Duration是如何做到逻辑运算的(>,<,>=,<=),构造函数实现了接口Comparable<Duration>重写了operator fun compareTo(other: Duration): Int,返回1,-1,0



Duration主要依靠对象内部持有的rawValue: Long,由于value的单位是“相同”的,就可以实现不同单位的换算和运算。



存储容量单位换算设计




  1. 存储容量的单位一般有比特(b),字节(B),千字节(KB),兆字节(MB),千兆字节(GB),太字节(TB),拍字节(PB),艾字节(EB),泽字节(ZB),尧字节(YB)
    ,考虑到实际应用和Long的取值范围我们最大支持PB即可。


    enum class DataUnit(val shortName: String) {
    BYTES("B"),
    KILOBYTES("KB"),
    MEGABYTES("MB"),
    GIGABYTES("GB"),
    TERABYTES("TB"),
    PETABYTES("PB")
    }



  2. 对于存储容量来说最小单位我们就定为Bytes,最大支持到PB,然后可以省去对数据过大的溢出的"单位鉴别器"设计。(注意使用pb时候,>= 8192.pb就会溢出)


    @JvmInline
    value class DataSize internal constructor(private val rawBytes: Long)



  3. 参照Duration在创建和最后单位换算时候都用到了convertDurationUnit函数,接受原始单位和目标单位。另外考虑到可能出现换算溢出使用Math.multiplyExact来抛出异常,防止数据计算异常无法追溯的问题。


    /** Bytes per Kilobyte.*/
    private const val BYTES_PER_KB: Long = 1024
    /** Bytes per Megabyte.*/
    private const val BYTES_PER_MB = BYTES_PER_KB * 1024
    /** Bytes per Gigabyte.*/
    private const val BYTES_PER_GB = BYTES_PER_MB * 1024
    /** Bytes per Terabyte.*/
    private const val BYTES_PER_TB = BYTES_PER_GB * 1024
    /** Bytes per PetaByte.*/
    private const val BYTES_PER_PB = BYTES_PER_TB * 1024

    internal fun convertDataUnit(value: Long, sourceUnit: DataUnit, targetUnit: DataUnit): Long {
    val valueInBytes = when (sourceUnit) {
    DataUnit.BYTES -> value
    DataUnit.KILOBYTES -> Math.multiplyExact(value, BYTES_PER_KB)
    DataUnit.MEGABYTES -> Math.multiplyExact(value, BYTES_PER_MB)
    DataUnit.GIGABYTES -> Math.multiplyExact(value, BYTES_PER_GB)
    DataUnit.TERABYTES -> Math.multiplyExact(value, BYTES_PER_TB)
    DataUnit.PETABYTES -> Math.multiplyExact(value, BYTES_PER_PB)
    }
    return when (targetUnit) {
    DataUnit.BYTES -> valueInBytes
    DataUnit.KILOBYTES -> valueInBytes / BYTES_PER_KB
    DataUnit.MEGABYTES -> valueInBytes / BYTES_PER_MB
    DataUnit.GIGABYTES -> valueInBytes / BYTES_PER_GB
    DataUnit.TERABYTES -> valueInBytes / BYTES_PER_TB
    DataUnit.PETABYTES -> valueInBytes / BYTES_PER_PB
    }
    }

    internal fun convertDataUnit(value: Double, sourceUnit: DataUnit, targetUnit: DataUnit): Double {
    val valueInBytes = when (sourceUnit) {
    DataUnit.BYTES -> value
    DataUnit.KILOBYTES -> value * BYTES_PER_KB
    DataUnit.MEGABYTES -> value * BYTES_PER_MB
    DataUnit.GIGABYTES -> value * BYTES_PER_GB
    DataUnit.TERABYTES -> value * BYTES_PER_TB
    DataUnit.PETABYTES -> value * BYTES_PER_PB
    }
    require(!valueInBytes.isNaN()) { "DataUnit value cannot be NaN." }
    return when (targetUnit) {
    DataUnit.BYTES -> valueInBytes
    DataUnit.KILOBYTES -> valueInBytes / BYTES_PER_KB
    DataUnit.MEGABYTES -> valueInBytes / BYTES_PER_MB
    DataUnit.GIGABYTES -> valueInBytes / BYTES_PER_GB
    DataUnit.TERABYTES -> valueInBytes / BYTES_PER_TB
    DataUnit.PETABYTES -> valueInBytes / BYTES_PER_PB
    }
    }



  4. 扩展属性和构造DataSize,rawBytes是Bytes因此所有的目标单位设置为DataUnit.BYTES,而原始单位就通过调用者告诉convertDataUnit


    fun Long.toDataSize(unit: DataUnit): DataSize {
    return DataSize(convertDataUnit(this, unit, DataUnit.BYTES))
    }
    fun Double.toDataSize(unit: DataUnit): DataSize {
    return DataSize(convertDataUnit(this, unit, DataUnit.BYTES).roundToLong())
    }
    inline val Long.bytes get() = this.toDataSize(DataUnit.BYTES)
    inline val Long.kb get() = this.toDataSize(DataUnit.KILOBYTES)
    inline val Long.mb get() = this.toDataSize(DataUnit.MEGABYTES)
    inline val Long.gb get() = this.toDataSize(DataUnit.GIGABYTES)
    inline val Long.tb get() = this.toDataSize(DataUnit.TERABYTES)
    inline val Long.pb get() = this.toDataSize(DataUnit.PETABYTES)

    inline val Int.bytes get() = this.toLong().toDataSize(DataUnit.BYTES)
    inline val Int.kb get() = this.toLong().toDataSize(DataUnit.KILOBYTES)
    inline val Int.mb get() = this.toLong().toDataSize(DataUnit.MEGABYTES)
    inline val Int.gb get() = this.toLong().toDataSize(DataUnit.GIGABYTES)
    inline val Int.tb get() = this.toLong().toDataSize(DataUnit.TERABYTES)
    inline val Int.pb get() = this.toLong().toDataSize(DataUnit.PETABYTES)

    inline val Double.bytes get() = this.toDataSize(DataUnit.BYTES)
    inline val Double.kb get() = this.toDataSize(DataUnit.KILOBYTES)
    inline val Double.mb get() = this.toDataSize(DataUnit.MEGABYTES)
    inline val Double.gb get() = this.toDataSize(DataUnit.GIGABYTES)
    inline val Double.tb get() = this.toDataSize(DataUnit.TERABYTES)
    inline val Double.pb get() = this.toDataSize(DataUnit.PETABYTES)



  5. 换算函数设计
    Duration用toLong(DurationUnit)或者toDouble(DurationUnit)来输出指定单位的数据,inWhole系列函数是对toLong(DurationUnit) 的封装。toLong和toDouble实现就比较简单了,把convertDataUnit传入输出单位,而原始单位就是rawValue的单位DataUnit.BYTES
    toDouble需要输出更加精细的数据,例如: 512mb = 0.5gb。


    val inWholeBytes: Long
    get() = toLong(DataUnit.BYTES)
    val inWholeKilobytes: Long
    get() = toLong(DataUnit.KILOBYTES)
    val inWholeMegabytes: Long
    get() = toLong(DataUnit.MEGABYTES)
    val inWholeGigabytes: Long
    get() = toLong(DataUnit.GIGABYTES)
    val inWholeTerabytes: Long
    get() = toLong(DataUnit.TERABYTES)
    val inWholePetabytes: Long
    get() = toLong(DataUnit.PETABYTES)

    fun toDouble(unit: DataUnit): Double = convertDataUnit(bytes.toDouble(), DataUnit.BYTES, unit)
    fun toLong(unit: DataUnit): Long = convertDataUnit(bytes, DataUnit.BYTES, unit)



操作符设计


在Kotlin 中可以为类型提供预定义的一组操作符的自定义实现,被称为操作符重载。这些操作符具有预定义的符号表示(如 + 或
*)与优先级。为了实现这样的操作符,需要为相应的类型提供一个指定名称的成员函数或扩展函数。这个类型会成为二元操作符左侧的类型及一元操作符的参数类型。


如果函数不存在或不明确,则导致编译错误(编译器会提示报错)。下面为常见操作符对照表:


操作符函数名说明
+aa.unaryPlus()一元操作 取正
-aa.unaryMinus()一元操作 取负
!aa.not()一元操作 取反
a + ba.plus(b)二元操作 加
a - ba.minus(b)二元操作 减
a * ba.times(b)二元操作 乘
a / ba.div(b)二元操作 除

算术运算支持



  1. 这里用算术运算符+实现来举例:假如DataSize对象需要重载操作符+
    val a = DataSize(); val c: DataSize = a + b


  2. 需要定义扩展函数1或者添加成员函数2
    1. operator fun DataSize.plus(other: T): DataSize {...}
    2. class DataSize { operator fun plus(other: T): DataSize {...} }


  3. 函数中的参数other: T表示b的对象类型,例如
    // val a: DataSize; val b: DataSize; a + DataSize()
    operator fun DataSize.plus(other: DataSize): DataSize {...}
    // val a: DataSize; val b: Int; a + 1
    operator fun DataSize.plus(other: Int): DataSize {...}


  4. 为了阅读性,Duration不会和同类型的对象乘除法运算,而使用了Int或Double,因此重载运算符用了operator fun times(scale: Int): Duration

  5. 那么在DataSize中我们也重载(+,-,*,/),并且(*,/)重载的参数只支持Int和Double即可
    operator fun unaryMinus(): DataSize {
    return DataSize(-this.bytes)
    }
    operator fun plus(other: DataSize): DataSize {
    return DataSize(Math.addExact(this.bytes, other.bytes))
    }

    operator fun minus(other: DataSize): DataSize {
    return this + (-other) // a - b = a + (-b)
    }

    operator fun times(scale: Int): DataSize {
    return DataSize(Math.multiplyExact(this.bytes, scale.toLong()))
    }

    operator fun div(scale: Int): DataSize {
    return DataSize(this.bytes / scale)
    }

    operator fun times(scale: Double): DataSize {
    return DataSize((this.bytes * scale).roundToLong())
    }

    operator fun div(scale: Double): DataSize {
    return DataSize((this.bytes / scale).roundToLong())
    }

    上面的操作符重载中minus(),我们使用了 plus()unaryMinus()重载组合a-b = a+(-b),这样我们可以多一个-DataSize的操作符


逻辑运算支持




  • (>,<,>=,<=)让DataSize构造函数实现了接口Comparable<DataSize>重写了operator fun compareTo(other: DataSize): Int,返回rawBytes对比值即可。




  • (==,!=)通过equals(other)函数实现的,value class默认为rawBytes的对比,可以通过java字节码看到。kotlin 1.9之前不支持重写value classs的equals和hashCode


    value class DataSize internal constructor(private val bytes: Long) : Comparable<DataSize> {
    override fun compareTo(other: DataSize): Int {
    return this.bytes.compareTo(other.bytes)
    }
    //示例
    600.mb > 0.5.gb //true
    512.mb == 0.5.gb




操作符重载的目的是为了提升阅读性,并不是所有对象为了炫酷都可以用操作符重载,滥用反而会增加代码的阅读难度。例如给DataSize添加*操作符,5mb * 2mb 就让人头大。



获取字符串形式


为了方便打印和UI展示,一般我们需要重写toSting。Duration的toSting不需要指定输出单位,可以详细的输出当前对象的字符串格式(1h 0m 45.677s)算法比较复杂。我不太会,就简单实现指定输出单位的toString(DataUnit)


 override fun toString(): String = String.format("%dB", rawBytes)

fun toString(unit: DataUnit, decimals: Int = 2): String {
require(decimals >= 0) { "decimals must be not negative, but was $decimals" }
val number = toDouble(unit)
if (number.isInfinite()) return number.toString()
val newDecimals = decimals.coerceAtMost(12)
return DecimalFormat("0").run {
if (newDecimals > 0) minimumFractionDigits = newDecimals
roundingMode = RoundingMode.HALF_UP
format(number) + unit.shortName
}
}

单元测试


功能都写好了需要验证期望的结果和实现的功能是否一直,那么这个时候就用单元测试最好来个100%覆盖。


class ExampleUnitTest {
@Test
fun data_size() {
val dataSize = 512.mb

println("format bytes:$dataSize")
// format bytes:536870912B
println("format kb:${dataSize.toString(DataUnit.KILOBYTES)}")
// format kb:524288.00KB
println("format gb:${dataSize.toString(DataUnit.GIGABYTES)}")
// format gb:0.50GB
// 单位换算
assertEquals(536870912, dataSize.inWholeBytes)
assertEquals(524288, dataSize.inWholeKilobytes)
assertEquals(512, dataSize.inWholeMegabytes)
assertEquals(0, dataSize.inWholeGigabytes)
assertEquals(0, dataSize.inWholeTerabytes)
assertEquals(0, dataSize.inWholePetabytes)
}

@Test
fun data_size_operator() {
val dataSize1 = 512.mb
val dataSize2 = 3.gb

val unaryMinusValue = -dataSize1 //取负数
println("unaryMinusValue :${unaryMinusValue.toString(DataUnit.MEGABYTES)}")
// unaryMinusValue :-512.00MB

val plusValue = dataSize1 + dataSize2 //+
println("plus :${plusValue.toString(DataUnit.GIGABYTES)}")
// plus :3.50GB

val minusValue = dataSize1 - dataSize2 // -
println("minus :${minusValue.toString(DataUnit.GIGABYTES)}")
// minus :-2.50GB

val timesValue = dataSize1 * 2 //乘法
println("times :${timesValue.toString(DataUnit.GIGABYTES)}")
// times :1.00GB

val divValue = dataSize2 / 2 //除法
println("div :${divValue.toString(DataUnit.GIGABYTES)}")
// div :1.50GB
}

@Test(expected = ArithmeticException::class)
fun data_size_overflow() {
8191.pb
8192.pb //溢出了不支持,如果要支持参考"单位鉴别器"设计
}

@Test
fun data_size_compare() {
assertTrue(600.mb > 0.5.gb)
assertTrue(512.mb == 0.5.gb)
}
}

总结


通过学习Kotlin Duration的源码,举一反三应用到储存容量单位转换和运算中。Duration中的拆解计算api,还有toSting算法实现就留给大家学习吧。当然了你也可以实现和Duration一样更加精细的"单位鉴别器"设计,支持bit、ZB、BB等大单位。


另外类似的进率场景也可以实现,用Kotlin通杀“一切进率换算”。比如Degrees角度计算 -90.0.degrees == 270.0.degrees;质量计算kg和磅,两等等1.kg == 2.20462262.lb;人民币汇率 (动态实现算法)8.dollar == 1.rmb 🐶


github 代码: github.com/forJrking/K…


操作符重载文档: book.kotlincn.net/text/operat…


作者:forJrking
来源:juejin.cn/post/7301145359852765218
收起阅读 »

MQTT客户端学习路线总结

本篇仅仅是记录一下MQTT学习的过程和感想,文字偏多 前言 总结一下MQTT协议的学习过程, 大概分为9步。 在IoT最热门时,有过一些了解,仅限名词解释。这次在为工厂装修设计时,涉及到了一些智能设备,也因此近距离接触到了MQTT协议相关的系统。虽然直接采...
继续阅读 »

本篇仅仅是记录一下MQTT学习的过程和感想,文字偏多



前言


总结一下MQTT协议的学习过程, 大概分为9步。


MQTT学习过程.jpg


在IoT最热门时,有过一些了解,仅限名词解释。这次在为工厂装修设计时,涉及到了一些智能设备,也因此近距离接触到了MQTT协议相关的系统。虽然直接采购的是成品(包含施工方案),但出于好奇和喜欢动手的本能,我想学习一下MQTT通信应用协议


使用


直接体验采购的智能设备,也算是使用,但总感觉少了点什么-感觉没有真正体验到MQTT协议通信。基于此种想法,翻阅MQTT环境搭建的指导文章,开始在自己的电脑上捣鼓安装MQTT客户端软件和MQTT服务器软件,一番折腾后,成功安装了MQTTXmosquitto


在MQTTX客户端上,看到可以成功的接收消息和发送消息,瞬间有种傻瓜式的成就感(我会用MQTT协议啦)。


体验完MQTTX,按照mosquitto官方指导,竟然发现它既可用作服务器同时还可用做客户端,在mac终端中急切的敲着发送主题的命令,在MQTTX上看到收到的消息,就一个感觉:呗爽。


对于一个新手,MQTTX和mosquitto的成就感促使我想继续阅读MQTT相关的文章,也因此从MQTT官网找到了Steve's Internet Guide博客


体验了MQTT软件和阅读了Steve's Internet Guide后,对运行一个客户端源码工程十分渴望,如此优秀的博客,配上程序运行时的日志,简直是学习MQTT的“下饭菜”。决定了就开干,从MQTT官网选型合适的MQTT协议实现库,最终我选定了Elipse paho


实践


Elipse paho共包含了17种版本,涉及多种语言(C语言,python, javascript, C++, golang, ruby, rust), 我实践学习用的是paho.mqtt.java版本。


用IntelliJ IDEA(社区版)创建一个属于自己的命令式应用MQTTHarvey, 将“org.eclipse.paho.mqttv5.client” 源码引入自己的应用。然后创建自己实践用入口代码(包括场景模拟和日志打印)。


最佳体验的方式:上手直接敲代码,调用核心功能API,看效果。如果API不熟,记住功能名称,疯狂的在掘金中搜索相关的介绍文章,比如:MQTT如何发送主题, MQTT如何订阅等等。


在首次成功运行已集成好源码的工程后,针对PUBLISH,SUBCRIBE需要先实践一遍(当然,最初我对实践这两个消息的称呼为:发送消息,接收消息).


消息的发送和接收源码在哪里?带着这个疑问,“胡乱一通”断点,终于找到了地方,接收消息的文件是MqttInputStream.java, 发送消息的文件是MqttOutputStream.java。


协议流到底是什么样子?能不能输出这些字节流,以观其全貌,知其结构。


协议初探


既然是协议,那必然有数据格式规范,编码后的数据按照字节流的方式进行传输,那么将字节流按照字节一个一个打印日志输出出来,就是协议的样子。我应该最先了解哪个协议样子呢?依据基础常识,MQTT客户端开始运行时,与服务端建立连接肯定是第一次联网操作,既然是这个样子,MQTT有没有关于联机的协议呢?经过一顿搜索,CONNECT 是关于客户端连接服务器的协议。那如何将其打印出来呢?因为都是字节流,又不了解协议规范,想要打印出来真的比较难。


正向研究协议在刚开始阶段,硬杠还是比较浪费时间的,甚至会打击继续学习的信心。后来我才用了一个比较讨巧的方式, 写一段只包含连接服务器的代码,就可以解决难以仅仅打印CONNECT协议的问题。


在二进制流打印完成后,在IDE控制台看到的日志,其实都是一个字节一个字节的字符串信息,很难以看懂。这个时候就需要拿出MQTT规范文档找到CONNECT协议的定义,然后手动尝试去解析每个字节,直到可以把字节都能和规范对上号,才可以证明对这个协议稍微搞懂了。


在手动完成CONNECT协议后,我就迫不及待的想看看消息发送协议,后来想了一下,没必要那么急,毕竟发送消息协议是如此的重要,肯定是比较复杂的。调整一下探究方向,一个新手,想要学习协议分析,从简单的协议入手,比如:断开连接,心跳。因此,我第二个分析的协议就是DISCONNECT,带着好奇的激情,最终完成了这个协议分析,那一天晚上睡觉时,感觉都是无比的开心。


解码


其实软件的职责就是能自动解决规范化的一些流程问题,数据格式问题。在协议初探时,还停留在手动分析字节流阶段,毕竟MQTT客户端如果应用在真实场景,自动解码字节流这样的功能,肯定是必备的。


既然在研究MQTT协议,那就需要拿出点诚意,自己写程序来解码字节流,然后将其拆解为规范中可描述的文字。


依然从简单的协议分析,然后再去分析复杂的。通过MQTT规范文档了解到,CONNECT,PING,DISCONNECT都是相对比较简单的,也容易写测试代码来完成解码实验场景。


在真正通过代码来解码CONNECT协议时,才发现固定头,可变头真的在用代码一步一步解码时,非常容易出错,因为是新手,再加上急于求成(毕竟已经会手动分析了),程序要么执行到一半就发生异常,要么在测试代码中稍微调整一下参数,程序就会崩溃。冷静下来后,发现还是要按照规范文档,一个属性一个属性往下研究,不能急躁。并且在这个过程中,发现自己对待MQTT规范不够重视,连数据类型都直接给忽略了,比如规范中的 “1.5 Data representation” 内容。因为没有重视它,导致在按照规范实现解码时,时常囫囵吞枣,自己认为是代表的是什么意思,就赶紧敲代码实现。


在刚开始对协议解码时,因为懒,所以只针对数据包的头部做了解码,完成了控制包类型1到14(控制包类型:“2.1.2 MQTT Control Packet type”)。完成之后,回看代码发现重复代码很多,分析完重复部分后,才发现像Reason Code,UTF-8 Encoded, Variable Byte Integer这些都是具有全局性的,即:可以把他们的解码分别做成公共部分。为了证实自己的这一点理解,赶紧翻看客户端源码是不是这样的,果不其然,理解正确。


工程分析


因为已经完成了一部分的解码工作,并且对MQTT规范阅读也已经上道,所以就想歇一歇,安静的学习一下客户端源码,它到底是如何实现发送接收消息的,它的代码是如何实现了每一条MQTT规范的。带着这些疑问,先确定程序执行每个控制包的方法调用流程,然后确定运行时的线程数,最终再分析代码之间的依赖关系。


代码跟踪是枯燥的,看别人将协议实现的如此漂亮,深深的受了打击。


每天偶尔看看源码,想象是自己在维护它,熟练的记住每个API,让自己的信心慢慢恢复。


术语理解


有了阅读MQTT规范的技能,自己解码的能力和分析源码之后,有种放空茫然的感觉。MQTT到底是什么,我如何描述它,它在布道时,是如何宣传的,等等?


可能要解决上述疑惑,需要找MQTT官方资料认真学习一下,然后再看看中文是如何教授的。


因此,我从MQTT官网找到HIVEMQ发布的MQTT基础文章,整个系列有十部分内容,然后再逐字逐句翻译,通过翻译,让自己产生疑问,然后再带着疑问去阅读MQTT规范,如果还是无法理解,再通过代码做实验。总之,硬着头皮也要将MQTT规范中的术语或者别人对MQTT的描述理解清楚。


场景模拟


在完成HIVEMQ基础文章的翻译之后,算得上对MQTT有点感觉了,也因此想要将自己的实践测试代码,能不能按照使用场景,分别实现一遍。



场景



  • 连接 -> 断开 -> 重连

  • 连接 -> 订阅 -> 解除订阅

  • 连接 -> 心跳

  • 连接 -> 恢复会话

  • 等等



为什么做这样的事情,我是基于2个原因:1. 我对代码库API及参数使用不熟 2. 提前模拟这些场景,对实战中发生的故障分析,肯定有指导作用


MQTT协议实现


通过一系列的学习和实践,如果真的已经搞懂了MQTT,那么我应该自己可以实现一个简化版的MQTT客户端


比如:实现最简化的功能,连接broker服务器


graph TD
建立Socket --> 发送CONNECT协议 --> 解析CONNACK

当然,自研一个MQTT客户端,从个人来讲,确实对技术能力提升有很大帮助,从公司来讲,那就是Money(比如:杭州映云科技有限公司)。


扩展


短时间内是否真的可以搞懂MQTT?难。协议规范纯理论学习是可以慢慢搞的十分清楚的,但MQTT最终是为了解决生活中实际的通信问题的,那就意味网络原因,数据安全也好,都可能让一个开发人员耗费大量的时间去排查定位问题。基于此种考虑,想要提高技能,增加增经验,可能需要建立问题库,及查看其他人或者公司在使用MQTT过程中的问题列表,并且尝试思考是否能给出解决方案。


作者:harvey_fly
来源:juejin.cn/post/7278953365224734774
收起阅读 »

Android 属性系统入门

这是一个介绍 Android 属性系统的系列文章: Android 属性系统入门(本文) 属性文件生成过程分析 如何添加系统属性 属性与 Selinux 属性系统整体框架与启动过程分析 属性读写过程源码分析 本文基于 AOSP android-10.0.0...
继续阅读 »

这是一个介绍 Android 属性系统的系列文章:



  • Android 属性系统入门(本文)

  • 属性文件生成过程分析

  • 如何添加系统属性

  • 属性与 Selinux

  • 属性系统整体框架与启动过程分析

  • 属性读写过程源码分析


本文基于 AOSP android-10.0.0_r41 版本讲解


在 Android 系统中,为统一管理系统的属性,设计了一个统一的属性系统,每个属性都是一个 key-value 对。
我们可以通过 shell 命令,Native 函数接口,Java 函数接口的方式来读写这些 key-vaule 对。


属性在哪里?


init 进程在启动会去加载后缀为 .prop 的属性文件, 将属性文件中的属性加载到共享内存中, 这样系统就有了默认的一些属性。


属性文件都在哪里呢?


属性文件的后缀绝大部分都是 prop,我们可以在 Android 模拟器的 shell 环境下搜索:


find . -name "*.prop"

/default.prop
/data/local.prop
/system/build.prop
/system/product/build.prop
/vendor/build.prop
/vendor/odm/etc/build.prop
/vendor/default.prop

我们看看 /default.prop 属性文件的内容:


cat /default.prop

#
# ADDITIONAL_DEFAULT_PROPERTIES
#
ro.actionable_compatible_property.enabled=true
ro.postinstall.fstab.prefix=/system
ro.secure=0
ro.allow.mock.location=1
ro.debuggable=1
debug.atrace.tags.enableflags=0
dalvik.vm.image-dex2oat-Xms=64m
dalvik.vm.image-dex2oat-Xmx=64m
dalvik.vm.dex2oat-Xms=64m
dalvik.vm.dex2oat-Xmx=512m
dalvik.vm.usejit=true
dalvik.vm.usejitprofiles=true
dalvik.vm.dexopt.secondary=true
dalvik.vm.appimageformat=lz4
ro.dalvik.vm.native.bridge=0
pm.dexopt.first-boot=extract
pm.dexopt.boot=extract
pm.dexopt.install=speed-profile
pm.dexopt.bg-dexopt=speed-profile
pm.dexopt.ab-ota=speed-profile
pm.dexopt.inactive=verify
pm.dexopt.shared=speed
dalvik.vm.dex2oat-resolve-startup-strings=true
dalvik.vm.dex2oat-max-image-block-size=524288
dalvik.vm.minidebuginfo=true
dalvik.vm.dex2oat-minidebuginfo=true
ro.iorapd.enable=false
tombstoned.max_tombstone_count=50
persist.traced.enable=1
ro.com.google.locationfeatures=1
ro.setupwizard.mode=DISABLED
persist.sys.usb.config=adb

可以看出属性确实是一些 key-value 对。


init 进程会调用 property_load_boot_defaults 函数来加载属性文件:


void property_load_boot_defaults(bool load_debug_prop) {
// TODO(b/117892318): merge prop.default and build.prop files int0 one
// We read the properties and their values int0 a map, in order to always allow properties
// loaded in the later property files to override the properties in loaded in the earlier
// property files, regardless of if they are "ro." properties or not.
std::map<std::string, std::string> properties;
if (!load_properties_from_file("/system/etc/prop.default", nullptr, &properties)) {
// Try recovery path
if (!load_properties_from_file("/prop.default", nullptr, &properties)) {
// Try legacy path
load_properties_from_file("/default.prop", nullptr, &properties);
}
}
load_properties_from_file("/system/build.prop", nullptr, &properties);
load_properties_from_file("/vendor/default.prop", nullptr, &properties);
load_properties_from_file("/vendor/build.prop", nullptr, &properties);
if (SelinuxGetVendorAndroidVersion() >= __ANDROID_API_Q__) {
load_properties_from_file("/odm/etc/build.prop", nullptr, &properties);
} else {
load_properties_from_file("/odm/default.prop", nullptr, &properties);
load_properties_from_file("/odm/build.prop", nullptr, &properties);
}
load_properties_from_file("/product/build.prop", nullptr, &properties);
load_properties_from_file("/product_services/build.prop", nullptr, &properties);
load_properties_from_file("/factory/factory.prop", "ro.*", &properties);

if (load_debug_prop) {
LOG(INFO) << "Loading " << kDebugRamdiskProp;
load_properties_from_file(kDebugRamdiskProp, nullptr, &properties);
}

for (const auto& [name, value] : properties) {
std::string error;
if (PropertySet(name, value, &error) != PROP_SUCCESS) {
LOG(ERROR) << "Could not set '" << name << "' to '" << value
<< "' while loading .prop files" << error;
}
}

property_initialize_ro_product_props();
property_derive_build_fingerprint();

update_sys_usb_config();
}

从源码中我们也可以看到 init 进程加载了哪些属性文件以及加载的顺序。


属性长什么样?


每一个属性是一个 key-value 对:


ro.actionable_compatible_property.enabled=true
ro.postinstall.fstab.prefix=/system
ro.secure=0
ro.allow.mock.location=1
ro.debuggable=1
debug.atrace.tags.enableflags=0
dalvik.vm.image-dex2oat-Xms=64m
dalvik.vm.image-dex2oat-Xmx=64m

等号左边是属性的名字,等号右边是属性的值


属性的分类:



  • 一般属性:普通的 key-value 对,没有其他功能,系统启动后,如果修改了某个属性值(仅修改了内存中的值,未写入到文件),再重启系统,修改的值不会被保存下来,读取到的仍是修改前的值

  • 特殊属性

    • 属性名称以 ro 开头,那么这个属性被视为只读属性。一旦设置,属性值不能改变。

    • net 开头的属性,顾名思义,就是与网络相关的属性,net 属性中有一个特殊的属性:net.change,它记录了每一次最新设置和更新的 net 属性,也就是每次设置和更新 net,属性时则会自动的更新 net.change 属性,net.change 属性的 value 就是这个被设置或者更新的 net 属性的 name。例如我们更新了属性 net.bt.name 的值,由于 net 有属性发生了变化,那么属性服务就会自动更新 net.change,将其值设置为 net.bt.name

    • persist 为开头的属性值,当在系统中通过 setprop 命令设置这个属性时,就会在 /data/property/ 目录下会保存一个副本。这样在系统重启后,按照加载流程这些 persist 属性的值就不会消失了。

    • 属性 ctrl.startctrl.stop 是用来启动和停止服务。这里的服务是指定义在 rc 后缀文件中的服务。当我们向 ctrl.start 属性写入一个值时,属性服务将使用该属性值作为服务名找到该服务,启动该服务。这项服务的启动结果将会放入 init.svc.<服务名> 属性中,可以通过查询这个属性值,以确定服务是否已经启动。




如何读写属性:


命令行:


getprop "wlan.driver.status"
setprop "wlan.driver.status" "timeout"

Native 代码:


char buf[20]="qqqqqq";
char tempbuf[PROPERTY_VALUE_MAX];
property_set("type_value",buf);
property_get("type_value",tempbuf,"0");

Java 代码:


String navBarOverride = SystemProperties.get("qemu.hw.mainkeys");
SystemProperties.set("service.bootanim.exit", "0");

属性的作用


常见的属性文件的作用如下:



参考资料



作者:阿豪讲Framework
来源:juejin.cn/post/7298645450555326464
收起阅读 »

底部弹出菜单原来这么简单

底部弹出菜单是什么 底部弹出菜单,即从app界面底部弹出的一个菜单列表,这种UI形式被众多app所采用,是一种主流的布局方式。 思路分析 我们先分析一下,这样一种UI应该由哪些布局组成?首先在原界面上以一小块区域显示界面的这种形式,很明显就是对话框Dial...
继续阅读 »

底部弹出菜单是什么


底部弹出菜单,即从app界面底部弹出的一个菜单列表,这种UI形式被众多app所采用,是一种主流的布局方式。


截屏2023-09-28 14.36.51.png


截屏2023-09-28 14.37.29.png


思路分析


我们先分析一下,这样一种UI应该由哪些布局组成?首先在原界面上以一小块区域显示界面的这种形式,很明显就是对话框Dialog做的事情吧!最底部是一个取消菜单,上面的功能菜单可以是一个,也可以是两个、三个甚至更多。所以,我们可以使用RecyclerView实现。需要注意一点的是,最上面那个菜单的样式稍微有点不一样,因为它上面是圆滑的,有圆角,这样的界面显示更加和谐。我们主要考虑的就是弹出对话框的动画样式,另外注意一点就是可以多支持几个语种,让框架更加专业,这里只需要翻译“取消”文字。


开始看代码


package dora.widget

import android.app.Activity
import android.app.Dialog
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.widget.TextView
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.listener.OnItemChildClickListener
import dora.widget.bean.BottomMenu
import dora.widget.bottomdialog.R

class DoraBottomMenuDialog : View.OnClickListener, OnItemChildClickListener {

private var bottomDialog: Dialog? = null
private var listener: OnMenuClickListener? = null

interface OnMenuClickListener {
fun onMenuClick(position: Int, menu: String)
}

fun setOnMenuClickListener(listener: OnMenuClickListener) : DoraBottomMenuDialog {
this.listener = listener
return this
}

fun show(activity: Activity, menus: Array<String>): DoraBottomMenuDialog {
if (bottomDialog == null && !activity.isFinishing) {
bottomDialog = Dialog(activity, R.style.DoraView_AlertDialog)
val contentView =
LayoutInflater.from(activity).inflate(R.layout.dview_dialog_content, null)
initView(contentView, menus)
bottomDialog!!.setContentView(contentView)
bottomDialog!!.setCanceledOnTouchOutside(true)
bottomDialog!!.setCancelable(true)
bottomDialog!!.window!!.setGravity(Gravity.BOTTOM)
bottomDialog!!.window!!.setWindowAnimations(R.style.DoraView_BottomDialog_Animation)
bottomDialog!!.show()
val window = bottomDialog!!.window
window!!.decorView.setPadding(0, 0, 0, 0)
val lp = window.attributes
lp.width = WindowManager.LayoutParams.MATCH_PARENT
lp.height = WindowManager.LayoutParams.WRAP_CONTENT
window.attributes = lp
} else {
bottomDialog!!.show()
}
return this
}

private fun initView(contentView: View, menus: Array<String>) {
val recyclerView = contentView.findViewById<RecyclerView>(R.id.dview_recycler_view)
val adapter = MenuAdapter()
val list = mutableListOf<BottomMenu>()
menus.forEachIndexed { index, s ->
when (index) {
0 -> {
list.add(BottomMenu(s, BottomMenu.TOP_MENU))
}
else -> {
list.add(BottomMenu(s, BottomMenu.NORMAL_MENU))
}
}
}
adapter.setList(list)
recyclerView.adapter = adapter
val decoration = DividerItemDecoration(contentView.context, DividerItemDecoration.VERTICAL)
recyclerView.addItemDecoration(decoration)
adapter.addChildClickViewIds(R.id.tv_menu)
adapter.setOnItemChildClickListener(this)
val tvCancel = contentView.findViewById<TextView>(R.id.tv_cancel)
tvCancel.setOnClickListener(this)
}

private fun dismiss() {
bottomDialog?.dismiss()
bottomDialog = null
}

override fun onClick(v: View) {
when (v.id) {
R.id.tv_cancel -> dismiss()
}
}

override fun onItemChildClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) {
listener?.onMenuClick(position, adapter.getItem(position) as String)
dismiss()
}
}

类的结构不仅可以继承,还可以使用聚合和组合的方式,我们这里就不直接继承Dialog了,使用一种更接近代理的一种方式。条条大路通罗马,能抓到老鼠的就是好猫。这里的设计是通过调用show方法,传入一个菜单列表的数组来显示菜单,调用dismiss方法来关闭菜单。最后添加一个菜单点击的事件,把点击item的内容和位置暴露给调用方。


package dora.widget

import com.chad.library.adapter.base.BaseMultiItemQuickAdapter
import com.chad.library.adapter.base.viewholder.BaseViewHolder
import dora.widget.bean.BottomMenu
import dora.widget.bottomdialog.R

class MenuAdapter : BaseMultiItemQuickAdapter<BottomMenu, BaseViewHolder>() {

init {
addItemType(BottomMenu.NORMAL_MENU, R.layout.dview_item_menu)
addItemType(BottomMenu.TOP_MENU, R.layout.dview_item_menu_top)
}

override fun convert(holder: BaseViewHolder, item: BottomMenu) {
holder.setText(R.id.tv_menu, item.menu)
}
}

多类型的列表布局我们采用BRVAH,


implementation("io.github.cymchad:BaseRecyclerViewAdapterHelper:3.0.10")

来区分有圆角和没圆角的item条目。


<?xml version="1.0" encoding="utf-8"?>

<resources>
<style name="DoraView.AlertDialog" parent="@android:style/Theme.Dialog">
<!-- 是否启用标题栏 -->
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>

<!-- 是否使用背景半透明 -->
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:background">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">true</item>
</style>

<style name="DoraView.BottomDialog.Animation" parent="Animation.AppCompat.Dialog">
<item name="android:windowEnterAnimation">@anim/translate_dialog_in</item>
<item name="android:windowExitAnimation">@anim/translate_dialog_out</item>
</style>
</resources>

以上是对话框的样式。我们再来看一下进入和退出对话框的动画。


translate_dialog_in.xml


<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:duration="300"
android:fromXDelta="0"
android:fromYDelta="100%"
android:toXDelta="0"
android:toYDelta="0">

</translate>

translate_dialog_out.xml


<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:duration="300"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="0"
android:toYDelta="100%">

</translate>

最后给你们证明一下我是做了语言国际化的。
截屏2023-09-28 15.08.20.png


使用方式


// 打开底部弹窗
val dialog = DoraBottomMenuDialog()
dialog.setOnMenuClickListener(object : DoraBottomMenuDialog.OnMenuClickListener {
override fun onMenuClick(position: Int, menu: String) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
startActivity(intent)
}
})
dialog.show(this, arrayOf("外部浏览器打开"))

开源项目


github.com/dora4/dview…


作者:dora
来源:juejin.cn/post/7283516197487214611
收起阅读 »

从Kotlin中return@forEach了个寂寞

今天在Review(copy)同事代码的时候,发现了一个问题,想到很久之前,自己也遇到过这个问题,那么就来看下吧。首先,我们抽取最小复现代码。 (1..7).forEach { if (it == 3) { return@forEach...
继续阅读 »

今天在Review(copy)同事代码的时候,发现了一个问题,想到很久之前,自己也遇到过这个问题,那么就来看下吧。首先,我们抽取最小复现代码。


(1..7).forEach {
if (it == 3) {
return@forEach
}
Log.d("xys", "Num: $it")
}

�很简单的代码,我相信很多人都这样写过,实际上就是遍历的过程中,满足条件后就退出遍历,那么上面的代码,能实现这样的需求吗?我们来看下执行结果。


Num: 1
Num: 2
Num: 4
Num: 5
Num: 6
Num: 7

很遗憾,即使等于3之后就return了,但是然并卵,遍历依然继续执行了。相信很多写Kotlin的开发者都遇到过这个问题,其原因,还是在于语法的思维定势,我们在Kotlin的文档上,可以找到非常明确的解释。
kotlinlang.org/docs/return…


我们先来看下Kotlin中forEach的源码。


/**
* Performs the given [action] on each element.
*/

@kotlin.internal.HidesMembers
public inline fun Iterable.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}

�我们来提取下关键信息:



  • 内联函数

  • 高阶函数


发现了吗,由于高阶函数的存在,当你在高阶函数的闭包内「return」时,是结束的整个函数,当你使用「return@forEach�」时,是结束当前的闭包,所以,如果你像这样写:


(1..7).forEach {
if (it == 3) {
return
}
Log.d("xys", "Num: $it")
}

那么等于3之后,整个函数就被return了,那么如果你像文章开头这样写,那么等效于continue,因为你结束了当前的闭包,而这个闭包只是其中的一次遍历过程。那么我们要如何实现我们最初的需求呢?看到这样,答案其实已经呼之欲出了,那就是要return整个遍历的闭包。所以,官方也给出了解决方案,那就是外面套一层闭包:


run loop@{
(1..7).forEach {
if (it == 3) {
return@loop
}
Log.d("xys", "Num: $it")
}
}

写起来确实是麻烦一点,但这却是必不可少的过程,是引入闭包所带来的一点副作用。



当然这里不仅限于run,任何闭包都是可以的。


作者:xuyisheng
来源:juejin.cn/post/7243819009866235964
收起阅读 »

鸿蒙 AkrUI 零基础教程第一集

前言 各位同学有段时间没有见面 因为一直很忙所以就你没有去更新博客。最近有在学习这个鸿蒙的ark ui开发 因为鸿蒙不是发布了一个鸿蒙next的测试版本 明年会启动纯血鸿蒙应用 所以我就想提前给大家写一些博客文章 线性布局(Row/Column) 线性布局(L...
继续阅读 »

前言


各位同学有段时间没有见面 因为一直很忙所以就你没有去更新博客。最近有在学习这个鸿蒙的ark ui开发 因为鸿蒙不是发布了一个鸿蒙next的测试版本 明年会启动纯血鸿蒙应用 所以我就想提前给大家写一些博客文章


线性布局(Row/Column)


线性布局(LinearLayout)是开发中最常用的布局,通过线性容器RowColumn构建。线性布局是其他布局的基础,其子元素在线性方向上(水平方向和垂直方向)依次排列。线性布局的排列方向由所选容器组件决定,Column容器内子元素按照垂直方向排列,Row容器内子元素按照水平方向排列。根据不同的排列方向,开发者可选择使用Row或Column容器创建线性布局。这个比较像flutter里面线性布局 学过flutter的就比较容易理解


横向线性布局


image.png


纵向线性布局


image.png


基本概念



  • 布局容器:具有布局能力的容器组件,可以承载其他元素作为其子元素,布局容器会对其子元素进行尺寸计算和布局排列。

  • 布局子元素:布局容器内部的元素。

  • 主轴:线性布局容器在布局方向上的轴线,子元素默认沿主轴排列。Row容器主轴为横向,Column容器主轴为纵向。

  • 交叉轴:垂直于主轴方向的轴线。Row容器交叉轴为纵向,Column容器交叉轴为横向。

  • 间距:布局子元素的间距。


具体代码实现


横向线性布局


@Entry
@Component
struct Index {
build() {
Row() {
Column({ space: 20 }) {
Row().width('90%').height(50).backgroundColor(0xFF0000)
Row().width('90%').height(50).backgroundColor(0xFF0000)
Row().width('90%').height(50).backgroundColor(0xFF0000)
}.width('100%')
}
.height('100%')
}
}



image.png


纵向线性布局


@Entry
@Component
struct Index {
build() {
Row({ space: 35 }) {
Text('space: 35').fontSize(15).fontColor(Color.Gray)
Row().width('10%').height(150).backgroundColor(0xFF0000)
Row().width('10%').height(150).backgroundColor(0xFF0000)
Row().width('10%').height(150).backgroundColor(0xFF0000)
}.width('90%')
}
.height('100%')
}
}

image.png


布局子元素在交叉轴上的对齐方式


在布局容器内,可以通过alignItems属性设置子元素在交叉轴(排列方向的垂直方向)上的对齐方式。且在各类尺寸屏幕中,表现一致。其中,交叉轴为垂直方向时,取值为VerticalAlign类型,水平方向取值为HorizontalAlign
alignSelf属性用于控制单个子元素在容器交叉轴上的对齐方式,其优先级高于alignItems属性,如果设置了alignSelf属性,则在单个子元素上会覆盖alignItems属性。



  • HorizontalAlign.Start:子元素在水平方向左对齐


@Entry
@Component
struct Index {
build() {
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)

Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)

Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').alignItems(HorizontalAlign.Start).backgroundColor('rgb(242,242,242)')
}
}

image.png
HorizontalAlign.Center:子元素在水平方向居中对齐



@Entry
@Component
struct Index {
build() {
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)

Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)

Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').alignItems(HorizontalAlign.Center).backgroundColor('rgb(242,242,242)')
}
}

image.png



  • HorizontalAlign.End:子元素在水平方向右对齐



@Entry
@Component
struct Index {
build() {
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)

Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)

Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').alignItems(HorizontalAlign.End).backgroundColor('rgb(242,242,242)')
}
}

image.png


Row容器内子元素在垂直方向上的排列



  • VerticalAlign.Top:子元素在垂直方向顶部对齐。


@Entry
@Component
struct Index {
build() {
// VerticalAlign.Center:子元素在垂直方向居中对齐
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)

Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)

Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).alignItems(VerticalAlign.Top).backgroundColor('rgb(242,242,242)')

}
}

image.png



  • VerticalAlign.Center:子元素在垂直方向居中对齐



@Entry
@Component
struct Index {
build() {
// VerticalAlign.Bottom:子元素在垂直方向底部对齐。

Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)

Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)

Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).alignItems(VerticalAlign.Center).backgroundColor('rgb(242,242,242)')


}
}

image.png



  • VerticalAlign.Bottom:子元素在垂直方向底部对齐


@Entry
@Component
struct Index {
build() {
// VerticalAlign.Bottom:子元素在垂直方向底部对齐
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)

Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)

Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).alignItems(VerticalAlign.Bottom).backgroundColor('rgb(242,242,242)')
}
}

image.png


布局子元素在主轴上的排列方式


在布局容器内,可以通过justifyContent属性设置子元素在容器主轴上的排列方式。可以从主轴起始位置开始排布,也可以从主轴结束位置开始排布,或者均匀分割主轴的空间


Column容器内子元素在主轴上的排列


justifyContent(FlexAlign.Start):元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐



@Entry
@Component
struct Index {
build() {
//justifyContent(FlexAlign.Start):元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)

Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)

Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Start)
}
}

image.png


justifyContent(FlexAlign.Center):元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同。



@Entry
@Component
struct Index {
build() {

//justifyContent(FlexAlign.Center):元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)

Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)

Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Center)
}
}

image.png



  • justifyContent(FlexAlign.End):元素在主轴方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐


@Entry
@Component
struct Index {
build() {
//justifyContent(FlexAlign.Spacebetween):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)

Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)

Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.End)

}
}

image.png
justifyContent(FlexAlign.Spacebetween):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐。



@Entry
@Component
struct Index {
build() {
//justifyContent(FlexAlign.Spacebetween):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐

Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)

Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)

Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceBetween)
}
}

image.png


justifyContent(FlexAlign.SpaceAround):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。


@Entry
@Component
struct Index {
build() {
//justifyContent(FlexAlign.SpaceAround):主轴方向均匀分配元素,相邻元素之间距离相同。
// 第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。


Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)

Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)

Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceAround)


}
}

image.png


justifyContent(FlexAlign.SpaceEvenly):主轴方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。


@Entry
@Component
struct Index {
build() {
//justifyContent(FlexAlign.SpaceEvenly):主轴方向均匀分配元素,
// 相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样
Column({}) {
Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)

Column() {
}.width('80%').height(50).backgroundColor(0xD2B48C)

Column() {
}.width('80%').height(50).backgroundColor(0xF5DEB3)
}.width('100%').height(300).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceEvenly)


}
}

image.png


Row容器内子元素在主轴上的排列


justifyContent(FlexAlign.Start):元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐



@Entry
@Component
struct Index {
build() {
//justifyContent(FlexAlign.Start):元素在主轴方向首端对齐,第一个元素与行首对齐,同时后续的元素与前一个对齐。
Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)

Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)

Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Start)



}
}

image.png


justifyContent(FlexAlign.Center):元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同



@Entry
@Component
struct Index {
build() {
// justifyContent(FlexAlign.Center):元素在主轴方向中心对齐,第一个元素与行首的距离与最后一个元素与行尾距离相同

Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)

Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)

Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.Center)

}
}

image.png


justifyContent(FlexAlign.Spacebetween):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐。



@Entry
@Component
struct Index {
build() {
// justifyContent(FlexAlign.End):元素在主轴方向尾部对齐,最后一个元素与行尾对齐,其他元素与后一个对齐。

Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)

Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)

Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.End)

}
}

image.png


justifyContent(FlexAlign.Spacebetween):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐


@Entry
@Component
struct Index {
build() {

//justifyContent(FlexAlign.Spacebetween):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素与行首对齐,最后一个元素与行尾对齐


Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)

Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)

Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceBetween)
}
}

image.png


justifyContent(FlexAlign.SpaceAround):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半。



@Entry
@Component
struct Index {
build() {
//justifyContent(FlexAlign.SpaceAround):主轴方向均匀分配元素,相邻元素之间距离相同。第一个元素到行首的距离和最后一个元素到行尾的距离是相邻元素之间距离的一半

Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)

Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)

Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceAround)


}

}

image.png


justifyContent(FlexAlign.SpaceEvenly):主轴方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样


@Entry
@Component
struct Index {
build() {
//justifyContent(FlexAlign.SpaceEvenly):主轴方向均匀分配元素,相邻元素之间的距离、第一个元素与行首的间距、最后一个元素到行尾的间距都完全一样。

Row({}) {
Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)

Column() {
}.width('20%').height(30).backgroundColor(0xD2B48C)

Column() {
}.width('20%').height(30).backgroundColor(0xF5DEB3)
}.width('100%').height(200).backgroundColor('rgb(242,242,242)').justifyContent(FlexAlign.SpaceEvenly)
}

}

image.png


最后总结


arkui 写法和flutter非常的像 有兴趣的同学可以多尝试哈 今天的文章就讲到这里
。最后呢 希望我都文章能帮助到各位同学工作和学习 如果你觉得文章还不错麻烦给我三连 关注点赞和转发 谢谢


作者:xq9527
来源:juejin.cn/post/7301242165279047707
收起阅读 »

Android设置IPV4优先、httpdns使用

Android设置IPV4优先、httpdns使用 前言 最近接了个比较奇怪的BUG,就是服务器开了IPV6之后,部分安卓手机会访问不了,或者访问时间特别久,查了下是DNS会返回多个IP,但是IPV6地址会放在前面,比如: [ms.bdstatic.com/2...
继续阅读 »

Android设置IPV4优先、httpdns使用


前言


最近接了个比较奇怪的BUG,就是服务器开了IPV6之后,部分安卓手机会访问不了,或者访问时间特别久,查了下是DNS会返回多个IP,但是IPV6地址会放在前面,比如:


[ms.bdstatic.com/240e:95d:801:2::6fb1:624, ms.bdstatic.com/119.96.52.36]

然后取域名的时候默认会取第一个IP,然后就蛋疼了,有的机型、系统、运行商、路由器都可能不支持IPV6,然后访问不了。 由于iOS是没问题的,剩下来的肯定是Android的问题了。


于是我花了些时间看了看,做了个IPV4优先方案(还没用到生产环境),测试了下可行性,顺便又学了下httpdns的使用,这里记录下。


核心思路


网上找了资料,解决办法都是通过okhttp的自定义DNS去处理的(可以用Interceptor,不推荐),这个也是解决办法的核心:


class MyDns : Dns {
@Throws(UnknownHostException::class)
override fun lookup(hostname: String): List<InetAddress> {
return try {
val inetAddressList: MutableList<InetAddress> = ArrayList()
val inetAddresses = InetAddress.getAllByName(hostname)
Log.d("TAG", "lookup before: ${Arrays.toString(inetAddresses)}")
for (inetAddress in inetAddresses) {

// 将IPV4地址放到最前面
if (inetAddress is Inet4Address) {
inetAddressList.add(0, inetAddress)
} else {
inetAddressList.add(inetAddress)
}
}
Log.d("TAG", "lookup after: $inetAddressList")
inetAddressList
} catch (var4: NullPointerException) {
val unknownHostException = UnknownHostException("Broken system behavior")
unknownHostException.initCause(var4)
throw unknownHostException
}
}
}

上面自定义了一个DNS,里面的lookup就是okhttp查找DNS的逻辑,前面我okhttp源码的文章也有说到,默认会取第一个inetAddress,下面看下如何使用:


val client = OkHttpClient.Builder()
.dns(DnsInterceptor.MyDns())
.build()

// 异步请求下百度
client.newCall(Request.Builder().url(originalUrl).build()).enqueue(
object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.d("TAG", "onFailure: ")
}

override fun onResponse(call: Call, response: Response) {
Log.d("TAG", "onResponse: $response")
}
}
)

看下log,第一个是我WiFi访问的,不支持IPV6,第二个是我用iPhone开热点访问的,支持IPV6:
dd.png


cc.png


ps. Android手机可以设置使用IPV6:



华为手机: 设置->移动网络->移动数据->接入点名称(APN)->新建一个APN,配置中的APN协议及APN漫游协议设置为仅ipv4或ipv6.



WebView内使用


okhttp好办,可是我们APP是套壳webView的,Android请求不多,大部分还是HttpURLConnection的,HttpURLConnection找了资料也不太好改,还不如改逻辑换成okhttp,但是webView就没得办法了。


好在API-21后,WebViewClient提供了新的shouldInterceptRequest方法,可以让我们代理它的请求操作,不过有很多限制操作。


shouldInterceptRequest方法


先来看下shouldInterceptRequest方法,它要求API大于等于21:


binding.webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest
)
: WebResourceResponse? {
// ...
}
}

方法会提供一个request携带一些请求信息,要求我们返回一个WebResourceResponse,将代理的请求结果封装进去。鸡肋的就是这两个类东西都不多,会限制我们的代理功能:
dd.png


image.png


功能封装


这里我把代理功能封装了一下,可能还有问题,请谨慎参考:


import android.os.Build
import android.text.TextUtils
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import okhttp3.Dns
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.Inet4Address
import java.net.InetAddress
import java.net.UnknownHostException
import java.nio.charset.Charset
import java.util.Arrays

object DnsInterceptor {

/**
* 设置okhttpClient
*/

lateinit var client: OkHttpClient

/**
* 拦截webView请求
*/

fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest
)
: WebResourceResponse? {
// WebResourceRequest Android6.0以上才支持header,不支持body所以只能拦截GET方法
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& request.method.lowercase() == "get"
&& (request.url.scheme?.lowercase() == "http"
|| request.url.scheme?.lowercase() == "https")) {

// 获取头部
val headersBuilder = Headers.Builder()
request.requestHeaders.entries.forEach {
headersBuilder.add(it.key, it.value)
}
val headers = headersBuilder.build()

// 生成okhttp请求
val newRequest = Request.Builder()
.url(request.url.toString())
.headers(headers)
.build()

// 同步请求
val response = client.newCall(newRequest).execute()

// 对于无mime类型的请求不拦截
val contentType = response.body()?.contentType()
if (TextUtils.isEmpty(contentType.toString())) {
return null
}

// 获取响应头
val responseHeaders: MutableMap<String, String> = HashMap()
val length = response.headers().size()
for (i in 0 until length) {
val name = response.headers().name(i)
val value = response.headers().get(name)
if (null != value) {
responseHeaders[name] = value
}
}

// 创建新的response
return WebResourceResponse(
"${contentType!!.type()}/${contentType.subtype()}",
contentType.charset(Charset.defaultCharset())?.name(),
response.code(),
"OK",
responseHeaders,
response.body()?.byteStream()
)
} else {
return null
}
}

/**
* 优先使用ipv4
*/

class MyDns : Dns {
@Throws(UnknownHostException::class)
override fun lookup(hostname: String): List<InetAddress> {
return try {
val inetAddressList: MutableList<InetAddress> = ArrayList()
val inetAddresses = InetAddress.getAllByName(hostname)
Log.d("TAG", "lookup before: ${Arrays.toString(inetAddresses)}")
for (inetAddress in inetAddresses) {
if (inetAddress is Inet4Address) {
inetAddressList.add(0, inetAddress)
} else {
inetAddressList.add(inetAddress)
}
}
Log.d("TAG", "lookup after: $inetAddressList")
inetAddressList
} catch (var4: NullPointerException) {
val unknownHostException = UnknownHostException("Broken system behavior")
unknownHostException.initCause(var4)
throw unknownHostException
}
}
}
}

把大部分操作封装到一个单例类去了,然后在webView使用的时候就可以这样写:


// 创建okhttp
val client = OkHttpClient.Builder().dns(DnsInterceptor.MyDns()).build()
DnsInterceptor.client = client

// 配置webView
val webSettings = binding.webView.settings
webSettings.javaScriptEnabled = true //启用js,不然空白
webSettings.domStorageEnabled = true //getItem报错解决
binding.webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest
)
: WebResourceResponse? {
try {
// 通过okhttp拦截请求
val response = DnsInterceptor.shouldInterceptRequest(view, request)
if (response != null) {
return response
}
}catch (e: Exception) {
// 可能有异常,发生异常就不拦截: UnknownHostException(MyDns)
e.printStackTrace()
}
return super.shouldInterceptRequest(view, request)
}
}

binding.button.setOnClickListener {
binding.webView.loadUrl(binding.ip.text.toString())
}

试了下,访问百度没啥问题


存在问题


上面方法虽然代理webView去发请求了,不过这里有好多限制:



  1. 需要API21以上,大部分机型应该满足

  2. 只能让GET请求优先使用IPV4,其他请求方法改不了

  3. 不支持MIME类型为空的响应

  4. 不支持contentType中,无法获取到编码的非二进制文件请求

  5. 不支持重定向


网上文章比较少,有几篇我看还都差不多,最后一对比,竟然是阿里云httpdns里面的说明,这里我也不太详叙了,看下文章吧:


Android端HTTPDNS+Webview最佳实践


HTTPDNS使用


上面修改DNS顺序的操作,实际和HTTPDNS的思路是一样的,看到相关内容后,触发了我知识的盲区,觉得还是有必要去学一学的。


HTTPDNS的作用就是代替本地的DNS解析,通过http请求访问httpdns的服务商,先拿到IP,再发起请求,可以防劫持,并且更快,当然这都是我简单的理解,可以看下阿里对它产品的介绍:



help.aliyun.com/document_de…



阿里HTTPDNS


这里我是选的阿里的httpdns服务,开通方式还是看他们自己的说明吧,不是很复杂: 服务开通


下面就来看如何使用,首先是添加依赖:


// setting.gradle.kts中
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven{ url = uri("./catalog_repo") }
maven {
url = uri("http://maven.aliyun.com/nexus/content/repositories/releases/")
name = "aliyun"
//一定要添加这个配置
isAllowInsecureProtocol = true
}
}
}

// 要使用的module中
implementation 'com.aliyun.ams:alicloud-android-httpdns:2.3.2'

这里是kts的依赖,groovy语法类似,gradle7.0以下甚至加个url就行。


再来看下具体使用,我在阿里云的后台配置了百度的域名(”http://www.baidu.com“),这里就来请求百度的IP:


val httpdns = HttpDns.getService(getContext(), "xxx")
// 预加载
httpdns.setPreResolveHosts(ArrayList(listOf("www.baidu.com")))

val originalUrl = "http://www.baidu.com"
val url = URL(originalUrl)
val ip = httpdns.getIpByHostAsync(url.host)
Log.d("TAG", "httpdns get init: ip = $ip")

这样使用我这直接就失败了,拿到的ip为null,所以初始化的操作应该要提前一点做:


// 点击事件
binding.button.setOnClickListener {
val ipClick = httpdns.getIpByHostAsync(url.host)
val ipv6 = httpdns.getIPv6sByHostAsync(url.host).let {
if (it.isNotEmpty()) return@let it[0]
else return@let "not get"
}
Log.d("TAG", "httpdns get: ip = $ipClick, ipv6 = $ipv6")
}

后面我把获取操作放到点击事件里面,就没问题了,也能拿到IPV6地址:
dd.png


这里要注意下,如果切换网络,IPV6的地址会有缓存,谨慎使用吧(网络可能不支持了):
dd.png


httpdns的使用应该算网络优化了吧,看别人文章说dns查找域名有的要几百毫秒,用httpdns可能只要一百毫秒,有机会来研究研究源码^_^


小结


稍微总结下吧,这篇文章分析了一下IPV6在Android上出错的原因,实践了下IPV4优先的思路,并且对webView做了支持,还研究了下httpdns的使用。


作者:方大可
来源:juejin.cn/post/7301573790342414351
收起阅读 »

IT界惊现文豪!华为领导及阿里P10遭吐槽!

来源:网络一篇奇文出现在某匿名社交软件,引起了大家对文豪的赞口不绝。先发原图:一遍好文一定少不了精彩评论写这么好,应该不是偶然,原来这位文豪之前也有关于P10的大作,分享给各位小伙伴以上只是工作之余的一点乐趣,仅供娱乐。真正的P10,还确实是挺厉害的,只不过他...
继续阅读 »

来源:网络

一篇奇文出现在某匿名社交软件,引起了大家对文豪的赞口不绝。

先发原图:

一遍好文一定少不了精彩评论

写这么好,应该不是偶然,

原来这位文豪之前也有关于P10的大作,分享给各位小伙伴

以上只是工作之余的一点乐趣,仅供娱乐。

真正的P10,还确实是挺厉害的,只不过

他的厉害,懂得人并不多,

因为懂得人至少也得P9!

作者:程序员直聘
来源:mp.weixin.qq.com/s/Fw0s7uE76a2h2opHRGVUfg

收起阅读 »

技术人该如何准备晋升答辩?

前言 大家好,我是路由器没有路。今天跟大家聊下关于技术人该如何准备晋升答辩的话题。 每到年中或者年底,都会有一波晋升答辩潮。所以在这个时间点,想跟大家聊聊我的个人经验,以及一些对技术人该如何准备晋升的一些启发。 在公司里,我曾参与过各个职级的晋升答辩,也见到过...
继续阅读 »

前言


大家好,我是路由器没有路。今天跟大家聊下关于技术人该如何准备晋升答辩的话题。


每到年中或者年底,都会有一波晋升答辩潮。所以在这个时间点,想跟大家聊聊我的个人经验,以及一些对技术人该如何准备晋升的一些启发。


在公司里,我曾参与过各个职级的晋升答辩,也见到过各种各样的答辩现场。就在前阵子,公司部门刚结束了年中职级晋升答辩,我也花了不少时间在团队成员的答辩辅导上。今天我就把一些晋升答辩的技巧和常见的坑跟大家说说,希望能够在晋升之路上对你有所启发。


争取获得答辩机会


现在很多公司都有完善的员工职级晋升管理制度。职级晋升的答辩当然是少不了的了。既然是答辩,就会涉及答辩内容及现场答辩发挥,每一个环节都不容忽视。


也有一些公司的年中或年度晋升是需要在时间范围内先自主提报的,也就是说每个人都有机会,但有些公司是按提名制的,需要你自己去争取答辩机会。下面我们就来说说怎样才能获得提名资格:



  • 自身能力能够达到了下一个职级的要求。但有些人可能会认为,要晋升了之后才需要具备下一个职级的能力,这观点是不正确的。

  • 公司对人才的要求具备高度的确定性。通常不会冒太大风险去晋升能力不确定的人。因此,你可以提前参考目标职级的要求或同事,关注他们的技术深度和业务能力。

  • 主动找领导沟通,确定努力发展方向。对发展方向制定可落地的措施。其实上级对于希望得到成长的员工都是非常欢迎的。



接下来,在争取获得答辩晋升机会后,就可以着手为答辩做准备了。


准备答辩素材


说到述职晋升答辩,当然少不了一份晋升汇报的 PPT。那么该如何准备晋升答辩素材呢?内容当然是包含近一年来的工作成果。


在答辩时,晋升答辩评委通常是跨团队或跨部门的领导。他们往往是不了解你工作成果的业务背景和技术实现细节,因此你需要在短时间内将业务背景、工作成果介绍清楚,这对不善演讲的技术人来说确实有比较大的挑战。


有人可能会说,平时是不是需要记录项目素材呀,我只能说,关系不太大。答辩素材是需要你去实践并产生的,而并非是靠记录。所以我建议,如果有机会,要尽可能多的去参与比较有挑战的项目建设,当然这可能开发难度较大、工作量大、比较累,但相比简单的项目则更容易创造价值、得到收获。


当然如果你没有参与过这种项目,那么你也可以对项目或者线上问题的点作技术深挖


比如线上有这么个问题:经常性的发生 CPU 占用突然飙高,停顿一两秒后又恢复正常。或者内存间歇性的发生 OOM 了。虽然这对业务影响不算大,很多人可能也不会在意和处理这种问题,最多重启下服务,恢复正常就好,但是如果你去深挖问题背后产生的原因,找到问题的根源和涉及的底层技术点,并在团队内部给大家分享。


这就是很有价值的内容,因为你不仅主动的解决了看似不起眼问题,而且还通过分享的方式让其他同事也明白其中的原因,帮助了其他同学的成长。


根据素材,编写答辩 PPT


在上面准备了答辩素材案例之后,接下来需要根据准备好的素材,编写答辩素材 PPT。这里有几个需要遵循的原则:



  1. 在讲述结果的同时,需要把问题点和解决方式也讲清楚。比如在这一年里,你负责了一个大型项目,并成功完成了上线。切记在 PPT 里花大篇幅介绍项目是什么,以及项目成功上线这一结果。因为评委无法通过结果评估你的能力和价值。所以,在介绍素材时,首先要介绍背景,然后介绍这个项目案例中存在哪些问题,你是如何解决的?

  2. 结果要有价值和数据体现。说到结果时,很多人习惯讲解项目如期上线等内容,但在评委看来,这是基本的要求,并不是加分项。正确的做法是通过一些上线后的数据说话。比如介绍上线后的系统性能、数据质量等相关内容。这里强调一点,很多研发同学习惯写上线后的一些业务数据。如新增用户数带来的金额、收入等,这类数据其实与产品、业务同学联系更紧密一些。研发应该更多的把关注点应放在技术层面上。

  3. 素材要匹配晋升的职级定位。像只有苦劳的内容,比如在赶工期的项目里,加班加点的保证他如期上线,且获得了领导认可等,建议不要写,原因很简单,就是不能体现你的技术价值,这些活,说不好听点,刚毕业的同学都可以完成。



晋升答辩素材 PPT 的一些建议


这里和大家分享下编写晋升答辩 PPT 的一些建议,可以参考一下:



  1. PPT 的基本格式要统一。答辩的 PPT 的内容不要太过绚丽。除了要保证基本的工整,细节也很重要,一定要注意审查错字。有些评委会认为错字多,可能写代码 bug 也多。还要注意统一字号,不要一页字大一页字小。此外,还要避免过多动画。注意控制字数,重要的内容标红、加粗。答辩一般都是集中评审,评委一天要评审,很多人没有耐心看太多字。把你想要表达的重点内容标红加粗,这样评委才能快速吸收。

  2. 不要放一张大而全的架构图。很多同学都习惯在 PPT 里放一张大家全的架构图。但在答辩时,你只讲解了其中的一小部分。你可能会认为大而全的架构图可以彰显自己系统的完善性。但如果你只讲解了其中一二,其实是很难讲出价值内容,毕竟时间有限,反而容易给评委留下浮于表面的形象。你可以对自己负责实现的那个模块单独拿出来讲,可以采用更优雅的方式进行展现,就具体问题的架构,加上细节问题描述,代替大而全的架构图,才能让评委快速了解问题的背景和解决方法,进而更好的评判你到底做的好还是不好。

  3. PPT 上不要露马脚。我曾经遇到过答辩人在 PPT 中写了幂等两个字,我想他写出来的目的是想表示如何实现它,但评委一直对这个点穷追不舍,导致答辩人最终答辩挂了。你写在 PPT 上的每一个字都需要十分熟悉,每一个内容都可能是地雷。


要写答辩稿并加以练习


写完 PPT 后,在正式答辩前的这段时间里,可以不断的加强练习。在练习时也有以下几个建议:



  1. 写出答辩稿。很多人没有写答辩稿的习惯,撰写文字稿能够帮助你发现答辩的逻辑是否通畅,还能够框定你的表达内容。咱们前面提到过,答辩是有时间限制的,如果没有固定的稿子,每次发挥的时长都不一样,最后很容易因为超时影响答辩效果。

  2. 你要准确按照答辩的时间完整练习至少十遍以上,注意是完整练习。非完整的零碎练习和完整的练习节奏差别非常大。

  3. 做预答辩。不同公司的晋升评委组成不同,有的是管理者,有的是技术专家,还有的是管理者加技术专家。因此,在做预答辩练习时,最好邀请相对应的人员辅导你答辩,比如选择你的 leader,相信部门 leader 还是比较乐意的。


调整答辩心态


在答辩将至时,大家都会跟你说,答辩时不要紧张,会影响发挥。但过来人都知道,不紧张好像太难了。下面就说说具体有什么方式可以抑制紧张。



  • 首先还是上一节的内容,答辩稿必须写出来,跟着稿子来。人在紧张时,智力和反应能力会呈指数级下滑。没有预先练习,顺溜的稿子你是临场发挥不出来的,只能满嘴跑火车或者照着 PPT 念,效果会大打折扣。

  • 一个能够避免紧张的好办法是做最坏假设。紧张是因为想通过晋升,你可以想象一下此次晋升没有通过的场景,你会怎样对待这个答辩结果,如果最坏的结果你都接纳了,还有什么是不能面对的呢?这也能反过来激励你好好准备。

  • 回答问题要言简意赅。一般答辩时都会设提问环节,很多时候答辩演讲的很好,但回答差,也会被一票否决,这种情况就比较可惜。作为过来人,我在这里给你提个醒,评委比较喜欢回答问题,言简意赅,直达重点的人印象分都比较高,你可以换位思考一下,评委提了一天的答辩,理解能力也下降的厉害,如果你半天收不到点子上,评委可能会认为你知识储备和逻辑能力薄弱。

  • 不会的问题不要直接回答不知道。如果评委临场抛出了一个较难的问题,你可以短暂思考,尝试从几个角度简单回答一下,一定不要直接回答不知道。


总结


最后用一句话总结下:有些人工作五年,但只有一年的经验,但有些人工作一年却拥有了五年经验,那是因为一直在学习、思考和总结


希望这篇文章能够对你在晋升道路上有所帮助。


作者:路由器没有路
来源:juejin.cn/post/7243248407535468600
收起阅读 »

如何做大促压测

一.背景&目标 1.1 常见的压测场景 电商大促:一众各大厂的促销活动场景,如:淘宝率先推出的天猫双11,而后京东拉出的京东 618 .还是后续陆陆续续的一些年货节, 3.8 女神节等等.都属于一些常规的电商大促 票务抢购:常见的如承载咱们 8...
继续阅读 »

一.背景&目标


1.1 常见的压测场景




  • 电商大促:一众各大厂的促销活动场景,如:淘宝率先推出的天猫双11,而后京东拉出的京东 618 .还是后续陆陆续续的一些年货节, 3.8 女神节等等.都属于一些常规的电商大促




  • 票务抢购:常见的如承载咱们 80,90 青春回忆的 Jay 的演唱会,还有普罗大众都参与的 12306 全民狂欢抢票.




  • 单品秒杀:往年被小米抢购秒杀带起来的红米抢购,还有最近这几年各大电商准点的茅台抢购;过去这三年中抢过的口罩,酒精等.这都属于秒杀的范畴.




  • toB 私有化服务:这个场景相对特殊.但是随着咱们 toC 的业务饱和,很多软件服务商也开始做 toB 的业务. toB 的业务特点其中有一个相对比较特别的就是存在私有化部署的诉求.主要的一些目的也是基于一些数据安全,成本这些因素来考虑的.




如上是在工作过程接触到的一些场景,书不尽言.下面就针对这些场景做一个压测的的梳理.


1.2 目标


  稳是第一位的,不久前某猫厂云事故,以及刚出现的某雀文档事故,历历在目.从大了说,整个产品的公信力被质疑将是后续用户是否持续购买的最大障碍;往小了说咱们这些小兵严重就是直接被离职,直接决定房贷,车贷下个月能不能交上的事情.所以除了稳,我们没别的.


WX20231115-101734@2x.png


  那其实从实际场景来说,除了稳定性是我们要求的第一位.还有一个整体的成本也是常用来被考虑的.所以压测的目标就是在稳定性和成本中间尽可能做一个权衡.


  如上在这些场景中前三的这种场景优先都是以稳定性是第一位,特别是电商大促,涉及的流程和各模块繁杂.在具体实施的过程中尽可能的去保证稳定性,资源优先度可以先往后放一放.


  其中稳定性的部分.我理解有两个部分.首先是面对峰值流量的时候的稳定性,一个是整个系统全链路的系统业务流程的稳定性.如:整体的交易的黄金流程.保证从用户的商详,购物车,结算,订单,支付都能够完整的走下来,这是业务流程的稳定性.


  最后一个私有化的场景相对比较特殊,更多的是一个私域的流量场景,流量相比公域要少的多.这时候尽可能要去压榨机器的性能,在尽可能少的资源成本下去提供更多的流量支持.因为成本就直接面临了产品的竞争力.


二.流程


    将流程划分为三个阶段压测前的一些前置准备;压测进行过程中的主要是测试和研发的具体的配合操作,以及监控观测;压测后的一些结果沉淀以及复盘,优化,复压.


2.1 压测前


2.1.1 流量预估


    这个是压测前第一项工作也是非常重要的一项工作,直接决定了本次压测的一个目标,而目标的准确制定就决定了本次的压测的最终目的---保证大促的稳定的直接成功与否.所以这里的流量预估显得非常重要.一般来说的话常用的有这两种形式.




  • 流量同比规则粗估


    如: 2012年6月1日 42w(qps) , 2013年6月1日 24w(qps) .同比下滑 42% .在得到 2012年11月1日 49w(qps) .以此推算 2013年11月1日 49w*0.57=28w .这是一个大概的量,如果压测的话按照这个量上浮 20% .压测按照 28*1.2= 34(w).




  • GMV 原则预估




从业务侧拿到2013年11月1日 11.11dau 的预估的量. 比如: dau 相比 618 的增长 1.2 倍.从监控里得到 618 的查车的量 20w ,占比 40% .得到整体流量为 50w. 得到 11.11 整体的量 50w*1.2 得到整体双 11 的量为 60w . 如果压测的话按照这个量上浮 ** 20%** .压测按照 60*1.2=72(w)
.


2.1.2 限流对齐以及配置


  限流毋庸置疑都是需要配置的,防止系统在承载能力之外的流量冲击下直接崩溃,造成xue'peng


2.1.2.1 限流配置原则


在整个流量预估完成之后,各模块基本上可以基于所域系统服务在流量预估的数值来进行设置.来保证峰值以上的一些突发情况也能够在系统承受范围.


2.1.2.2 限流的配置



  • 单机维度


一般单机房维度设置限流有两个方面. cpu 维度和 qps 维度.



  • 机房维度


每个机房的压测流量不一样,如张北,中云信.需要根据机房来进行限流配置,因为一般场景下优先保障同机房调用.


2.1.2.3 机器配置



  • 单机核心配置


机器配置.16c32g 50G SAS硬盘. SAS [既有的机械硬盘升级]


export maxParameterCount="10000"
export acceptCount="1000"
export maxSpareThreads="750"
export maxThreads="1000"
export minSpareThreads="50"
export URIEncoding="UTF-8"
export JAVA_OPTS=" -Xms16384m -Xmx16384m -XX:MaxMetaspaceSize=512m -XX:MetaspaceSize=512m -XX:ConcGCThreads=4 -XX:ParallelGCThreads=16 -Djava.library.path=/usr/local/lib -server -Xmn4096m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses -XX:+CMSClassUnloadingEnabled -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75 -XX:+CMSScavengeBeforeRemark "


  • 集群机房资源配比及配置


2.1.2.4 监控配置


监控配置主要分两个方面.
本身系统的机器的物理监控.
主要的指标[ CPU 使用率, load 负载.内存使用率,磁盘使用率, TCP 重传,连通性.].示例如下:
在这里插入图片描述



  • 接口服务监控.主要指标.



调用次数(秒级,分钟级),平均响应时长,TP99,TP999,可用率.示例如下:
在这里插入图片描述


核心的监控面板:


1.自身系统依赖的服务接口监控面板.
2.常见上游/自身/下游error状态码监控面板.
3.自身系统核心接口监控面板


2.1.3 流量切割



  • 入口流量切割


  从域名到压测机器的流量,保证生产环境和压测环境进行流量切分



  • *DB *流量切割


  一般通过识别压测上下文指标的路由标,来判定是否需要重新切换数据源.这个技术很常见.常见的做法就是通过 AbstractRoutingDataSource 的重写来实现 determineCurrentLookupKey 方法来切换数据源.动态数据源切割.压测的数据源一般会重新 copy 一遍现有的数据库 schema 建立一个影子库,保证线上数据不受影响,有时候为了压测还需要进行一些线上数据的一些冲入,保证测试场景的完整进行.



  • MQ 流量切割


  主要是消费和发送都需要增加识别压测标来进行消息的发送和消息的消费.如:原有 topic .rd_product_add ,通过识别 isForceBot

标来增加 rd_product_add_shadow .



  • cache 流量切割


  方案基本同上.通过识别标来具体使用具体的 cacheClient 不同.



  • 其他的中间件具体改造


如: es,ck,blink 等.


   如上的流量切割后要进行小流量的试跑来保证改造的方案是可行的.防止出现压测过程的流量逃逸.影响线上真实的环境,污染生产数据等.


2.1.4 压测前的机器状态检查


   这一步主要是 check 机器指标异常的,主要指标有 CPU, 硬盘, 内存, 连通性.防止一些特别的机器造成压测一直压不上去.出现指标异常的机器进行流量摘除的处理或者重启能消除隐患也可以继续使用.


2.1.5 测试的数据&脚本准备



  • 数据准备


  这里的数据准备要充分的模拟生产的环境数据,例如:加车的数据多样性每个维度都要充分的添加到.常见的加车数量6-10.
常见的重要的生产数据模拟.用户数据,订单数据,产品数据,购物车数据.



  • 脚本
      要保证基本的用例case能通


2.2 压测中


2.2.1 单场景压测


特定的场景压测,比如商详.这种场景下的压测因为是单场景的,所以在压测过程中不能够按照打满的场景去操作.比如说:整体商详压测的目标机器 cpu 目标是 60% .单场景的时候可能要留一些 buffer 去给全链路的场景做一些预留.


2.2.2 全链路压测


2.2.3 故障演练


通过演练做到面对故障时的响应机制.目标:完成3分钟内发现,5分钟内应急处理.10分钟定位原因.
大致分为这几个方面.


2.2.3.1 系统及硬件


系统方面涉及: CPU ,硬盘, TCP 重传,内存,磁盘可用率.
JVM :频繁 GC ,高频 YGC .
应对预案:快速通过监控平台完成具体IP机器定位,通过IP摘除流量完成,机器流量下线.通知运维定位原因. JVM 相关 DUMP 响应日志进行分析.


2.2.3.1 中间件相关演练


  在服务中间件出现异常时系统能够正常提供服务,对应接口的指标能够满足目标要求.常见的中间件故障.
存储类: ES,DB,cache.
中间件: MQ
应对预案:中间件能够做到手动预案热备数据源切换,缓存中间件降级. MQ 停止消费等.


2.2.3.2 上下游服务异常演练


  通过观察上下游服务监控面板快速定位上下游接口超时.
应对预案:非核心链路接口,主动通过开关进行降级.核心链路接口快速联系上下游进行相关原因排查.


2.6 限流演练



  • 单机限流演练
      在日常qps 平均值的前提上浮一些,保证生产的正常流量能够进行正常访问而不会触发限流.

  • 集群演练


2.3 压测后



  • 压测后机器挂载流量回切

  • 压测复盘


2.3.1 压测优化



  • 代码优化

  • 资源扩缩容

  • 针对场景复压测


2.3.2 压测其他收官



  • 完成压测报告

  • 沉淀操作手册

  • 沉淀压测记录

  • 动态扩缩容规则确认,资源确认

  • 流量回切


   如果在整个压测过程中是使用的同样的生产环境,保证压测后机器及时归还线上.避免影响线上集群性能和用户体验.


三.压测中遇到的问题


3.1 硬件相关


   首先定位具体硬件 IP 地址,优先进行流量摘取.出现大面积故障时同时保留现场同时立即联系运维同学协助排查定位.


3.2 接口相关


   首先通过接口监控得到相关接口的tp99avg,观测到实际的接口耗时已经影响主接口的调用时,进行主动的开关降级做到不影响主接口和核心逻辑.


3.3 其他



  • tomcat 6 定期主动回收问题
    tomcat6.0.33为防止内存泄露周期性每 1 小时触发 1 次System.gc(),导致tp周期性波动。tomcat源码JreMemoryLeakPreventionListener fullgc触发位置:
    在这里插入图片描述
    修复方案:从fullgc平均耗时200ms左右来看,fullgc耗时引发接口超时导致图文详情h5超时风险较小。计划618后升级tomcat版本解决。


作者:柏修
来源:juejin.cn/post/7300845951865290767
收起阅读 »

如何在 SwiftUI 中实现音频图表

iOS
前言 在可访问性方面,图表是复杂的事物之一。iOS 15 引入了一项名为“音频图表”的新功能。 下面我们将学习如何通过使用 accessibilityChartDescriptor 视图修饰符为任何 SwiftUI 视图构建音频表示,呈现类似自定义条形图视图或...
继续阅读 »

前言


在可访问性方面,图表是复杂的事物之一。iOS 15 引入了一项名为“音频图表”的新功能。


下面我们将学习如何通过使用 accessibilityChartDescriptor 视图修饰符为任何 SwiftUI 视图构建音频表示,呈现类似自定义条形图视图或图像的图表。


DataPoint 结构体


让我们从在 SwiftUI 中构建一个简单的条形图视图开始,该视图使用垂直条形显示一组数据点。


struct DataPoint: Identifiable {
let id = UUID()
let label: String
let value: Double
let color: Color
}

在这里,我们有一个 DataPoint 结构,用于描述条形图视图中的条形。它具有 id、标签、数值和填充颜色。


BarChartView 结构体


接下来,我们可以定义一个条形图视图,它接受一组 DataPoint 结构体实例并将它们显示出来。


struct BarChartView: View {
let dataPoints: [DataPoint]

var body: some View {
HStack(alignment: .bottom) {
ForEach(dataPoints) { point in
VStack {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(point.color)
.frame(height: point.value * 50)
Text(point.label)
}
}
}
}
}

如上例所示,我们有一个 BarChartView,它接收一组 DataPoint 实例并将它们显示为水平堆栈中不同高度的圆角矩形。


ContentView 结构体


我们能够在 SwiftUI 中轻松构建条形图视图。接下来让我们尝试使用带有示例数据的新 BarChartView


struct ContentView: View {
@State private var dataPoints = [
DataPoint(label: "1", value: 3, color: .red),
DataPoint(label: "2", value: 5, color: .blue),
DataPoint(label: "3", value: 2, color: .red),
DataPoint(label: "4", value: 4, color: .blue),
]

var body: some View {
BarChartView(dataPoints: dataPoints)
.accessibilityElement()
.accessibilityLabel("Chart representing some data")
}
}

在这里,我们创建了一组 DataPoint 实例的示例数组,并将其传递给 BarChartView。我们还为图表创建了一个可访问元素,并禁用了其子元素的可访问性信息。为了改进图表视图的可访问性体验,我们还添加了可访问性标签。


最后,我们可以开始为我们的条形图视图实现音频图表功能。音频图表可以通过旋钮菜单获得。要使用旋钮,请在 iOS 设备的屏幕上旋转两个手指,就像您在拨盘。VoiceOver 会说出第一个旋钮选项。继续旋转手指以听到更多选项。松开手指选择音频图表。然后在屏幕上上下滑动手指以导航。


音频图表允许用户使用音频组件理解和解释图表数据。VoiceOver 在移动到图表视图中的条形时播放具有不同音调的声音。VoiceOver 对于更大的值使用高音调,对于较小的值使用低音调。这些音调代表数组中的数据。


实现协议


现在,我们可以讨论在 BarChartView 中实现此功能的方法。首先,我们必须创建一个符合 AXChartDescriptorRepresentable 协议的类型。AXChartDescriptorRepresentable 协议只有一个要求,即创建 AXChartDescriptor 类型的实例。AXChartDescriptor 类型的实例表示我们图表中的数据,以 VoiceOver 可以理解和交互的格式呈现。


extension ContentView: AXChartDescriptorRepresentable {
func makeChartDescriptor() -> AXChartDescriptor {
let xAxis = AXCategoricalDataAxisDescriptor(
title: "Labels",
categoryOrder: dataPoints.map(\.label)
)

let min = dataPoints.map(\.value).min() ?? 0.0
let max = dataPoints.map(\.value).max() ?? 0.0

let yAxis = AXNumericDataAxisDescriptor(
title: "Values",
range: min...max,
gridlinePositions: []
) { value in "\(value) points" }

let series = AXDataSeriesDescriptor(
name: "",
isContinuous: false,
dataPoints: dataPoints.map {
.init(x: $0.label, y: $0.value)
}
)

return AXChartDescriptor(
title: "Chart representing some data",
summary: nil,
xAxis: xAxis,
yAxis: yAxis,
additionalAxes: [],
series: [series]
)
}
}

我们所需做的就是符合 AXChartDescriptorRepresentable 协议,并添加 makeChartDescriptor 函数,该函数返回 AXChartDescriptor 的实例。


首先,我们通过使用 AXCategoricalDataAxisDescriptorAXNumericDataAxisDescriptor 类型定义 X 轴和 Y 轴。我们希望在 X 轴上使用字符串标签,这就是为什么我们使用 AXCategoricalDataAxisDescriptor 类型的原因。在线图的情况下,我们将在两个轴上都使用 AXNumericDataAxisDescriptor 类型。


实现线图


接下来,我们使用 AXDataSeriesDescriptor 类型定义图表中的点。有一个 isContinuous 参数,允许我们定义不同的图表样式。例如,对于条形图,它应该是 false,而对于线图,它应该是 true。


struct ContentView: View {
@State private var dataPoints = [
DataPoint(label: "1", value: 3, color: .red),
DataPoint(label: "2", value: 5, color: .blue),
DataPoint(label: "3", value: 2, color: .red),
DataPoint(label: "4", value: 4, color: .blue),
]

var body: some View {
BarChartView(dataPoints: dataPoints)
.accessibilityElement()
.accessibilityLabel("Chart representing some data")
.accessibilityChartDescriptor(self)
}
}

作为最后一步,我们使用 accessibilityChartDescriptor 视图修饰符将符合 AXChartDescriptorRepresentable 协议的实例设置为描述我们图表的实例。


示例截图:



总结


音频图表功能对于视力受损的用户来说是一项重大改进。音频图表功能的好处是,可以将其用于任何您想要的视图,甚至包括图像视图。只需创建 AXChartDescriptor 类型的实例。


作者:Swift社区
来源:juejin.cn/post/7301496834232401959
收起阅读 »

如何优雅地创建对象?

1. 写在前头 大家好,我是方圆,最近读完了《Effective Java 第三版》,准备把其中可供大家一起学习的点来分享出来。 这篇博客儿主要是关于建造者模式在创建对象时的应用,这已经成了我现在写代码的习惯,它在灵活性和代码整洁程度上,都让我十分满意。以下的...
继续阅读 »

1. 写在前头


大家好,我是方圆,最近读完了《Effective Java 第三版》,准备把其中可供大家一起学习的点来分享出来。


这篇博客儿主要是关于建造者模式在创建对象时的应用,这已经成了我现在写代码的习惯,它在灵活性和代码整洁程度上,都让我十分满意。以下的内容非常的长,也是我费尽心力去完成的一篇博客儿,从初次应用建造者模式,到发现Lombok方便的注解,最后深挖Lombok的源码,大家既可以简单的学会它的应用,也可以从源码的角度来弄清楚它为什么是这样儿,就看你有什么需求了!


那,我们开始吧!


2. Java Beans创建对象


先创建一个Student类做准备,包含如下五个字段,姓名,年龄,爱好,性别和介绍


public class Student {

private String name;

private Integer age;

private String hobby;

/**
* 性别 0-女 1-男
*/

private Integer sex;

/**
* 介绍
*/

private String describe;

}

2.1 最常见的创建对象方式



  • 直接new一个对象,之后逐个set它的值,比如我们现在需要一个芳龄23岁的男生叫小明


Student xm = new Student();
xm.setName("小明");
xm.setAge(23);
xm.setSex(1);


  • 四行代码看着好多,我现在想让代码好看一些,一行就把这个对象创建出来,那就,添加个构造函数呗


// Student中添加构造函数
public Student(String name, Integer age, Integer sex) {
this.name = name;
this.age = age;
this.sex = sex;
}

// 一行一个小明
Student xm2 = new Student("小明", 23, 1);

这下看着是舒心多了,一行代替了之前的四行代码



  • 又来新需求了,创建一个对象,只要年龄和姓名,不要性别了,如果还要使用一行代码的话,我们又需要维护一个构造方法


// Student中添加构造函数
public Student(String name, Integer age) {
this.name = name;
this.age = age;
}

// 一行一个小明
Student xm3 = new Student("小明", 23);

两个构造方法,维护起来感觉还好...



  • 但是,需求接连不断,“再给我来一个只有名字的小明!”,“我还要一个有名字,有爱好的小明”,“我还要...”


有没有发现点儿什么,也就是说,只要创建包含不同字段的对象,都需要维护一个构造方法,五个字段最多维护“5 x 4 x 3 x 2 x 1...” 个构造方法,这才仅仅是五个字段,现在想想如果每打开一个实体类文件映入眼帘的是无数个构造方法,我就...


image.png


所以这个弊端很明显,Java Beans创建对象会让代码行数很多,一行set一个属性,不美观,而采用了构造方法创建对象之后,又要对构造方法进行维护,代码量大增,难道代码美观和少代码量不能兼得吗?


3. effective Java说:用建造者模式创建对象


我先直接把代码写好,再一点点给大家讲


public class Student {

private String name;

private Integer age;

private String hobby;

/**
* 性别 0-女 1-男
*/

private Integer sex;

/**
* 介绍
*/

private String describe;

// 注意这里添加了一个private的构造函数,建造者字段和实体字段一一对应赋值
private Student(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.hobby = builder.hobby;
this.sex = builder.sex;
this.describe = builder.describe;
}

// 静态方法创建建造者对象
public static Builder builder() {
return new Builder();
}

/**
* 采用建造者模式,每个字段都有一个设置字段的方法
* 且返回值为Builder,能进行链式编程
*/

public static class Builder {
private String name;
private Integer age;
private String hobby;
private Integer sex;
private String describe;

// 私有构造方法
private Builder() {
}

public Builder name(String val) {
this.name = val;
return this;
}

public Builder age(Integer val) {
this.age = val;
return this;
}

public Builder hobby(String val) {
this.hobby = val;
return this;
}

public Builder sex(Integer val) {
this.sex = val;
return this;
}

public Builder describe(String val) {
this.describe = val;
return this;
}

public Student build() {
return new Student(this);
}
}

}


  • 需要注意的点:




  1. 为Student添加了一个private的构造函数,参数值为Builder,建造者字段和实体字段在构造函数中一一对应赋值




  2. 建造者中对每个字段都添加一个方法,且返回值为建造者本身,这样才能进行链式编程




3.1 这下能自如应对对象创建


// 创建一个23岁的小明
Student xm4 = Student.builder().name("小明").age(23).build();
// 创建一个男23岁小明
Student xm5 = Student.builder().name("小明").age(23).sex(1).build();
// 创建一个喜欢写代码的小明
Student xm6 = Student.builder().name("小明").hobby("代码").build();
// ...

3.2 新添加字段怎么办?



  • 如果要新增一个国籍的字段,不光要在实体类中添加,还需要在建造者中添加对应的字段方法,而且还要更新实体类的构造方法


// 实体类和建造者中均新增字段
private String country;

// 建造者中添加对应方法
public Builder country(String val) {
this.country = val;
return this;
}

// 更新实体类的构造方法
private Student(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.hobby = builder.hobby;
this.sex = builder.sex;
this.describe = builder.describe;
// 新增赋值代码
this.country = builder.country;
}

完成如上工作就可以创建对象为country赋值了


Student xm7 = Student.builder().name("小明").country("中国").build();



  • 那,建造者模式的好处又有什么? 难道不是既有了JavaBeans创建对象的可读性避免了繁重的代码量吗?




  • 题外话: 在我刚使用如上建造者模式创建对象的时候,觉得分分钟能吊打Java Beans创建对象的代码,也乐此不疲的为我要使用的实体类进行维护,但是也正所谓“凡事都很难经得住时间的磨砺”,当发现了更好的方法后,我变懒了!




4. Lombok的@Builder注解


4.1 注解带来的代码整洁



  • 在类上注解标注@Builder注解,会自动生成建造者的代码,且和上述用法一致,而且不需要再为新增字段特意维护代码,也太香了吧...


@Data
@Builder
public class Student {
...
}


  • 所以可以直接标注@Builder注解使用建造者模式创建对象(使用方法和上文中3.1节一致)


4.2 你可能听说过@Accessors要比@Builder灵活



  • @Builder在创建对象时具有链式赋值的特点,但是在创建对象后,就不能链式赋值了,虽然toBuilder注解属性可以返回一个新的建造者,并复用对象的成员变量值,但是这并不是在原对象上进行修改,调用完build方法后,会返回一个新的对象


// 在@Builder注解中,指定属性toBuilder = true
@Builder(toBuilder = true)

// 在创建完成对象后使用toBuilder方法获取建造者,指定新的属性值创建对象
Student xm7 = Student.builder().name("小明").country("中国").build();

Student xm8 = xm7.toBuilder().age(23).build();


  • @Accessors注解可以在原对象上进行赋值,这里先解读一下@Accessors的源码,方便对下面的用法理解


/**
* @Accessors注解是不能单独使用的,单独标记不会产生任何作用
* 需要搭配@Data或者@Getter@Setter使用才能生效
*/

@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Accessors {
/**
* 这个属性默认是false,为false时,getter和setter方法会有get和set前缀
* 什么意思呢,比如字段name,在该属性为false生成的get和set方法为getName和setName
* 而当属性为true时,就没有没有get和set前缀,get方法和set方法都名为name,只不过set方法要有参数,是对name方法的重载
*/

boolean fluent() default false;

/**
* chain属性,显然从字面意思它能实现链式编程,默认属性false
* 为true时,setter方法的返回值是该对象,那么我们就能进行链式编程了
* 为false时,setter的返回值为void,就不能进行链式编程了
*
* 注意:特殊的一点是,当fluent属性为true时,该值在不指定的情况下也会为true
*/

boolean chain() default false;

/**
* 这个属性值当我们指定的时候,会将字段中已经匹配到的前缀进行'删除'后生成getter和setter方法
* 但是它也有生效条件:字段必须是驼峰式命名,且前几个小写字母与我们指定的前缀一致
*
* 举个例子:
* 我们有一个字段如下
* private String lastName
* 在我们不指定prefix时,生成的getter和setter方法为 getLastName 和 setLastName
* 当我们指定prefix为last时,那么生成的getter和setter方法 为 getName 和 setName
*/

String[] prefix() default {};
}


  • 下面我们来看看用法,它实在是很灵活


// 我们为Student类标记一个如下注解,方法不含get和set前缀,同时又支持链式编程
@Accessors(fluent = true, chain = true)

// 这里我们创建一个25岁的小明
Student xm9 = new Student().age(25).name("小明");
// do something

// 使用完之后,假设这里需要对25岁的小明的属性进行修改,可采用如下方法,之后重新复用这个对象即可
xm9.country("中国");


  • 这也实在太好用了吧!


4.3 既然把@Accessors的源码读了,@Builder的源码我也讲给你听吧


@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.SOURCE)
public @interface Builder {
// 指定创建建造者的方法名,默认为builder
String builderMethodName() default "builder";

// 指定创建对象的方法名,默认为build
String buildMethodName() default "build";

// 指定静态内部建造者类的名字,默认为 类名 + Builder,如StudentBuilder
String builderClassName() default "";

// 是否能重新从对象生成建造者,默认为false,上文中有使用样例
boolean toBuilder() default false;

// 建造者能够使用的范围,默认是PUBLIC
AccessLevel access() default AccessLevel.PUBLIC;

// 标注了该注解的字段必须指定默认初始化值
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Default {
}

// 这个注解的使用是要和 @Builder(toBuilder = true) 一同使用才可生效
// 在调用toBuilder方法时,会根据被标注该注解的字段或方法对字段进行赋值
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.SOURCE)
public @interface ObtainVia {
// 指定要获取值的字段
String field() default "";

// 指定要获取值的方法
String method() default "";

// 这个值在指定method才有效,为true时获取值的方法必须为静态的,且方法参数值为本类(参考下文代码)
boolean isStatic() default false;
}
}


  • 全网很少有人讲@ObtainVia注解,那我们就来说说,它到底有什么用,该怎么用



  1. 指定field赋值


// 在类中注解标记和新增字段如下
@Builder.ObtainVia(field = "hobbies")
private String hobby;

// 供hobby获取值使用
private String hobbies = "唱跳RAP";

// 测试调用toBuilder方法,检查hobby值,若为‘唱跳RAP’证明注解生效
System.out.println(new Student().toBuilder().build().getHobby());
结果:唱跳RAP

查看编译后的源码,可以发现赋值语句hobby(this.hobbies),原来它是如此生效的


public Student.StudentBuilder toBuilder() {
return (new Student.StudentBuilder()).name(this.name).lastNames(this.lastNames).age(this.age)
.hobby(this.hobbies).hobbies(this.hobbies)
.sex(this.sex).describe(this.describe).country(this.country);
}


  1. 指定非静态method赋值


// 在类中标注如下注解和创建如下方法
@Builder.ObtainVia(method = "describe")
private String describe;

// 非静态方法赋值
private String describe() {
return "小明的自我介绍";
}

// 测试调用toBuilder方法,检查describe值,若为‘小明的自我介绍’证明注解生效
System.out.println(new Student().toBuilder().build().getDescribe());
结果:小明的自我介绍

查看编译后的源码,发现会调用该方法


public Student.StudentBuilder toBuilder() {
// 这里会调用该方法进行赋值,在下面生成Builder时使用
String describe = this.describe();
return (new Student.StudentBuilder()).name(this.name).lastNames(this.lastNames).age(this.age).hobby(this.hobby).sex(this.sex)
.describe(describe)
.country(this.country);
}


  1. 指定静态method赋值


// 在类中标注如下注解和创建如下静态方法
@Builder.ObtainVia(method = "describe", isStatic = true)
private String describe;

// 静态方法赋值,需要指定本类类型参数
private static String describe(Student student) {
return "小明的自我介绍";
}

// 测试调用toBuilder方法,检查describe值,若为‘小明的自我介绍’证明注解生效
System.out.println(new Student().toBuilder().build().getDescribe());
结果:小明的自我介绍

查看编译后的源码


public Student.StudentBuilder toBuilder() {
// 这里调用静态方法赋值
String describe = describe(this);
return (new Student.StudentBuilder()).name(this.name).lastNames(this.lastNames).age(this.age).hobby(this.hobby).sex(this.sex).describe(describe).country(this.country);
}

5. 番外:@Builder,@Singular 夫妻双双把家还


5.1 @Singular简介


@Singular必须搭配@Builder使用,相辅相成,@Singular标记在集合容器字段上,在建造者中自动生成针对集合容器的添加单个值添加多个值清除其中值的方法,可进行标记的集合容器类型如下(参考官方文档) java.util.Iterable, Collection, List, Set, SortedSet, NavigableSet, Map, SortedMap, NavigableMap com.google.common.collect.ImmutableCollection, ImmutableList, ImmutableSet, ImmutableSortedSet, ImmutableMap, ImmutableBiMap, ImmutableSortedMap, ImmutableTable



  • 使用演示


// 在类中添加如下字段,并标注@Singular注解
@Singular
private List<String> subjects;

// 测试代码,调用单个添加和多个值添加的方法
Student xm11 = Student.builder()
.subject("Math").subject("Chinese")
.subjects(Arrays.asList("English", "History")).build();

// 查看添加结果
System.out.println(xm11.getSubjects().toString());
结果:[Math, Chinese, English, History]

// 调用clearSubjects清空方法,并查看结果
System.out.prinln(xm11.toBuilder().clearSubjects().build().getSubjects().toString());
结果:[]

5.2 @Singular源码解析


@Target({FIELD, PARAMETER})
@Retention(SOURCE)
public @interface Singular {
// 指定添加单个值的方法的方法名,不指定时会自动生成方法名,比例中为'subject'
String value() default "";

// 添加多个值是否忽略null,默认不忽略,添加null的列表时会抛出异常
// 为ture时,添加为null的列表不进行任何操作
boolean ignoreNullCollections() default false;
}


  • @Singular(ignoreNullCollections = false)编译后的代码


public Student.StudentBuilder subjects(Collection<? extends String> subjects) {
// 添加的列表为null,抛出异常
if (subjects == null) {
throw new NullPointerException("subjects cannot be null");
} else {
if (this.subjects == null) {
this.subjects = new ArrayList();
}

this.subjects.addAll(subjects);
return this;
}
}


  • @Singular(ignoreNullCollections = true)编译后的代码


public Student.StudentBuilder subjects(Collection<? extends String> subjects) {
// 为null时不进行任何操作
if (subjects != null) {
if (this.subjects == null) {
this.subjects = new ArrayList();
}

this.subjects.addAll(subjects);
}

return this;
}

5.3 @Singular在build方法中的细节



  • 创建完对象后,被标记为@Singular的列表能修改吗?我们试试


Student xm11 = Student.builder()
.subject("Math").subject("Chinese")
.subjects(Arrays.asList("English", "History")).build();

// 再添加一门Java课程
xm11.getSubjects().add("Java");

结果:抛出不支持操作的异常
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at builder.TestBuilder.main(Student.java:177)


  • 为什么这样?我们看看源码中的build方法就知道了,build方法根据不同的列表大小走不同的初始化列表方法,返回的列表都是不能进行修改的


public Student build() {
List subjects;
switch(this.subjects == null ? 0 : this.subjects.size()) {
case 0:
// 列表大小为0时,创建一个空列表
subjects = Collections.emptyList();
break;
case 1:
// 列表大小为1时,创建一个不可修改的单元素列表
subjects = Collections.singletonList(this.subjects.get(0));
break;
default:
// 其他情况,创建一个不可修改的列表
subjects = Collections.unmodifiableList(new ArrayList(this.subjects));
}

// 下面进行忽略只看上边就好
String name$value = this.name$value;
if (!this.name$set) {
name$value = Student.$default$name();
}

return new Student(name$value, this.lastNames, this.age, this.hobby, this.sex, this.describe, this.country, subjects);
}

6. 写在最后


呼!终于写完了,做个总结吧(文末有博客对应的代码仓库)




  • @Accessors注解非常的轻便,我觉得它现在已经能cover我在业务开发中创建对象的需求了,代码可读性高,代码量又很少




  • @Builder注解它的功能相对来说更多一些,通过方法和字段来初始化建造者的值,搭配@Singular操作列表等,但是这些功能真正的在业务开发中的应用效果,还有待考量




巨人的肩膀



作者:方圆想当图灵
来源:juejin.cn/post/7246025362969722936
收起阅读 »

前端版本过低引导弹窗方案分享

web
作者:费昀锋 背景 作为 TOB 的业务方,我们偶尔会收到一些如下图所示的反馈。 作为 PC 页面为主的业务方,大多数用户在一天的工作中,可能都不太会刷新或者重新打开我们的页面,导致我们在下午或者白天发布的前端版本,往往需要到几个小时甚至第二天,才能覆盖到...
继续阅读 »

作者:费昀锋



背景


作为 TOB 的业务方,我们偶尔会收到一些如下图所示的反馈。



作为 PC 页面为主的业务方,大多数用户在一天的工作中,可能都不太会刷新或者重新打开我们的页面,导致我们在下午或者白天发布的前端版本,往往需要到几个小时甚至第二天,才能覆盖到 98% 以上的用户。



我们统计了 bscm 平台 5 次下午 2-3 点左右发布的版本,在发布后每个时间段内老版本用户的占比情况。选择这个时间点发布的原因是这个时间点基本是平台用户的上班时间,是最有可能出现用户已经打开了页面同时我们在发布新代码的场景的,比较具有代表性。按平台用户六七点下班来看,我们可以看到还有将近 6% 的用户在当天是会一直访问老版本的前端代码的,按照 bscm 平台 1w+的 uv 来看,约有 600 多人会可能遇到前端版本过低导致的使用问题。


方案


弹窗内容



弹窗的触发条件


首先介绍两个概念,本地版本号和云端版本号。本地版本号是用户请求到的前端页面的代码版本号,是用户访问页面时决定;云端版本号可以理解为最新前端版本号,它是每次开发者发布前端代码时决定的。



判断触发条件的时机


有了弹窗的触发条件,我们还需要去决定什么时候判断弹窗是否满足触发的条件,上面也提到了,出现这类问题的场景多见于用户在使用过程中,开发者进行了前端代码发布,那我们主要可以有两个类型的时机去进行触发条件的判断。




  1. 前端代码去感知什么时候有新版本的代码发布了,去进行条件判断(消息推送)




  2. 前端在一定的条件下主动去判断触发条件(轮询,请求后端接口时,一些中频前端事件的监听)




我们对这些时机在更新是否及时,判断次数多少、实现成本高低等维度进行一个对比。



⭐️ 越多表示这个维度得分越高




根据表格可以看到 websocket 消息推送和前端事件监听这两种方案综合来看是更合适一些的,但是前端事件监听其实它的劣势在实际运用场景中会被弱化(一天的上线数量有限,请求次数一天不会多太多次),但是实现成本远低于 websocket,所以无疑是实际落地场景中比较理想的选择。



根据 can i use 的结果我们也可以发现 visibilitychange 事件也基本符合我们目前 B 端页面对于 PC 浏览器的要求。


版本号的生成


本地版本号


本地版本号是用户访问时决定的,那无疑页面的 html 文件就是这个版本号存在的最佳载体,我们可以在打包时通过 plugin 给 html 文件注入一个版本号。


云端版本号


云端版本号的选择则有很多方式了,数据库、cdn 等等都可以满足需求。不过考虑到实现成本和泳道的情况,想了一下两个思路一个是打包的同时生成一个 version.json 文件,配一个路由去访问;另一个是直接访问对应的 html 代码,解析出注入的版本号,二者各自有适合的场景。


微前端的适配


我们现在的大多数项目都包含了主应用和子应用,那其实不管是子应用的更新还是主应用的更新都应该有相关的提示,而且相互独立,但同时又需要保证弹窗不同时出现。


想要沿用之前的方案其实只需要解决三个问题。



  1. 主子应用的本地版本号标识需要有区分,因为 html 文件只有一个,需要能在 html 文件中区分出哪个应用的版本是什么,这个我们只需在 plugin 中注入标识即可解决。

  2. 云端版本号请求时也要请求对应的云端版本号,这个目前采用的方案是主应用去请求唯一的 version.json 文件,因为主应用路由是唯一的,子应用则去请求最新的 html 资源文件,解析出云端版本号。

  3. 不重复弹窗我们只需要在展示弹窗前,多加一个是否已经有弹窗展示的判断即可了。


具体实现


版本号的写入和读取



监听时机和频控逻辑


正如前文提到的,本身版本发布不是一个高频事件,但是监听事件的频次有时候可能过高了,不希望频繁的去进行触发条件判断。同时如果出现一天内多次发布的场景,也不希望这个弹窗对于用户有过多的打扰,所以需要去添加一个频控逻辑。



具体代码


plugin


/* eslint-disable */
import { CoraWebpackPlugin, WebpackCompiler } from '@ies/eden-web-build';
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');

interface IVersion {
name?: string; // 编译完的文件夹名称
subName?: string; // 子应用的名称,主应用可以不传
}

export class VersionPlugin implements CoraWebpackPlugin {
readonly name = 'versionPlugin'; // 插件必须要有一个名字,这个名字不能和已有插件冲突
private _version: number;
private _name: string;
private _subName: string;
constructor(params: IVersion) {
this._version = new Date().getTime();
this._name = params?.name || 'build';
this._subName = params?.subName || ''
}
apply(compiler: WebpackCompiler): void {
compiler.hooks.afterCompile.tap('versionPlugin', () => {
try {
const filePath = path.resolve(`./${this._name}/template/version.json`);
fs.writeFile(filePath, JSON.stringify({ version: this._version }), (err: any) => {
if (err) {
console.log('@@@err', err);
}
});
const htmlPath = path.resolve(`./${this._name}/template/index.html`);
const data = fs.readFileSync(htmlPath);
const $ = cheerio.load(data);
$('body').append(`
${this._subName}versionTag" style="display: none">${this._version}
`
);
fs.writeFile(htmlPath, $.html(), (err: any) => {
if (err) {
console.log('@@@htmlerr', err);
}
});
} catch (err) {
console.log(err);
}
});
}
}

弹窗组件


import React, { useEffect } from 'react';

import { Modal } from '@ecom/auxo';
import axios from 'axios';
import moment from 'moment';

export interface IProps {
isSub?: boolean; // 是否为子应用
subName?: string; // 子应用名称
resourceUrl?: string; // 子应用的资源url
}

export type IType = 'visibilitychange' | 'popstate' | 'init';

export default React.memo<IProps>(props => {
const { isSub = false, subName = '', resourceUrl = '' } = props || {};

const cb = (latestVersion: number | undefined, currentVersion: number | undefined, type: IType) => {
try {
// 版本落后,提示可以刷新页面
if (latestVersion && currentVersion && latestVersion > currentVersion) {
// 提醒过了就设置一个更新提示过期时间,一天内不需要再提示了,弹窗过期时间暂时全局只需要一个!!
localStorage.setItem(`versionUpdateExpireTime`, moment().endOf('day').format('x'));
if (!document.getElementById('versionModalTitle')) {
Modal.confirm({
title: <div id="versionModalTitle">版本更新提示div>,
content:
'您已经长时间未使用此页面,在此期间平台有过更新,如您此时在页面中没有填写相关信息等操作,请点击刷新页面使用最新版本!',
okText: <div data-text={`前端版本升级引导-立即更新 ${type}`}>刷新页面div>,
cancelText: <div data-text={`前端版本升级引导-我知道了 ${type}`}>我知道了div>,
onCancel: () => {
console.log('fe-version-watcher INFO: 未更新~');
},
onOk: () => {
location.reload();
},
});
}
}
// 不管版本是否落后,半小时内都不需要去重新请求判断
localStorage.setItem(`versionInfoExpireTime`, String(new Date().getTime() + 1000 * 60 * 30));
} catch {}
};

const formatVersion = (text?: string) => (text ? Number(text) : undefined);

useEffect(() => {
try {
const fn = function (type: IType) {
if (document.visibilityState === 'visible') {
/**
*
@desc 为了防止打扰,版本更新每个应用一天只提示一次 所以过期时间设为当天23:59:59,没过期则直接return
*/

if (Number(localStorage.getItem(`versionUpdateExpireTime`) || 0) >= new Date().getTime()) {
return;
}
/**
*
@desc 不需要每次切换页面都去判断资源,每次从服务器获取到的版本信息,给半个小时的缓存时间,需要区分子应用
*/

if (Number(localStorage.getItem(`versionInfoExpireTime`) || 0) > new Date().getTime()) {
return;
}

if (!isSub) {
/**
*
@desc 主应用使用version.json文件来获取最新的版本号
*/

const dom = document.getElementById('versionTag');
const currentVersion = formatVersion(dom?.innerText);
axios.get(`/version?timestamp=${new Date().getTime()}`).then(res => {
const latestVersion = res?.data?.version;
cb(latestVersion, currentVersion, type);
});
} else {
/**
*
@desc 子应用使用最新html中的innerText来获取最新版本号
*/

if (resourceUrl) {
const dom = document.getElementById(`${subName}versionTag`);
const currentVersion = dom?.innerText ? Number(dom?.innerText) : undefined;
axios.get(resourceUrl).then(res => {
/** ignore_security_alert */
try {
const html = res.data;
const doc = new DOMParser().parseFromString(html, 'text/html');
const latestVersion = formatVersion(doc.getElementById(`${subName}versionTag`)?.innerText);
cb(latestVersion, currentVersion, type);
} catch {}
});
}
}
}
};
const visibleFn = () => {
fn('visibilitychange');
};
const routerFn = () => {
fn('popstate');
};
if (isSub) {
// 子应用可能会有缓存,初始化的时候先判断一次
fn('init');
}
document.addEventListener('visibilitychange', visibleFn);
window.addEventListener('popstate', routerFn);
return () => {
document.removeEventListener('visibilitychange', visibleFn);
window.removeEventListener('popstate', routerFn);
};
} catch {}
}, []);

return <div />;
});

如何接入


主应用版本



  1. 安装依赖


npm i @ecom/fe-version-watcher-plugin # 安装plugin 
npm i @ecom/logistics-supply-chain-fe-version-watcher # 安装引导弹窗


  1. 引入 versionPlugin,自动生成 version.json + html 文件中自动注入


import { VersionPlugin } from '@ecom/fe-version-watcher-plugin';

// 有些项目打包后template文件夹下的名字不是build而是build_cn
// 可以根据自己项目的实际情况传入{name: build_cn}

{
...,
plugins: [
...,
[VersionPlugin, {}],
]
}


  1. 引入版本引导弹窗


import { FeVersionWatcher } from '@ecom/logistics-supply-chain-fe-version-watcher';

<FeVersionWatcher />


  1. goofy 新增路由配置,/version 指向 version.json 文件 (或者其它方式可以使得/version 的路由指向该 version.json 文件)



预告


采用 version.json 的方案,引入 FersionWatcher 组件就不再需要任何参数,目前主应用只支持这种模式。未来也将参考子应用,主应用支持读取 html 中版本标识的能力,将配置路由的工作改成组件 props 传入资源 url,开发者可以根据实际情况自行选择。


子应用版本



  1. 安装依赖


npm i @ecom/fe-version-watcher-plugin # 安装plugin
npm i @ecom/logistics-supply-chain-fe-version-watcher # 安装引导弹窗


  1. 引入 versionPlugin, html 文件中自动注入版本号,需要子应用标识参数(必填)


import { VersionPlugin } from '@ecom/fe-version-watcher-plugin';

// 有些项目打包后template文件夹下的名字不是build而是build_cn
// 可以根据自己项目的实际情况传入{name: build_cn}

{
...,
plugins: [
...,
[VersionPlugin, {subName: 'general-supplier', name: 'build_cn'}],
]
}


  1. 引入版本引导弹窗(subName 和 plugin 中保持一致,resourceUrl 为配置的子应用路由)


import { FeVersionWatcher } from '@ecom/logistics-supply-chain-fe-version-watcher';

// subName需要和plugin的参数保持一致,resourceUrl为子应用资源的路径(子引用goofy上配置的路由)
<FeVersionWatcher isSub subName="general-supplier" resourceUrl="/webApp/general-supplier" />

resourceUrl一般就是goofy上配置的路由设置,,如果不同平台有区分,可以动态传入。



如何调试/效果展示


发布成功后,可以根据如下步骤测试:




  1. 删除 localstorage 中相关的 value





  2. 修改 html 中的 version,改成一个比较小的数值即可





  3. 切换路由,或者隐藏/打开页面,出现弹窗




收益统计



同样我们截取了 4 次该平台 2-3 点发布的版本情况,可以看到老版本用户的 uv 占比有着明显的下降。



上线至今共计提示 10 万+用户,帮助约 5 万人次及时更新了前端代码。


作者:字节前端
来源:juejin.cn/post/7301530293377843235
收起阅读 »

索引数据结构千千万 , 为什么B+Tree独领风骚

索引的由来 大数据时代谁掌握了数据就是掌握了流量,就是掌握的号召力。面对浩瀚的数据如何存储并非难事, 难点在于如何在大数据面前查询依旧快如闪电! 这时候索引就产生了,索引的产生主要还是借鉴于图书管理员书签的功能。在大数据面前 es 产生了,而我们今天要...
继续阅读 »

索引的由来




  • 大数据时代谁掌握了数据就是掌握了流量,就是掌握的号召力。面对浩瀚的数据如何存储并非难事, 难点在于如何在大数据面前查询依旧快如闪电!




  • 这时候索引就产生了,索引的产生主要还是借鉴于图书管理员书签的功能。在大数据面前 es 产生了,而我们今天要说的索引却不是它 而是目前中小项目中广泛使用的 mysql 数据库中的索引。




  • 本文主题着重介绍索引是什么?索引如何存储?为什么这么设计索引?常见的索引有哪些?最后我们在通过案列来分析如何命中索引以及索引失效的部分场景。




什么是索引



索引是创建在表上的,对数据库表中一列或多列的值进行排序的一种结构,可以提高查询的速度。




  • 索引是一种数据结构,以协助快速查询,更新数据库中的数据 。 mysql 的索引主要由 B+Tree 进行存储。在存储主题上又分为聚簇索引和非聚簇索引。


聚簇索引




  • 聚簇索引从字面上理解就是聚集在一起。所以凡事索引和数据存放在一起的我们就叫做聚簇索引。在mysqlINNODB 的主键索引就是采用的聚簇索引,因为在叶子节点负责存放数据,而非叶子节点负责存放索引。而除了主键索引外其他索引则是非聚簇索引,因为其他索引的叶子节点存储的是主键索引的地址指向。




非聚簇索引



  • MyISAM 引擎中就是非聚簇索引,我们通过它的文件结构也能够看出索引和数据是分开存放的。 非聚簇索引也会带来一些问题。诸如回表

  • INNODB 中非主键索引就是非聚簇索引,同时这种非主键索引也会带来一个问题就是二次索引也称回表。因为我们通过非主键索引是无法定位到最终数据的。大部分情况下我们是需要在根据主键索引进行第二次查找的。加入你有一个索引idx_name

    • select name from t where name=13 发生一次索引,不会回表查询

    • select * from t where name=13 发生两次索引,会发生回表



  • 上面第一个sql 不会发生回表是因为我门的sql 发生了索引覆盖,意思是idx_name 这颗树已经覆盖了我们查询的范围。


索引存储结构



  • 先说结论 mysql 中索引是通过 B+ Tree 进行存储的。但是在 mysql 中一开始是采取的 二叉树存储的。关于树形存储结构都是二叉树。那么我们是mysql 中不采用二叉树、红黑树呢?下面我们来分析下采用二叉树、红叉树分别会带来哪些问题。


二叉树



  • 二叉树是根据顺序在根据大小判断其存储的左右节点的。这就导致如果我们是按递增ID作为索引的话,最终就导致二叉树变成一颗偏向一边的树,换个角度看其实就是链表。


image-20221116191402773.png




  • 而针对一张表我们往往就是ID作为索引的居多。而ID采用自增策略的居多,所以如果索引采用的是二叉树的,毋庸置疑销量基本无提升,这也是为什么官方放弃 二叉树 作为索引存储的数据结构。




  • 而二叉树一共有如下几种极端情况




image-20221116203557935.png


平衡二叉树



  • 在开始红黑树之前,我们需要先了解下有种临界状态叫平衡二叉树。

  • 平衡二叉树又叫做Self-balancing binary search tree 。 平衡二叉树是二叉树的一种特例

  • 在二叉树中有一个定义平衡度(平衡因子)的概念。他的公式是左右高度的绝对值。

  • 当这个平衡度<=1的时候我们就称之为平衡二叉树

  • 在平衡二叉树中他的高度是最稳定的,换句话说平衡二叉树和其他二叉树相比能够在相同的节点情况下保证树的高度最低;这也是为什么mysql中索引的结构是一种平衡二叉树的升级版


image-20221116203517550.png


红黑树



红黑树实际上是一颗平衡二叉树;所以在构建的过程中他会发生自平衡



image-20221116194807714.png



  • 因为二叉树在极端的情况会变成一个链表,针对链表的问题红黑树的自平衡特性就完美的规避了二叉树的缺点。那么为什么最终索引也不是选择红黑树呢?

  • 仔细观察能够发现红黑树是一颗标准的二叉树。他所能容纳的最大节点数和他的高度正好成二的次方这个关系。也就是说假设红黑树的高度是h ,那么他能容纳最多的节点为 2^h。

  • 这样看来在数据量过大时,通过红黑树去构建貌似这颗二叉树高度就过去庞大了。高度也高给我们查询就带来更多次交互。要知道每个节点都是存储在硬盘中的,那么每一次的访问都会带来一次IO消耗。所以为了能够提高查询效率 mysql 最终还是没有选择红黑树。


①、每个节点要么红色要么黑色


②、根节点是黑色的


③、叶子节点是黑色的


④、红色节点的子节点一定是黑色的


⑤、从一个节点出发,到达任意一个叶子结点(NULL)路径上一定具有相同的黑色节点(保证了平衡度<=2)


image-20221116203407778.png


BTree



BTree的设计主要是针对磁盘获取其他存储的一种平衡树(不一定是二叉这里往往指的是多叉)



image-20221116203109328.png



  • B树非常适合读取和写入相对较大的数据块(如光盘)的存储系统。它通常用于数据库和文件系统。

  • 总结下BTree 具有如下特点:


①、至少是2阶,即至少有两个子节点
②、对于m阶BTree来说,非根节点所包含的关键词个数j需要满足 (m/2)-1<=j<=m-1
③、除叶子结点外,节点内关键词个数+1总是等于指针个数
④、所有叶子结点都在同一层
⑤、每个关键字保存实际磁盘数据


B+Tree



B+Tree 是BTree的一种变体。BTree节点里出了索引还会存储指针数据,而B+Tree仅存储索引值,这样同样空间节点能够存储更多的索引




  • B+Tree 因为压缩了数据存储空间,这样就能够在相同高度的BTree上存储更多的索引,这样更加提高索引定位销率。


image-20221116203306106.png


Hash表


①、hash索引无法进行范围查询,因为上述的hash结构是没有顺序的,hash索引只能实现等于、In等查询
②、hash值是针对元数据的一种散列运算。hash值得大小并不能反应元数据的大小。元数据a 、b对应的hash值有可能是3333、2222,而实际上上a<b . 所以我们无法通过hash值进行排序,从而hash索引无法进行排序
③、对于组合索引来说,在B+Tree中我们有最左匹配原则,但是在hash索引中是不支持的。因为组合索引整个映射成hash值,我们通过联合索引中部分值进行hash运算得带的值与hash索引中是没有关系的
④、hash索引在查询时是需要遍历整个hash表的。这点我们Java中的HashMap一样
⑤、hash索引在数据量少的情况下比BTree快。但是当hash冲突比较多的时候定位就会比B+Tree慢很多了。


image-20221116203746016.png


总结



  • 现在看来数据库运行的很牛逼,而且索引也很快,但这并不是一口吃成胖子的,了解了索引的底层数据结构后我们也能够了解 mysql 也是一步一步尝试过来的, 索引也是不断的优化而成的。说不定以后还会有其他结构产生,只能说每种数据结构都是最好的,前提是在特定的场景下。

  • 本专栏最后一篇我们将介绍下 mysql 的索引如何命中,以及那些场景导致索引失效。然后再着重介绍下高频面试题--回表&&索引下推



作者:zxhtom
来源:juejin.cn/post/7168268214713974798
收起阅读 »

说一个大家都知道的 Spring Boot 小细节!

小伙伴们知道,我们在创建 Spring Boot 项目的时候,默认都会有一个 parent,这个 parent 中帮我们定了项目的 JDK 版本、编码格式、依赖版本、插件版本等各种常见内容,有的小伙伴可能看过 parent 的源码,这个源码里边有这么一个配置:...
继续阅读 »

小伙伴们知道,我们在创建 Spring Boot 项目的时候,默认都会有一个 parent,这个 parent 中帮我们定了项目的 JDK 版本、编码格式、依赖版本、插件版本等各种常见内容,有的小伙伴可能看过 parent 的源码,这个源码里边有这么一个配置:


<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/application*.yml</include>
<include>**/application*.yaml</include>
<include>**/application*.properties</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
<excludes>
<exclude>**/application*.yml</exclude>
<exclude>**/application*.yaml</exclude>
<exclude>**/application*.properties</exclude>
</excludes>
</resource>
</resources>

首先小伙伴们知道,这个配置文件的目的主要是为了描述在 maven 打包的时候要不要带上这几个配置文件,但是咋一看,又感觉上面这段配置似乎有点矛盾,松哥来和大家捋一捋就不觉得矛盾了:



  1. 先来看第一个 resource,directory 就是项目的 resources 目录,includes 中就是我们三种格式的配置文件,另外还有一个 filtering 属性为 true,这是啥意思呢?这其实是说我们在 maven 的 pom.xml 文件中定义的一些变量,可以在 includes 所列出的配置文件中进行引用,也就是说 includes 中列出来的文件,可以参与到项目的编译中。

  2. 第二个 resource,没有 filter,并且将这三个文件排除了,意思是项目在打包的过程中,除了这三类文件之外,其余文件直接拷贝到项目中,不会参与项目编译。


总结一下就是 resources 下的所有文件都会被打包到项目中,但是列出来的那三类,不仅会被打包进来,还会参与编译。


这下就清晰了,上面这段配置实际上并不矛盾。


那么在 properties 或者 yaml 中,该如何引用 maven 中的变量呢?


这块原本的写法是使用 $ 符号来引用,但是,我们在 properties 配置文件中,往往用 $ 符号来引用当前配置文件的另外一个 key,所以,我们在 Spring Boot 的 parent 中,还会看到下面这行配置:


<properties>
<java.version>17</java.version>
<resource.delimiter>@</resource.delimiter>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

这里的 <resource.delimiter>@</resource.delimiter> 就表示将资源引用的符号改为 @ 符号。也就是在 yaml 或者 properties 文件中,如果我们想引用 pom.xml 中定义的变量,就可以通过 @ 符号来引用。


松哥举一个简单的例子,假设我想在项目的 yaml 文件中配置当前项目的 Java 版本,那么我就可以像下面这样写:


app:
java:
version: @java.version@

这里的 @java.version@ 就表示引用了 pom.xml 中定义的 java.version 变量。


现在我们对项目进行编译,编译之后再打开 application.yaml,内容如下:



可以看到,引用的变量已经被替换了。


按照 Spring Boot parent 中默认的配置,application*.yaml、application*.yml 以及 application*.properties 文件中可以引用 pom.xml 中定义的变量,其他文件则不可以。如果其他文件也想引用,就要额外配置一下。


例如,想让 txt 文件引用 pom.xml 中的变量,我们可以在 pom.xml 中做如下配置:


<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.txt</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

include 所有的 txt 文件,并且设置 filtering 为 true(不设置默认为 false),然后我们就可以在 resources 目录下的 txt 文件中引用 pom.xml 中的变量了,像下面这样:



编译之后,这个变量引用就会被替换成真正的值:



在 yaml 中引用 pom.xml 的配置,有一个非常经典的用法,就是多环境切换。


假设我们现在项目中有开发环境、测试环境以及生产环境,对应的配置文件分别是:



  • application-dev.yaml

  • application-test.yaml

  • application-prod.yaml


我们可以在 application.yaml 中指定具体使用哪个配置文件,像下面这样:


spring:
profiles:
active: dev

这个表示使用开发环境的配置文件。


但是有时候我们的环境信息是配置在 pom.xml 中的,例如 pom.xml 中包含如下内容:


<profiles>
<profile>
<id>dev</id>
<properties>
<package.environment>dev</package.environment>
</properties>
<!-- 是否默认 true表示默认-->
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>prod</id>
<properties>
<package.environment>prod</package.environment>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<package.environment>test</package.environment>
</properties>
</profile>
</profiles>

这里配置了三个环境,其中默认是 dev(activeByDefault)。那么我们在 application.yaml 中就可以使用 package.environment 来引用当前环境的名称,而不用硬编码。如下:


spring:
profiles:
active: @package.environment@

此时,我们通过 maven 命令对项目打包时,就可以指定当前环境的版本了,例如使用 test 环境,打包命令如下:


mvn package -Ptest

打包之后我们去看 application.yaml,就会发现里边的环境已经是 test 了。


如果你使用的是 IDEA,则也可以手动勾选环境之后点击打包按钮,如下:



可以先勾选上面的环境信息,再点击下面的打包。


好啦,一个小小知识点,因为有小伙伴在微信上问这个问题,就拿出来和大家分享下。


作者:江南一点雨
来源:juejin.cn/post/7226916546931949626
收起阅读 »

程序员黑话集

为什么 10.24 是程序员节计算机采用的是 2 进制,2 的 10 次方是 1024,而数据存储的单位从 B, KiB, MiB, GiB, TiB ..... 以 1024 作为一个跨度,比如 1 KiB = 1024 B。程序员在日常工作中接触到 102...
继续阅读 »

为什么 10.24 是程序员节

计算机采用的是 2 进制,2 的 10 次方是 1024,而数据存储的单位从 B, KiB, MiB, GiB, TiB ..... 以 1024 作为一个跨度,比如 1 KiB = 1024 B。程序员在日常工作中接触到 1024 的机会太多了,看到 1024 就会产生条件反射。
除了 1024 外,程序员届还有许多的术语/黑话,借着 10.24 这个日子,也和大家分享一下吧。学会了这些术语,基本就能和程序员们谈笑风生,而把非程序员们唬得一愣一愣。

通用术语

404

当请求的网络资源不存在时,返回的错误编号。扩展为某样东西不存在/找不到。

403

当没有权限访问网络资源时,返回的错误编号。扩展为没有权限/资格接触某项信息。

500

当在请求某个网络资源时,服务器内部发生错误时,返回的错误编号。扩展为系统发生内部故障。

API

Application Programming Interface。程序间对接的接口,两边程序只要遵循这个接口,就能互相交互。扩展为两个对象之间交互应该遵循的规范,对象没有限定,可以为人,机器,系统等概念。

Bug

软件错误。扩展到因为人为疏漏导致的问题。

Cookie

网站记录你个人访问行为的载体。你能被广告主精准命中的关键情报仓库,各种 “Allow Cookie" 骚扰弹窗的根源。

Cache

一层缓冲,商家每年搞剁手节,能不被海量用户冲垮的核心堤坝。当然偶尔流量实在太大,也会决堤,比如被屡次冲垮的微博。

RFC

Request for Comments。起初用于起草互联网标准,比如 HTTP 1.0 协议是 RFC 1945。后来不少公司的设计文档也沿用了 RFC 这个名字。

Interesting

有趣🧐。起初是个褒义词,但现在在许多场合下,因为无法给出积极评价的时候,会用该词来化解尴尬。有时甚至会传达嘲讽,所以请慎用。

UI vs UX

UI - User Interface。用户的视觉感受。UX - User Experience。用户的真实使用感受。

Frontend and Backend

前端和后端,一个应用的两个组成部分。前端是用户能感知的部分,后端则是用户感知不到,隐藏起来的部分。既有左图里美如画的前端,脏乱差的后端,也有右图里风雨飘摇的前端,高精尖的后端。

研发流程相关

DevOps

把研发(Dev) 和运维(Ops) 两种职能融合在一起的运动。也有人认为这是让一个人打两份工的阴谋。

生产环境 (Production / Prod)

也叫线上环境,实际在生产当中使用的。与之对应的是内部的研发环境 (Dev),测试环境 (Test),预发环境(Staging)。

Test in Prod

一种以快速迭代作为借口,不做全面的测试,就直接把代码发布到生产环境的行为。虽然许多产品都是这么做的。

Canary (金丝雀)

过去矿工下井时会带上金丝雀,因为金丝雀对有害气体敏感,如果有害气体超标,金丝雀会率先死亡,矿工便知道需要撤离。在软件发布中,也通常会采用金丝雀模式,把新的版本先发布到一小部分用户中。有些地方把前面提到的预发环境也直接叫做金丝雀环境。

删库跑路

最严重的事故莫过于误删了应用数据库,所有的数据都没了,那整个公司的业务也就完蛋了。就像当年的扁鹊一样,无法挽回,唯有跑路了。扩展为强调后果的严重性。

Fat Finger

胖胖的手指。因为手指粗,所以误敲到了回车键,引发了诸如删库这样的事故。扩展为人为的操作不小心。

Repository / Repo

仓库,托管代码的地方。

Branch

分支。多人在同一个仓库上工作时,为了避免互相影响到对方,会在不同的分支上进行开发。

Trunk

主干或者叫主干分支。通常主干的分支名字叫 main 或者 master。主干也是一个仓库的代码主线,大家在不同的分支上开发,但某一个时刻,还是要把开发的代码合并进入主干。另外要开启新的分支时,通常也会基于主干进行分叉。当然也可以基于其他分支进行进一步分叉,但最终在某一个时间点,代码还是要合并回主干的。

主干开发 (Trunk-based development)

一种强调迭代速度的开发模式。要求大家尽可能频繁地把开发完的功能合并进主干,但同时也要尽量保证主干的健康。如果主干经常有问题的话,团队就无法基于主干开启新的分支,进行新的研发任务。所以主干开发会强调小步快跑,高频提交小规模变更。

Cherrypick

有时候你只想从一个分支里挑选一部分的内容合并到另一个分支,这个动作就像是捡樱桃一样。落水三千,只取一瓢。

Release Train

发布列车。每隔一段时间,软件就会进行发布,发布负责人往往会规定一个时间点,如果希望让自己的功能赶上这次的发布,就需要在这个时间点前把代码提交,不然发布列车就开走了。不过现实中,发布列车往往要等某一个 VIP 功能,会推迟发车时间。

TODO

在代码里记录一个将来再解决的问题,但至于什么时候去解决,谁知道呢。

Postmortem

原义是验尸报告。在软件研发领域,在发生事故后,详细的故障分析报告,报告最后通常会留好几个 TODO。

On-call (Carry the pager)

值班,以前带着的传呼机叫做 Pager。现在传呼机被手机/软件取代了,但 Pager 这个名字沿用了下来。

Spaghetti code

形容代码乱成一团,像通心粉一样交织在一起。

PR

不是指公关 (Public Relations),而是 Pull Request。GitHub 里要像某一个分支合并代码时,提交的合并请求。而在 GitLab 里,对应的概念叫 Merge Request (MR)。你看出来了,这两家老对着干,倒霉的是我们这帮程序员。

虚构概念

ACME

一家虚构的公司,一般文档例子里要用到一家子虚乌有的公司时,会用这个名字。

Alice and Bob

一组虚构人物,起源于网络安全,交换信息的双方。

example.com

访问这个网站就知道是用来干嘛的了。

Foo, Bar, Baz

一组没有意义的占位符(Placeholder)来指代某种概念。失去自我,成就大我。

缩略语

LGTM

Looks Good To Me。通常用于审核(Review)流程中,比如代码审核,文档审核。审核人(Reviewer)用来表示认同,批准👍。

PTAL

Please Take Another Look。通常用于审核(Review)流程中,比如代码审核,文档审核。发起人(Author)告知审核人(Reviewer)再次审核。

Nit

Nitpicking。吹毛求疵,通常用于审核(Review)流程中,比如代码审核,文档审核。审核人(Reviewer)提出的一些不影响核心内容,或者带有主观判断的建议。

WDYT

What Do You Think。在双方问题讨论中,在提出自己观点后, 一种启发式的希望获得对方反馈的表达方式。

YMMV

Your Mileage May Vary。在分享完自己的经验和观点后,也提醒他人要思辨地吸收。

EOF

End Of File。标识一个文件结尾的符号。扩展为表示一件事情的终结。

IMO / IMHO

In My Opinion / In My Humble Opinion。在表达自己观点前,为了降低对方抵触情绪,附加的谦逊语气。但也不要指望加了这个,就能避免引起对方的不适。

WYSIWYG

What You See Is What You Get。所见即所得,一种提供更好用户体验的交互方式。

TGIF

Thank God It's Friday 🪩!

TIL

Today I Learned.

Happy 1024! 早点下班!🎉


作者:Bytebase
来源:mp.weixin.qq.com/s/WRANBcZ69_COkZfnZY0pCQ

收起阅读 »

工作了5年你居然不知道版本号有这些规范?

前言 所谓语义化版本控制,就是要求你的版本号能按照特定规则及条件来进行约束,以期达到见到版本号即能了解其修改内容的信息或相邻版本间的更新迭代关系。通过阅读本文,你将能够对语义化版本控制规范能够有一个全面的了解,同时也对各平台上依赖版本时的语法有个大体的了解。 ...
继续阅读 »



前言


所谓语义化版本控制,就是要求你的版本号能按照特定规则及条件来进行约束,以期达到见到版本号即能了解其修改内容的信息或相邻版本间的更新迭代关系。通过阅读本文,你将能够对语义化版本控制规范能够有一个全面的了解,同时也对各平台上依赖版本时的语法有个大体的了解。


背景


在正式开始之前,先问大家几个问题:


我们经常在类似 Github、npm、或者 pub.dev 上看到一些软件或者库的版本号包含如下信息,你是否会疑惑他们之间的区别是什么?分别适用什么场景?



  • alpha

  • beta

  • rc

  • release


再看看下面几组版本号,你是否能弄清楚各个版本号之间谁更新更大?



  • 1.0.0 1.0.1 1.1.0

  • 1.0.0-beta 1.0.0

  • 1.0.0-release 1.0.0

  • 1.0.0-alpha 1.0.0-alpha.1

  • 1.0.0-alpha 1.0.0-rc.1


这次将借着我们在做组件管理平台的机会,像大家介绍一下日常软件开发中的语义化版本控制规范。相信通过下面的学习,上述的问题也能够迎刃而解。


常见先行版本号标识


上面说到 alpha、beta、rc、release 等版本号中常见的一些标识,有一个正式的名称叫做:先行版本号标识。我们可以通过一个生活中的例子来通俗易懂的说明它们之间的差异和联系。


现在假设你是一个蛋糕店的老板,你打算给你的蛋糕店推出一个新品,那么上述所谓的先行版本号就是如下几个阶段的蛋糕:


Alpha 版就是你对于你蛋糕的最终形式还在脑海当中,只有一个蛋糕的基本样子,口味应该是什么味道你心里还没谱,对于装饰如奶油、水果还有蜡烛这些甚至都还没有放在一起(你的软件各功能甚至都没有打通)。由于过于简陋,并且口味还没固定,你还不能将其给你的顾客品尝。你只能自己反复摸索尝试,或者让自己店里的员工对口味、外观以及一些缺陷进行点评。


Alpha版蛋糕


Beta 版就是你的蛋糕已经开始尝试将部分奶油涂抹在蛋糕上,你已经尝试将所有的元素组装起来,这时候的蛋糕还处于不能拿出去卖的阶段,但口味和后续方向已经基本固定。你甚至可以邀请你店里的熟客来参加小规模的试吃活动,并让他们针对你的这款蛋糕进行全方面的点评。


Beta版蛋糕


RC 版就是你的蛋糕已经基本做完了,其最核心的口味和外观已经确定下来,你可以再检查一下蛋糕是否有裂缝、哪些地方需要针对性的进行一些美化或修补。


RC版蛋糕


release 版就是你已经把蛋糕装饰好了,插上蜡烛,撒上曲奇,进行裱花。这时候蛋糕已经完成了,你可以正式的将这块蛋糕摆上橱窗,向大家兜售你的艺术品了。


release版蛋糕


通过上述的蛋糕制作过程,你应该对这些先行版本号标识有了自己的认知。接下来我们再总结下这些先行版本号标识的常见含义:


标识常见含义
alpha(α)内部测试版(有些也叫 A 测版)。α 是希腊字母的第一个,表示最早的版本,一般此类版本包含很多 BUG ,功能也不全,主要面向的是开发人员和测试人员。
beta(β)公开测试版(有些也叫 B 测版)。 β 是希腊字母的第二个,因此这个版本比alpha版发布较晚一些,主要是给参与内部体验的用户测试用,该版本仍然存在很多 BUG ,但是相对 alpha 版要稳定一些。此时,基本功能已经固定,但仍然可能增加新功能。
rc(Release Candidate)rc (候选版本),该版本又较 beta 版更进一步了,该版本功能不再增加,和最终发布版功能一样。
这个版本的作用是提前预览即将发行版本的内容,并且在该版本后,最终发行版的发布也迫在眉睫了。
release稳定版(有些也叫做 stable、GA 版)。在开源软件中,都有正式版,这个就是开源软件的最终发行版,用户可以放心大胆的用了。

相信阅读到这里,上面的第一个问题你已经有了答案。那么明白这些标识的具体含义之后,它到底应该怎么用呢?具体要放在版本号里的哪个位置上呢?接下来我们将通过对语义化版本控制规范的详细介绍,来帮助你解答这些疑惑。


何为语义化版本控制规范


在介绍什么是语义化版本控制规范之前,我们先需要了解为什么需要语义化版本控制规范。


大家先设身处地的设想这样一个开发场景:


你现在的项目现在分别依赖了 foo : 1.0.0bar : 2.0.0baz : 3.0.0


项目example_app的依赖项


同时 foo 组件也依赖了 bar : 2.0.0baz : 3.0.0


组件foo的依赖项


同时 bar 组件也依赖了 baz : 3.0.0


组件bar的依赖项


现在你很幸运,项目可以跑起来。


突然有一天因为要修改一个问题,需要升级你项目中 baz 组件的版本号,需要将它从 3.0.0 升级到 3.0.1。但很不幸的是,baz 组件这个小小的版本升级却发生了破坏性的 API 改动。然后你发现你不仅需要修改主工程 example_app 的版本号,还需要升级 foo 组件的版本号以及 bar 组件的版本号。而在你做完这些之后,发现 foo 依赖的其他组件的版本又和你主工程 example_app 项目中依赖的组件的版本冲突了,于是你崩溃了。


这就是软件管理领域中被称作“依赖地狱”的死亡之谷。即当你的系统规模越大,引入的包越多,你就越可能遇到由于依赖导致的问题:



  • 如果依赖关系过高,可能面临版本控制被锁死的风险(必须对每一个依赖包改版才能完成某次升级)

  • 而如果依赖关系过于松散,又将无法避免版本的混乱(假设兼容于未来的多个版本已超出了合理数量) 当你项目的进展因为版本依赖被锁死或版本混乱变得不够简便和可靠,就意味着你正处于依赖地狱之中。


通过上述的场景我们可以看到,版本号的管理(包括依赖关系的控制以及版本号的命名)并不是一个随心所欲的事情:管理好了,能给你带来极大便利,反之则会给你的开发带来很多没必要的麻烦。那么我们应该如何解决这些事情呢?


基于上述的一些问题,Gravatars 及 Github 的创始人之一的 Tom Preston-Werner 提出了一个名为语义化版本控制规范(Semantic Versioning)的解决方案,它期望用一组简单的规则及条件来约束版本号的配置和增长。这套规则是根据现在各种封闭、开源软件所广泛使用的版本号命名规则所设计。为了让这套理论运作,必须要定义好你的公共 API。一旦定义了公共 API,你就可以透过修改相应的版本号来向大家说明你的修改。考虑使用这样的版本号格式:



版本格式:主版本号.次版本号.修订号,版本号递增规则如下:



  1. 主版本号:当你做了不兼容的 API 修改

  2. 次版本号:当你做了向下兼容的功能性新增

  3. 修订号:当你做了向下兼容的问题修正


先行版本号版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。



以上这套规则或者说系统,就是所谓的”语义化版本控制”,在这套规则之下,版本号以及版本号的更新都潜在包含了代码的修改信息,而这套规则被简称为 semver (Semantic Versioning 简写)。


接下来我将基于 semver 2.01 向大家介绍 这套规则的一些细则。


语义化版本控制规范细则


语义化版本控制规范的细则有很多,有兴趣的同学可以直接到semver 2.01 的官方文档 查看即可,我们这里将其主要内容总结给大家。


X.Y.Z(主版本号.次版本号.修订号)修复问题但不影响 API 时,递增修订号;API 保持向下兼容的新增及修改时,递增次版本号;进行不向下兼容的修改时,递增主版本号。


一个标准的语义化版本号各部分的含义


其实所谓语义化版本控制规范,本来也只是一种约定,它并不能完美适合每一个团队,我们也没必要完全生搬硬套,这里以 Google 官方推出的 mockito2 的版本号为例,可以看到其也没有严格按照细则进行遵守。


组件mockito的一些非正式版本号


所以如果你团队内已经统一认知,了解版本号中每个地方代表的含义,做到“见号知意”:看到 1.0.0-npeFixed.8 就知道这个组件是从 1.0.0 拉出来 为了修复 NPE 的;看到 2.3.0-addFaceIdSupport.1 就知道这个组件是基于 2.3.0 来做 FaceId 支持的;见到 5.0.0-nullSafety.6 就知道这个版本是为了空安全的。那么我们的语义化版本控制规范的目的也就达到了,不是吗?


版本语法


就像人类的烹饪方式从最开始的单纯用火烤到发明陶器之后的烹煮,再到现代社会中基于烤、煮、蒸而演化出来的各类五花八门的烹饪方式一样,语义化版本控制规范在各个平台上也衍生出不同的版本规范和版本语法(Version Syntax),但万变不离其宗。接下来我将大致介绍下常见平台版本语法的异同,期望能对你有所帮助。



由于 PyPI上的版本规范及版本说明符比较特殊且繁琐,这里就不进行比对,有兴趣的同学可以查看PEP 440 – Version Identification and Dependency Specification3 了解更多细节。



和烹饪方式的的演化过程一样, 语义化版本控制规范在不同平台、不同时期也有不同的表现


定义平台格式示例描述
完全匹配目标版本gradleversion

version!!
com.example:foo:5.0.2

com.example:foo:5.0.2!!
这里用5.0.2 和 5.0.2!! 是有区别的。
5.0.2 这种写法是对此版本最低要求,但是可以被引擎升级,这里的5.0.2是被称作必须版本(Required version4), 也就是说它最小版本是5.0.2,并且相信未来的任何升级都是可以正常工作的。

而5.0.2!! 则是严格版本(Strict version5), 即只能使用5.0.2的版本,传递依赖项过来的版本如果没有更高约束或者别的严格版本,会被覆写为此版本,否则会失败。
mavenversion

[version]
5.0.2

[5.0.2]
和gradle类似,这里用5.0.2 和 [5.0.2] 是有区别的。
5.0.2 这种写法是对此版本的软要求(Soft requirement6),如果依赖关系树中较早没有出现其他版本,则使用 5.0.2。

而 [5.0.2] 这种写法是对此版本的硬性要求(Hard requirement6)。使用 5.0.2并且仅使用 5.0.2。
pubversionfoo: 5.0.2
npmversionfoo: 5.0.2
podversion

= version
pod 'foo', '5.0.2'

pod 'foo', '=5.0.2'
兼容版本gradleversion.+com.example:foo:1.+>= 1.0.0 < 2.0.0
maven[version, version+1)[1.0.0, 2.0.0)同上
pub^version

~version
foo: ^1.0.0

foo: ~1.0.0
>= 1.0.0 < 2.0.0

>=1.0.0 < 1.1.0

^version 和 ~version 分别被称作 插入符语法(Caret Syntax7) 和 波形语法(Tilde Syntax8),他们的主要区别在于前者兼容当前版本后及后续所有的 次版本号及修订号,即 ^X.Y.Z 等价于 >=X.Y.Z
<(X+1).0.0;

而后着只兼容当前版本号及后续所有的修订号,即
~X.Y.Z 等价于 >=X.Y.Z
npm^versionfoo: ^1.0.0同上
pod~> versionpod 'foo', '~> 1'同上
匹配任意版本gradlecom.example:foo任意一个版本,具体约束可能由其他组件依赖了此组件并且存在具体约束,否则默认取最新的
maven[firstVersion,)[0.0.1,)>=0.0.1
pubanyfoo: any任意一个版本,具体约束可能由其他组件依赖了此组件并且存在具体约束,否则默认取最新的
npm*foo: *同上
podpod 'foo'同上
已发布的最新版本gradle+com.example:foo:+任意一个版本,具体约束可能由其他组件依赖了此组件并且存在具体约束,否则默认取最新的
mavenLATEST

LATESTLATEST 在maven 3.x版本被废弃
pubanyfoo: any任意一个版本,具体约束可能由其他组件依赖了此组件并且存在具体约束,否则默认取最新的
npm*foo: *同上
pod> 0.0.1pod 'foo', '>0.0.1'同上
大于当前版本gradle(version, )com.example:foo: (0.0.1, )
maven(version, )(0.0.1, )
pub>versionfoo: >0.0.1
npm> versionfoo: > 0.0.1
pod> versionpod 'foo', '> 0.0.1'
大于等于当前版本gradle[version, )com.example:foo: [0.0.1, )
maven[version, )[0.0.1, )
pub>=versionfoo: >=0.0.1
npm>= versionfoo: >= 0.0.1
pod>= versionpod 'foo', '>= 0.0.1'
小于当前版本gradle(, version)com.example:foo: (, 2.0.0)
maven(, version)(, 2.0.0)
pubfoo: <2.0.0
npm< versionfoo: < 2.0.0
pod< versionpod 'foo', '< 2.0.0'
小于等于当前版本gradle(, version]com.example:foo: (, 2.0.0]
maven(, version](, 2.0.0]
pub<=versionfoo: <=2.0.0
npm<= versionfoo: <= 2.0.0
pod<= versionpod 'foo', '<= 2.0.0'
范围区间gradle[version1, version2]com.example:foo: [1.0.0, 2.0.0]
maven[version1, version2][1.0.0, 2.0.0]
pub'>=version1 <=version2 'foo: '>=1.0.0 <=3.0.0'当存在区间约束的时候,版本号需要通过单引号进行包裹
npmversion1-version2

>=version1 <=version2
foo: 1.0.0-3.0.0


foo: >=1.0.0 <=3.0.0
version1 到 version的任意版本号,包含自身
pod>=version1, <=version2pod 'foo', '>= 1.0.0' , '<= 3.0.0'
范围集合gradle(,version1), [version2,)com.example:foo:
(,1.0.0),[3.0.0,)
< 1.0.0 或者 >= 3.0.0
maven(,version1), [version2,)(,1.0.0),[3.0.0,)同上
pub不支持不支持不支持
npmversion1version2foo: <1.0.0>= 3.0.0< 1.0.0 或者 >= 3.0.0
pod=version2pod 'foo', '< 1.0.0' , '>= 3.0.0'同上
排除制定版本gradle(,version), (version,)com.example:foo:
(,1.0.5),(1.0.5,)
不等于 1.0.5
maven(,version), (version,)(,1.0.5),(1.0.5,)同上
pub不支持不支持不支持
npm>versionfoo: <1.0.5>1.0.5不等于 1.0.5
pod!= versionpod 'foo', '!= 1.0.5'不等于 1.0.5
特有gradlemaven特殊版本标识: -SNAPSHOTcom.example:foo: 1.0.0-SNAPSHOT这个其实是maven的特殊版本标识,当你发布此带-SNAPSHOT标识版本后,maven自己会根据你的发布时间将版本展开为类似于1.0-yyyyMMdd-HHmmss-1 的格式,所以如果你带了此标识,你可以重复发布此版本,当前前提是你的maven开启了对应的配置。
其他特殊标识
dev

rc\snapshot\final\ga\release\sp
1. dev会被判定低于任何其他非数字部分,如:
1.0.0-dev < 1.0.0-ALPHA < 1.0.0-alpha < 1.0-rc
2. 字符串rc,snapshot,final,ga,release和 sp 被认为高于其他字符串部分(按此顺序排序),如:
1.0-zeta < 1.0-rc < 1.0-snapshot < 1.0-final < 1.0-ga < 1.0-release < 1.0-sp < 1.0


有些平台中还有一些特定的其他语法和规则,如果感兴趣,可以点击平台名称的超链接进入对应平台的官方文档自行查看。



相信你读到了这里,对语义化版本控制规范已经了然于胸。那么开篇的两个问题你是否也有了答案,欢迎在评论区留言。


Q&A 环节


经过上面的分享,相信大家对语义化版本已经有了一个整体的了解,那么我们来检验一下你的学习效果,请尝试回答下面几个问题:


Q:“v1.2.3” 是一个语义化版本号吗?


首先,“v1.2.3” 并不是的一个语义化的版本号。但是,在语义化版本号之前增加前缀 “v” 是用来表示版本号的常用做法。在版本控制系统中,将 “version” 缩写为 “v” 是很常见的。但是我们可以通过 npm-semver 来进行处理并转化成语义化的版本。


npm-semver可以帮你处理和转化语义化版本


Q:这么多规则及要求,我该如何验证我的语义化版本是否符合规范或者比较他们之间的大小关系呢?


这里就推荐 npm 的 github.com/npm/node-se…


node-semver 也可以帮你做到


对于脚本上对版本是否符合要求进行验证,可以使用 semver 2.0 文档中推荐的如下两个正则表达式。


第一个用于支持按组名称提取的语言,PCRE(Perl 兼容正则表达式,比如 Perl、PHP 和 R)、Python 和 Go。参见: regex101.com/r/Ly7O1x/3/



/^(?P0|[1-9]\d*).(?P0|[1-9]\d*).(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:+(?P[0-9a-zA-Z-]+(?:.[0-9a-zA-Z-]+)*))?$/gm



第二个用于支持按编号提取的语言(与第一个对应的提取项按顺序分别为:major、minor、patch、prerelease、buildmetadata)。主要包括 ECMA Script(JavaScript)、PCRE(Perl 兼容正则表达式,比如 Perl、PHP 和 R)、Python 和 Go。 参见: regex101.com/r/vkijKf/1/



/^(0|[1-9]\d ).(0|[1-9]\d).(0|[1-9]\d )(?:-((?:0|[1-9]\d|\d*a-zA-Z-)(?:.(?:0|[1-9]\d |\da-zA-Z- )) ))?(?:+([0-9a-zA-Z-]+(?:.[0-9a-zA-Z-]+)))?$/gm



Q:万一不小心把一个不兼容的改版当成了次版本号发行了,或者在修订等级的发布中,误将重大且不兼容的改变加到代码之中,我能通过重复发布当前版本来解决问题吗?


首先必须强调一点,不管如何都不能去修改已发行的版本(这点在部分平台已经帮你处理掉了,例如 pub 本身已经做了这种限制)。然后最好根据场景升级一个对应级别的版本来回滚逻辑,最后再将你的重大且不兼容的改版升一个主版本号进行发布。记住,语义化的版本控制就是透过版本号的改变来传达意义。


尾声


至此,我们已经了解了语义化版本控制规范的具体细则,常用的先行版本号标识的含义及应用场景,希望能在大家日后的工作生活当中所有帮助。你还见过哪些常见的先行版本号,你们团队又是如何避免包依赖地狱的,欢迎在评论区补充。感谢大家的观看,再见。


作者:政采云技术
来源:juejin.cn/post/7278238875456684090
收起阅读 »