注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

项目中前端如何实现无感刷新 token!

前一阵遇到了一个问题,线上平台有时会出现用户正在使用的时候,突然要用户去进行登录,这样会造成很不好的用户体验,但是当时一直没有好的思路因此搁置了下来;通过零散时间查询资料以及思考,最终解决了这个问题,接下来跟大家分享一下! 环境请求采用的 Axios V1.3...
继续阅读 »

前一阵遇到了一个问题,线上平台有时会出现用户正在使用的时候,突然要用户去进行登录,这样会造成很不好的用户体验,但是当时一直没有好的思路因此搁置了下来;通过零散时间查询资料以及思考,最终解决了这个问题,接下来跟大家分享一下!


环境

  1. 请求采用的 Axios V1.3.2。
  2. 平台的采用的 JWT(JSON Web Tokens) 进行用户登录鉴权。
    (拓展:JWT 是一种认证机制,让后台知道该请求是来自于受信的客户端;更详细的可以自行查询相关资料)

问题现象


线上用户在使用的时候,偶尔会出现突然跳转到登录页面,需要重新登录的现象。


原因

  1. 突然跳转到登录页面,是由于当前的 token 过期,导致请求失败;在 axios 的响应拦截axiosInstance.interceptors.response.use中处理失败请求返回的状态码 401,此时得知token失效,因此跳转到登录页面,让用户重新进行登录。
  2. 平台目前的逻辑是在 token 未过期内,用户登录平台可直接进入首页,无需进行登录操作;因此就存在该现象:用户打开平台,由于此时 token 未过期,用户直接进入到了首页,进行其他操作。但是在用户操作的过程中,token 突然失效了,此时就会出现突然跳转到登录页面,严重影响用户的体验感!
    注:目前线上项目中存在数据大屏,一些实时数据的显示;因此存在用户长时间停留在大屏页面,不进行操作,查看实时数据的情况

切入点

  1. 怎样及时的、在用户感知不到的情况下更新token
  2. 当 token 失效的情况下,出错的请求可能不仅只有一个;当失效的 token 更新后,怎样将多个失败的请求,重新发送?

操作流程


好了!经过了一番分析后,我们找到了问题的所在,并且确定了切入点;那么接下来让我们实操,将问题解决掉。

前要:

1、我们仅从前端的角度去处理。

2、后端提供了两个重要的参数:accessToken(用于请求头中,进行鉴权,存在有效期);refreshToken(刷新令牌,用于更新过期的 accessToken,相对于 accessToken 而言,它的有效期更长)。


1、处理 axios 响应拦截


注:在我实际的项目中,accessToken 过期后端返回的 statusCode 值为 401,需要在axiosInstance.interceptors.response.useerror回调中进行逻辑处理

// 响应拦截
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
let {
data, config
} = error.response;
return new Promise((resolve, reject) => {
/**
* 判断当前请求失败
* 是否由 toekn 失效导致的
*/
if (data.statusCode === 401) {
/**
* refreshToken 为封装的有关更新 token 的相关操作
*/
refreshToken(() => {
resolve(axiosInstance(config));
});
} else {
reject(error.response);
}
})
}
)


  1. 我们通过判断statusCode来确定,是否当前请求失败是由token过期导致的;

  2. 使用 Promise 处理将失败的请求,将由于 token 过期导致的失败请求存储起来(存储的是请求回调函数,resolve 状态)。理由:后续我们更新了 token 后,可以将存储的失败请求重新发起,以此来达到用户无感的体验


补充:


现象:在我过了几天登录平台的时候发现,refreshToken过期了,但是没有跳转到登录界面
原因

1、当refreshToken过期失效后,后端返回的状态码也是 401

2、发起的更新token的请求采用的也是处理后的axios,因此响应失败的拦截,对更新请求同样适用

问题:

这样会造成,当refreshToken过期后,会出现停留在首页,无法跳转到登录页面。

解决方法

针对这种现象,我们需要完善一下axios中响应拦截的逻辑

axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
let {
data, config
} = error.response;
return new Promise((resolve, reject) => {
/**
* 判断当前请求失败
* 是否由 toekn 失效导致的
*/
if (
data.statusCode === 401 &&
config.url !== '/api/token/refreshToken'
) {
refreshToken(() => {
resolve(axiosInstance(config));
});
} else if (
data.statusCode === 401 &&
config.url === '/api/token/refreshToken'
) {
/**
* 后端 更新 refreshToken 失效后
* 返回的状态码, 401
*/
window.location.href = `${HOME_PAGE}/login`;
} else {
reject(error.response);
}
})
}
)

2、封装 refreshToken 逻辑


要点:



  1. 存储由于token过期导致的失败的请求。
  2. 更新本地以及axios中头部的token
  3. 当 refreshToken 刷新令牌也过期后,让用户重新登录
// 存储由于 token 过期导致 失败的请求
let expiredRequestArr: any[] = [];

/**
* 存储当前因为 token 失效导致发送失败的请求
*/
const saveErrorRequest = (expiredRequest: () => any) => {
expiredRequestArr.push(expiredRequest);
}

// 避免频繁发送更新
let firstRequre = true;
/**
* 利用 refreshToken 更新当前使用的 token
*/
const updateTokenByRefreshToken = () => {
firstRequre = false;
axiosInstance.post(
'更新 token 的请求',
).then(res => {
let {
refreshToken, accessToken
} = res.data;
// 更新本地的token
localStorage.setItem('accessToken', accessToken);
// 更新请求头中的 token
setAxiosHeader(accessToken);
localStorage.setItem('refreshToken', refreshToken);

/**
* 当获取了最新的 refreshToken, accessToken 后
* 重新发起之前失败的请求
*/
expiredRequestArr.forEach(request => {
request();
})
expiredRequestArr = [];
}).catch(err => {
console.log('刷新 token 失败err', err);
/**
* 此时 refreshToken 也已经失效了
* 返回登录页,让用户重新进行登录操作
*/
window.location.href = `${HOME_PAGE}/login`;
})
}

/**
* 更新当前已过期的 token
* @param expiredRequest 回调函数,返回由token过期导致失败的请求
*/
export const refreshToken = (expiredRequest: () => any) => {
saveErrorRequest(expiredRequest);
if (firstRequre) {
updateTokenByRefreshToken();
}
}

补充:


问题:

1、怎么能保证当更新token后,在处理存储的过期请求时,此时没有过期请求还在存呢?;万一此时还在expiredRequestArr推失败的请求呢?

解决方法
我们需要调整一下更新 token的逻辑,确保当前由于过期失败的请求都接收到了,再更新token然后重新发起请求。


最终结果:

// refreshToken.ts

/**
* 功能:
* 用于实现无感刷新 token
*/
import { axiosInstance, setAxiosHeader } from "@/axios"
import { CLIENT_ID, HOME_PAGE } from "@/systemInfo"

// 存储由于 token 过期导致 失败的请求
let expiredRequestArr: any[] = [];

/**
* 存储当前因为 token 失效导致发送失败的请求
*/
const saveErrorRequest = (expiredRequest: () => any) => {
expiredRequestArr.push(expiredRequest);
}

/**
* 执行当前存储的由于过期导致失败的请求
*/
const againRequest = () => {
expiredRequestArr.forEach(request => {
request();
})
clearExpiredRequest();
}

/**
* 清空当前存储的过期请求
*/
export const clearExpiredRequest = () => {
expiredRequestArr = [];
}

/**
* 利用 refreshToken 更新当前使用的 token
*/
const updateTokenByRefreshToken = () => {
axiosInstance.post(
'更新请求url',
{
clientId: CLIENT_ID,
userName: localStorage.getItem('userName')
},
{
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Authorization': 'bearer ' + localStorage.getItem("refreshToken")
}
}
).then(res => {
let {
refreshToken, accessToken
} = res.data;
// 更新本地的token
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
setAxiosHeader(accessToken);
/**
* 当获取了最新的 refreshToken, accessToken 后
* 重新发起之前失败的请求
*/
againRequest();
}).catch(err => {
/**
* 此时 refreshToken 也已经失效了
* 返回登录页,让用户重新进行登录操作
*/
window.location.href = `${HOME_PAGE}/login`;
})
}

let timer: any = null;
/**
* 更新当前已过期的 token
* @param expiredRequest 回调函数,返回过期的请求
*/
export const refreshToken = (expiredRequest: () => any) => {
saveErrorRequest(expiredRequest);
// 保证再发起更新时,已经没有了过期请求要进行存储
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
updateTokenByRefreshToken();
}, 500);
}
// 响应拦截 区分登录前
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
let {
data, config
} = error.response;
return new Promise((resolve, reject) => {
/**
* 判断当前请求失败
* 是否由 toekn 失效导致的
*/
if (
data.statusCode === 401 &&
config.url !== '/api/token/refreshToken'
) {
refreshToken(() => {
resolve(axiosInstance(config));
});
} else if (
data.statusCode === 401 &&
config.url === '/api/token/refreshToken'
) {
/**
* 后端 更新 refreshToken 失效后
* 返回的状态码, 401
*/
clearExpiredRequest();
window.location.href = `${HOME_PAGE}/login`;
} else {
reject(error.response);
}
})
}
)

补充


感谢很多朋友提出了很多更好的方法;我写这篇文章主要是为了分享一下,恰好这种问题推到了我(前端工程师)身上,我是怎样处理的;虽然有可能在一些朋友看来很低级,但它确是我实际工作中碰到的问题,每一个问题的出现解决后都对自身是一种成长,通过分享的方式来巩固自己,也希望能对他人有一些帮助!


总结


经过一波分析以及操作,我们最终实现了实际项目中的无感刷新token,最主要的是有效避免了:用户在平台操作过程中突然要退出登录的现象(尤其是当用户进行信息填写,突然要重新登录,之前填写的信息全部作废,是很容易让人发狂的)。

其实回顾一下,技术上并没有什么难点,只是思路上自己是否能够想通、自洽。人是一棵会思想的芦苇,我们要有自己的思想,面对问题,有自己的思考。

希望我们能在技术的路上走的越来越远,与君共勉!!!


作者:fly_dream
链接:https://juejin.cn/post/7254572706536734781
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

假如面试官让你讲一下新版雪花算法

这是一篇由 Seata 的官网上一篇叫做“关于新版雪花算法的答疑”的文章引发的思考。 seata.io/zh-cn/blog/… 还是有点意思的,结合自己的理解和代码,加上画几张图来拆解一下 Seata 里面的“改良版雪花算法”。 虽然是在 Seata ...
继续阅读 »

这是一篇由 Seata 的官网上一篇叫做“关于新版雪花算法的答疑”的文章引发的思考。



seata.io/zh-cn/blog/…



image.png


还是有点意思的,结合自己的理解和代码,加上画几张图来拆解一下 Seata 里面的“改良版雪花算法”。


虽然是在 Seata 里面看到的,但是本篇文章的内容和 Seata 框架没有太多关系,反而和数据库的基础知识有关。


所以,即使你不了解 Seata 框架,也不影响你阅读。


当你理解了这个类的工作原理之后,你完全可以把这个只有 100 多行的类搬运到你的项目里面,然后就变成你的了。


dddd(懂的都懂)!



先说问题



雪花算法的使用场景:


如果你的项目中涉及到需要一个全局唯一的流水号,比如订单号、流水号之类的,又或者在分库分表的情况下,需要一个全局唯一的主键 ID 的时候,就需要一个算法能生成出这样“全局唯一”的数据。


一般来说,我们除了“全局唯一”这个基本属性之外,还会要求生成出来的 ID 具有“递增趋势”,这样的好处是能减少 MySQL 数据页分裂的情况,从而减少数据库的 IO 压力,提升服务的性能。


此外,在当前的市场环境下,不管你是啥服务,张口就是高并发,我们也会要求这个算法必须得是“高性能”的。


雪花算法,就是一个能生产全局唯一的、递增趋势的、高性能的分布式 ID 生成算法。


关于雪花算法的解析,网上相关的文章比雪花还多,这个玩意,应该是“面试八股文”中重点考察模块,分布式领域中的高频考题之一,如果是你的盲区的话,可自行去了解一下。


比如一个经典的面试题就是:


雪花算法最大的缺点是什么?


背过题的小伙伴应该能立马答出来:时钟敏感


因为在雪花算法中,由于要生成单调递增的 ID,因此它利用了时间的单调递增性,所以是强依赖于系统时间的。


如果系统时间出现了回拨,那么生成的 ID 就可能会重复。


而“时间回拨”这个现象,是有可能出现的,不管是人为的还是非人为的。


当你回答出这个问题之后,面试官一般会问一句:


那如果真的出现了这种情况,应该怎么办呢?


很简单,正常来说只要不是不是有人手贱或者出于泄愤的目的进行干扰,系统的时间漂移是一个在毫秒级别的极短的时间。


所以可以在获取 ID 的时候,记录一下当前的时间戳。然后在下一次过来获取的时候,对比一下当前时间戳和上次记录的时间戳,如果发现当前时间戳小于上次记录的时间戳,所以出现了时钟回拨现象,对外抛出异常,本次 ID 获取失败。


理论上当前时间戳会很快的追赶上上次记录的时间戳。


但是,你可能也注意到了,“对外抛出异常,本次 ID 获取失败”,意味着这段时间内你的服务对外是不可使用的。


比如,你的订单号中的某个部分是由这个 ID 组成的,此时由于 ID 生成不了,你的订单号就生成不了,从而导致下单失败。


再比如,在 Seata 里面,如果是使用数据库作为 TC 集群的存储工具,那么这段时间内该 TC 就是处于不可用状态。


你可以简单的理解为:基础组件的错误导致服务不可用。



再看代码



基于前面说的问题,Seata 才提出了“改良版雪花算法”。



seata.io/zh-cn/blog/…



图片


在介绍改良版之前,我们先把 Seata 的源码拉下来,瞅一眼。


在源码中,有一个叫做 IdWorker 的类:



io.seata.common.util.IdWorker



再来看一下它的提交记录:


图片


2020 年 5 月 4 日第一次提交,从提交时的信息可以看出来,这是把分布式 ID 的生成策略修改为 snowflake,即雪花算法。


同时我们也能在代码中找到前面提到的“对外抛出异常,本次 ID 获取失败”相关代码,即 nextId  方法,它的比较方式就是用当前时间戳和上次获取到的时间戳做对比:



io.seata.common.util.IdWorker#nextId



图片


这个类的最后一次提交是 2020 年 12 月 15 日:


图片


这一次提交对于 IdWorker 这个类进行了大刀阔斧的改进,可以看到变化的部分非常的多:


图片


我们重点关注刚刚提到的 nextId 方法:


图片


整个方法从代码行数上来看都可以直观的看到变化,至少没有看到抛出异常了。


这段代码到底是怎么起作用的呢?


首先,我们得理解 Seata 的改良思路,搞明白思路了,再说代码就好理解一点。


在前面提到的文章中 Seata 也说明了它的核心思路:


图片


原版的雪花算法 64 位 ID 是分配这样的:


图片


可以看到,时间戳是在最前面的,因为雪花算法利用了时间的单调递增的特性。


所以,如果前面的时间戳一旦出现“回退”的情况,即打破了“时间的单调递增”这个前提条件,也就打破了它的底层设计。


它能怎么办?


它只能给你抛出异常,开始摆烂了。


可以看到节点 ID 长度为二进制的 10 位,也就是说最多可以服务于 1024 台机器,所以你看 Seata 最开始提交的版本里面,有一个在 1024 里面随机的动作。


因为算法规定了,节点 ID 最多就是 2 的 10 次方,所以这里的 1024 这个值就是这样来的:


图片


包括后面有大佬觉得用这个随机算法一点都不优雅,就把这部分改成了基于 IP 去获取:


图片


看起来有点复杂,但是我们仔细去分析最后一行:



return ((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF);



变量 & 0B11 运算之后的最大值就是 0B11 即 3。


Byte.SIZE = 8。


所以,3 << 8,对应二进制 1100000000,对应十进制 768。


变量 & 0xFF 运算之后的最大值就是 0xFF 即 255。


768+255=1023,取值范围都还是在 [0,1023] 之间。


然后你再看现在最新的算法里面,官方的老哥们认为获取 IP 的方式不够好:


图片


所以又把这个地方从使用 IP 地址改成了获取 Mac 地址。


图片


最后一行是这样的:



return ((mac[4] & 0B11) << 8) | (mac[5] & 0xFF);



那么理论上的最大值就是 768 | 255 ,算出来还是 1023。


所以不管你怎么玩出花儿来,这个地方搞出来的数的取值范围就只能是 [0,1023] 之间。


别问,问就是规定里面说了,算法里面只给节点 ID 留了 10 位长度。


最后,就是这个 12 位长度的序列号了:


图片


这个玩意没啥说的,就是一个单纯的、递增的序列号而已。


既然 Seata 号称是改良版,那么具体体现在什么地方呢?


简单到你无法想象:


图片


是的,仅仅是把时间戳和节点 ID 换个位置就搞定了。


然后每个节点的时间戳是在 IdWorker 初始化的时候就设置完成了,具体体现到代码上是这样的:



io.seata.common.util.IdWorker#initTimestampAndSequence



图片


在获取 ID 的过程中,只有在初始化的时候获取过一次系统时间,之后和它就再也没有关系了。


所以,Seata 的分布式 ID 生成器,不再依赖于时间。


然后,你再想想另外一个问题:


由于序列号只有 12 位,它的取值范围就是 [0,4095]。如果我们序列号就是生成到了 4096 导致溢出了,怎么办呢?



很简单,序列号重新归 0,溢出的这一位加到时间戳上,让时间戳 +1。


那你再进一步想想,如果让时间戳 +1 了,那么岂不是会导致一种“超前消费”的情况出现,导致时间戳和系统时间不一致了?


朋友,慌啥啊,不一致就不一致呗,反正我们现在也不依赖于系统时间了。


然后,你想想,如果出现“超前消费”,意味着什么?


意味着在当前这个毫秒下,4096 个序列号不够用了。


4096/ms,约 400w/s。


你啥场景啊,怎么牛逼?


(哦,原来是面试场景啊,那懂了~)


另外,官网还抛出了另外一个问题:这样持续不断的"超前消费"会不会使得生成器内的时间戳大大超前于系统的时间戳,从而在重启时造成 ID 重复?


你想想,理论上确实是有可能的。假设我时间戳都“超前消费”到一个月以后了。


那么在这期间,你服务发生重启时我会重新获取一次系统时间戳,导致出现“时间回溯”的情况。


理论上确实有可能。



但是实际上...


看看官方的回复:


图片



别问,问就是不可能,就算出现了,最先崩的也不是我这个地方。



说了这么多其实就记住住这个图,就完事了:


图片


那么问题又来了:


改良版的算法是单调递增的吗?



在单节点里面,它肯定是单调递增的,但是如果是多个节点呢?


在多个节点的情况下,单独看某个节点的 ID 是单调递增的,但是多个节点下并不是全局单调递增。


因为节点 ID 在时间戳之前,所以节点 ID 大的,生成的 ID 一定大于节点 ID 小的,不管时间上谁先谁后。



这一点我们也可以通过代码验证一下,代码的意思是三个节点,每个节点各自生成 5 个 ID:


图片


从输出来看,一眼望去,生成的 ID 似乎是乱序的,至少在全局的角度下,肯定不是单调递增的:


但是我们把输出按照节点 ID 进行排序,就变成了这样,单节点内严格按照单调递增,没毛病:


图片


而在原版的雪花算法中,时间戳在高位,并且始终以系统时钟为准,每次生成的时候都会严格和系统时间进行对比,确保没有发生时间回溯,这样可以保证早生成的 ID 一定小于晚生成的 ID ,只有当 2 个节点恰好在同一时间戳生成 ID 时,2 个 ID 的大小才由节点 ID 决定。


这样看来,Seata 的改进算法是不是错的?


先别急,继续往下看。



分析一波



分析之前,先抛出官方的回答:


图片


先来一个八股文热身:


请问为什么不建议使用 UUID 作为数据库的主键 ID ?


就是为了避免触发 MySQL 的页分裂从而影响服务性能。


比如当前主键索引的情况是这样的:


图片


如果来了一个  433,那么直接追加在当前最后一个记录 432 之后即可。


图片


但是如果我们要插入一个 20 怎么办呢?


那么数据页 10 里面已经放满了数据,所以会触发页分裂,变成这样:


图片


进而导致上层数据页的分裂,最终变成这样的一个东西:


图片


上面的我们可以看出页分裂伴随着数据移动,所以我们应该尽量避免。


理想的情况下,应该是把一页数据塞满之后,再新建另外一个数据页,这样 B+ tree 的最底层的双向链表永远是尾部增长,不会出现上面画图的那种情况:在中间的某个节点发生分裂。


那么 Seata 的改良版的雪花算法在不具备“全局的单调递增性”的情况下,是怎么达到减少数据库的页分裂的目的的呢?


我们还是假设有三个节点,我用 A,B,C 代替,在数值上 A < B < C,采用的是改良版的雪花算法,在初始化的情况下是这样的。


图片


假设此时,A 节点申请了一个流水号,那么基于前面的分析,它一定是排在 A-seq1 之后,B-seq1 之前的。


但是这个时候数据页里面的数据满了,怎么办?


分裂呗:


图片


又来了 A-seq3 怎么办?


问题不大,还放的下:


图片


好,这个时候数据页 7 满了,但是又来了 A-seq4,怎么办?


只有继续分裂了:


图片


看到这个分裂的时候,你有没有嗦出一丝味道,是不是有点意思了?


因为在节点 A 上生成的任何 ID 都一定小于在节点 B 上生成的任何 ID,节点 B 和节点 C 同理。


在这个范围内,所有的 ID 都是单调递增的:


图片


而这样的范围最多有多少个?


是不是有多少个节点,就有多少个?


那么最多有多少个节点?


图片


2 的 10 次方,1024 个节点。


所以官方的文章中有这样的一句话:



新版算法从全局角度来看,ID 是无序的,但对于每一个 workerId,它生成的 ID 都是严格单调递增的,又因为 workerId 是有限的,所以最多可划分出 1024 个子序列,每个子序列都是单调递增的。



经过前面的分析,每个子序列总是单调递增的,所以每个子序列在有限次的分裂之后,最终都会达到稳态。


或者用一个数学上的说法:该算法是收敛的。


再或者,放个图看看:


图片


我想说作者画的时候尽力了,至于你看懂看不懂的,就看天意了。


页分裂


前面写的所有内容,你都能在官网上前面提到的两个文章中找到对应的部分。


但是关于页分裂部分,官方并没有进行详细说明。本来也是这样的,人家只是给你说自己的算法,没有必要延伸的太远。


“刚才说到了页分裂”,以面试官的嘴脸怎么可能放过你,“展开讲讲?”


链接已放,自行展开:



mysql.taobao.org/monthly/202…



还是先搞个图:


图片


问,在上面的这个 B+ tree 中,如果我要插入 9,应该怎么办?


因为数据页中已经没有位置了,所以肯定要触发页分裂。


会变成这样:


图片


这种页分裂方式叫做插入点(insert point)分裂。


其实在 InnoDB 中最常用的是另外一种分裂方式,中间点(mid point)分裂。


如果采用中间点(mid point)分裂,上面的图就会变成这样:


图片


即把将原数据页面中的 50% 数据移动到新页面,这种才是普遍的分裂方法。


这种分裂方法使两个数据页的空闲率都是 50%,如果之后的数据在这两个数据页上的插入是随机的话,那么就可以很好地利用空闲空间。


但是,如果后续数据插入不是随机,而是递增的呢?


比如我插入 10 和 11。


插入 10 之后是这样的:


图片


插入 11 的时候又要分页了,采用中间点(mid point)分裂就变成了这样:


图片


你看,如果是在递增的情况下,采用中间点(mid point)分裂,数据页 8 和 20 的空间利用率只有 50%。


因为数据的填充和分裂的永远是右侧页面,左侧页面的利用率只有 50%。


所以,插入点(insert point)分裂是为了优化中间点(mid point)分裂的问题而产生的。


InnoDB 在每个数据页上专门有一个叫做 PAGE_LAST_INSERT 的字段,记录了上次插入位置,用来判断当前插入是是否是递增或者是递减的。


如果是递减的,数据页则会向左分裂,然后移动上一页的部分数据过去。


如果判定为递增插入,就在当前点进行插入点分裂。


比如还是这个图:


图片


上次插入的是记录 8,本次插入 9,判断为递增插入,所以采用插入点分裂,所以才有了上面这个图片。


好,那么问题就来了,请听题:


假设出现了这种情况,阁下又该如何应对?


图片


在上面这个图的情况下,我要插入 10 和 9:


当插入 10 的时候,按 InnoDB 遍历 B+ tree 的方法会定位到记录 8,此时这个页面的 PAGE_LAST_INSERT 还是 8。所以会被判断为递增插入,在插入点分裂:


图片


同理插入 9 也是这样的:


图片


最终导致 9、10、11 都独自占据一个 page,空间利用率极低。


问题的根本原因在于每次都定位到记录 8(end of page),并且都判定为递增模式。


哦豁,你说这怎么办?


答案就藏在这一节开始的时候放的链接中:


图片


前面所画的图都是在没有并发的情况下展开的。


但是在这个部分里面,牵扯到了更为复杂的并发操作,同时也侧面解释了为什么 InnoDB 在同一时刻只能有有一个结构调整(SMO)进行。


这里面学问就大了去了,有兴趣的可以去了解一下 InnoDB 在 B+ tree 并发控制上的限制,然后再看看 Polar index 的破局之道。


反正我是学不动了。


哦,对了。前面说了这么多,还只是聊了页分裂的情况。


有分裂,就肯定有合并。


那么什么时候会触发页合并呢?


页合并会对我们前面探讨的 Seata 的改良版雪花算法带来什么影响呢?


别问了,别问了,学不动了,学不动了。


作者:essenceNow
来源:juejin.cn/post/7265641370495451148
收起阅读 »

代码中被植入了恶意删除操作,太狠了!

背景 在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。 事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。 对方...
继续阅读 »

背景


在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。


事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。


对方拿到镜像恢复之后,系统起来怎么也无法正常处理业务,于是就找到我帮忙看是什么原因。经过排查,原来交接的人在镜像中做了多处手脚,多处删除核心数据及jar包操作。下面来给大家细细分析排查过程。


排查过程


由于只提供了镜像文件,导致到底启动哪些服务都是问题。好在是Linux操作系统,镜像恢复之后,通过history命令可以查看曾经执行了哪些命令,能够找到都需要启动哪些服务。但服务启动之后,业务无法正常处理,很多业务都处于中间态。


原本系统是可以正常跑业务的,打个镜像之后再恢复就不可以了?这就奇怪了。于是对项目(jar包或war)文件进行排查,查看它们的修改时间。


在文件的修改时间上还真找到了一些问题,发现在打镜像的两个小时前,项目中一个多个项目底层依赖的jar包被修改过,另外还有两个class文件被修改过。


于是,就对它们进行了重点排查。首先反编译了那两个被修改过的class文件,在代码中找到了可疑的地方。


可疑代码


在两个被修改的类中都有上述代码。最开始没太留意这段代码,但直觉告诉我不太对,一个查询业务里面怎么可能出现删除操作呢?这太不符合常理了。


于是仔细阅读上述代码,发现上述红框中的代码无论何时执行最终的结果都是id=1。你是否看出来了?问题就出在三目表达式上,无论id是否为null,id被赋的值都是1。看到这里,也感慨对方是用心了。为了隐藏这个目的,前面写了那么多无用的代码。


但只有这个还不是什么问题,毕竟如果只是删除id为1的值,也只是删除了一条记录,影响范围应该有限。


紧接着反编译了被修改的jar包,依次去找上述删除方法的底层实现,看到如下代码:


删除操作


原来前面传递的id=1是为了配合where条件语句啊,当id=1被传递进来之后,就形成了where 1=1的条件语句。这个大家在mybatis中拼接多条件语句时经常用到。结果就是一旦执行了上述业务逻辑,就会触发删除T_QUART_DATA全表数据的操作。


T_QUART_DATA表中是用于存储触发定时任务的表达式,到这里也就明白了,为啥前面的业务跑不起来,全部是中间态了。因为一旦在业务逻辑中触发开关,把定时任务的cron表达式全部删除,十多个定时任务全部歇菜,业务也就跑步起来了。


找到了问题的根源,解决起来就不是啥事了,由于没有源代码,稍微费劲的是只能把原项目整个反编译出来,然后将改修改地方进行了修改。


又起波折


本以为到此问题已经解决完毕了,没想到第二天又出现问题了,项目又跑不起来了。经过多方排查和定位,感觉还有定时任务再进行暗箱操作。


于是通过Linux的crontab命令查看是否有定时任务在执行,执行crontab -ecrontab -l,还真看到有三个定时任务在执行。跟踪到定时任务执行的脚本中,而且明目张胆的起名deleteXXX:


删除脚本


而在具体的脚本中,有如下执行操作:


删除核心依赖包


这下找到为什么项目中第二天为啥跑不起来了,原来Linux的定时任务将核心依赖包删除了,并且还会去重启服务。


为了搞破坏,真是煞费苦心啊。还好的是这个jar包在前一天已经反编译出来了,也算有了备份。


小结


原本以为程序员在代码中进行删库操作或做一些其他小手脚只是网络上的段子,大多数人出于职业操守或个人品质是不会做的。没想到这还真遇到了,而且对方为了隐藏删除操作,还做了一些小伪装,真的是煞费苦心啊。如果有这样的能力和心思,用在写出更优秀的代码或系统上或许更好。


当然,不知道他们在交接的过程中到底发生了什么,竟然用这样的方式对待昔日合作的伙伴。之所以写这篇文章,是想让大家学习如何排查代码问题的过程,毕竟用到了不少知识点和技能,但这并不是教大家如何去做手脚。无论怎样,最起码的职业操守还是要有

作者:程序新视界
来源:juejin.cn/post/7140066341469290532
的,这点不接受反驳。

收起阅读 »

万能的异步处理方案

异步处理通用方案 前言 良好的系统设计必须要做到开闭原则,随着业务的不断迭代更新,核心代码也会被不断改动,出错的概率也会大大增加。但是大部分增加的功能都是在扩展原有的功能,既要保证性能又要保证质量,我们往往都会使用异步线程池来处理,然而却增加了很多不确定性因素...
继续阅读 »

异步处理通用方案


前言


良好的系统设计必须要做到开闭原则,随着业务的不断迭代更新,核心代码也会被不断改动,出错的概率也会大大增加。但是大部分增加的功能都是在扩展原有的功能,既要保证性能又要保证质量,我们往往都会使用异步线程池来处理,然而却增加了很多不确定性因素。 由此我设计了一套通用的异步处理SDK,可以很轻松的实现各种异步处理


目的


通过异步处理不仅能够保证方法能够得到有效的执行而且不影响主流程


更重要的是各种兜底方法保证数据不丢失,从而达到最终一致性\color{red}最终一致性


优点


无侵入设计,独立数据库,独立定时任务,独立消息队列,独立人工执行界面(统一登录认证)


使用spring事务事件机制,即使异步策略解析失败也不会影响业务


如果你的方法正在运行事务,会等事务提交后再处理事件


就算事务提交了,异步策略解析失败了,我们还有兜底方案执行(除非数据库有问题,消息队列有问题,方法有bug)


组件


kafka 消息队列


xxl job 定时任务


mysql 数据库


spring 切面


vue 界面


设计模式


策略


模板方法


动态代理


流程图


image.png


数据库脚本


CREATE TABLE `async_req` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`application_name` varchar(100) NOT NULL DEFAULT '' COMMENT '应用名称',
`sign` varchar(50) NOT NULL DEFAULT '' COMMENT '方法签名',
`class_name` varchar(200) NOT NULL DEFAULT '' COMMENT '全路径类名称',
`method_name` varchar(100) NOT NULL DEFAULT '' COMMENT '方法名称',
`async_type` varchar(50) NOT NULL DEFAULT '' COMMENT '异步策略类型',
`exec_status` tinyint NOT NULL DEFAULT '0' COMMENT '执行状态 0:初始化 1:执行失败 2:执行成功',
`exec_count` int NOT NULL DEFAULT '0' COMMENT '执行次数',
`param_json` longtext COMMENT '请求参数',
`remark` varchar(200) NOT NULL DEFAULT '' COMMENT '业务描述',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_applocation_name` (`application_name`) USING BTREE,
KEY `idx_exec_status` (`exec_status`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理请求';

CREATE TABLE `async_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`async_id` bigint NOT NULL DEFAULT '0' COMMENT '异步请求ID',
`error_data` longtext COMMENT '执行错误信息',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_async_id` (`async_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理日志';

异步策略


image.png


安全级别


image.png


执行状态


image.png


流程图


image.png


image.png
image.png

apollo 配置


# 开关:默认关闭
scm.async.enabled=true

# 数据源 druid
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/fc_async?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
spring.datasource.username=user
spring.datasource.password=xxxx
spring.datasource.filters=config
spring.datasource.connectionProperties=config.decrypt=true;config.decrypt.key=yyy
#静态地址
spring.resources.add-mappings=true
spring.resources.static-locations=classpath:/static/


# 以下配置都有默认值
# 核心线程数
async.executor.thread.corePoolSize=10
# 最大线程数
async.executor.thread.maxPoolSize=50
# 队列容量
async.executor.thread.queueCapacity=10000
# 活跃时间
async.executor.thread.keepAliveSeconds=600

# 执行成功是否删除记录:默认删除
scm.async.exec.deleted=true

# 自定义队列名称前缀:默认应用名称
scm.async.topic=应用名称

# 重试执行次数:默认5次
scm.async.exec.count=5

# 重试最大查询数量
scm.async.retry.limit=100

# 补偿最大查询数量
scm.async.comp.limit=100

用法


1,异步开关
scm.async.enabled=true

2,在需要异步执行的方法加注解 (必须是spring代理方法)
@AsyncExec(type = AsyncExecEnum.SAVE_ASYNC, remark = "数据字典")

3,人工处理地址
http://localhost:8004/async/index.html

注意


1,应用名称
spring.application.name

2,队列名称
${scm.async.topic:${spring.application.name}}_async_queue
自定义topic:scm.async.topic=xxx

3,自己业务要做幂等

4,一个应用公用一个队列
自产自消

5,定时任务
异步重试定时任务(2分钟重试一次,可配置重试次数)
异步补偿定时任务(一小时补偿一次,创建时间在一小时之前的)

效果展示


image.png


image.png


作者:三火哥
来源:juejin.cn/post/7266087843239084090
收起阅读 »

AI欣赏-街头少女🔥🔥🔥

描述 💯💯💯 你更喜欢哪一位选手呢? 自己制作的一些AI绘画关键词,一是为了保存下来,二是分享给大家看看,可以收集一些意见和建议、想法等等。所以欢迎大家踊跃发言! 本次文章的描述主题如下:一个女孩在街头大笑,湿透的,辫子,写实风格的,电影级别的,HDR的。Lo...
继续阅读 »

描述


💯💯💯 你更喜欢哪一位选手呢?


自己制作的一些AI绘画关键词,一是为了保存下来,二是分享给大家看看,可以收集一些意见和建议、想法等等。所以欢迎大家踊跃发言!


本次文章的描述主题如下:一个女孩在街头大笑,湿透的,辫子,写实风格的,电影级别的,HDR的。

  • Lora:无

  • Embeddings:ng_deepnegative_v1_75t [1a3e]


Prompt



a young woman, street, laughing, ponytails, (hdr:1.3), (muted colors:1.2), dramatic, complex background, cinematic, filmic, (rutkowski, artstation:0.8), soaking wet,



Negative Prompt



(nsfw:2),Multiple people,easynegative,(worst quality:2),(low quality:2),lowres,(monochrome:1.4),(grayscale:1.4),big head,severed legs,short legs,missing legs,acnes,skin blemishes,age spot,backlight,(ugly:1.4),(duplicate:1.4),(morbid:1.2),(mutilated:1.2),mutated hands,(poorly drawn hands:1.4),blurry, (bad anatomy:1.4),(bad proportions:1.4),(disfigured:1.4),(unclear eyes:1.4),bad hands, bad tooth,missing fingers,extra digit,bad body,NG_DeepNegative_V1_75T,glans,EasyNegative:0.5,gross proportions.short arm,(missing arms:1.4),missing thighs,missing calf,mutation,duplicate,morbid,mutilated,poorly drawn cloth,strange finger,bad finger,(mutated hands and fingers:1.4),(text:1.4), bad-artist, bad_prompt_version2, bad-hands-5, bad-image-v2-39000,



基础配置




生成图的效果展示


选手1




选手2




选手3




选手4




选手5




选手6




选手7




选手8




选手9




投票


🌺 请开始诸位的投票吧!评论区见!!


作者:襄垣
链接:https://juejin.cn/post/7223267912727298103
来源:稀土掘金
收起阅读 »

iPhone两秒出图,目前已知的最快移动端Stable Diffusion模型来了

Stable Diffusion (SD)是当前最热门的文本到图像(text to image)生成扩散模型。尽管其强大的图像生成能力令人震撼,一个明显的不足是需要的计算资源巨大,推理速度很慢:以 SD-v1.5 为例,即使用半精度存储,其模型大小也有 1.7...
继续阅读 »

Stable Diffusion (SD)是当前最热门的文本到图像(text to image)生成扩散模型。尽管其强大的图像生成能力令人震撼,一个明显的不足是需要的计算资源巨大,推理速度很慢:以 SD-v1.5 为例,即使用半精度存储,其模型大小也有 1.7GB,近 10 亿参数,端上推理时间往往要接近 2min。


为了解决推理速度问题,学术界与业界已经开始对 SD 加速的研究,主要集中于两条路线:(1)减少推理步数,这条路线又可以分为两条子路线,一是通过提出更好的 noise scheduler 来减少步数,代表作是 DDIM [1],PNDM [2],DPM [3] 等;二是通过渐进式蒸馏(Progressive Distillation)来减少步数,代表作是 Progressive Distillation [4] 和 w-conditioning [5] 等。(2)工程技巧优化,代表作是 Qualcomm 通过 int8 量化 + 全栈式优化实现 SD-v1.5 在安卓手机上 15s 出图 [6],Google 通过端上 GPU 优化将 SD-v1.4 在三星手机上加速到 12s [7]。


尽管这些工作取得了长足的进步,但仍然不够快。


近日,Snap 研究院推出最新高性能 Stable Diffusion 模型,通过对网络结构、训练流程、损失函数全方位进行优化,在 iPhone 14 Pro 上实现 2 秒出图(512x512),且比 SD-v1.5 取得更好的 CLIP score。这是目前已知最快的端上 Stable Diffusion 模型!



论文地址:arxiv.org/pdf/2306.00…
Webpage: snap-research.github.io/SnapFusion


核心方法


Stable Diffusion 模型分为三部分:VAE encoder/decoder, text encoder, UNet,其中 UNet 无论是参数量还是计算量,都占绝对的大头,因此 SnapFusion 主要是对 UNet 进行优化。具体分为两部分:(1)UNet 结构上的优化:通过分析原有 UNet 的速度瓶颈,本文提出一套 UNet 结构自动评估、进化流程,得到了更为高效的 UNet 结构(称为 Efficient UNet)。(2)推理步数上的优化:众所周知,扩散模型在推理时是一个迭代的去噪过程,迭代的步数越多,生成图片的质量越高,但时间代价也随着迭代步数线性增加。为了减少步数并维持图片质量,我们提出一种 CFG-aware 蒸馏损失函数,在训练过程中显式考虑 CFG (Classifier-Free Guidance)的作用,这一损失函数被证明是提升 CLIP score 的关键!


下表是 SD-v1.5 与 SnapFusion 模型的概况对比,可见速度提升来源于 UNet 和 VAE decoder 两个部分,UNet 部分是大头。UNet 部分的改进有两方面,一是单次 latency 下降(1700ms -> 230ms,7.4x 加速),这是通过提出的 Efficient UNet 结构得到的;二是 Inference steps 降低(50 -> 8,6.25x 加速),这是通过提出的 CFG-aware Distillation 得到的。VAE decoder 的加速是通过结构化剪枝实现。




下面着重介绍 Efficient UNet 的设计和 CFG-aware Distillation 损失函数的设计。


(1)Efficient UNet


我们通过分析 UNet 中的 Cross-Attention 和 ResNet 模块,定位速度瓶颈在于 Cross-Attention 模块(尤其是第一个 Downsample 阶段的 Cross-Attention),如下图所示。这个问题的根源是因为 attention 模块的复杂度跟特征图的 spatial size 成平方关系,在第一个 Downsample 阶段,特征图的 spatial size 仍然较大,导致计算复杂度高。




为了优化 UNet 结构,我们提出一套 UNet 结构自动评估、进化流程:先对 UNet 进行鲁棒性训练(Robust Training),在训练中随机 drop 一些模块,以此来测试出每个模块对性能的真实影响,从而构建一个 “对 CLIP score 的影响 vs. latency” 的查找表;然后根据该查找表,优先去除对 CLIP score 影响不大同时又很耗时的模块。这一套流程是在线自动进行,完成之后,我们就得到了一个全新的 UNet 结构,称为 Efficient UNet。相比原版 UNet,实现 7.4x 加速且性能不降。


(2)CFG-aware Step Distillation


CFG(Classifier-Free Guidance)是 SD 推理阶段的必备技巧,可以大幅提升图片质量,非常关键!尽管已有工作对扩散模型进行步数蒸馏(Step Distillation)来加速 [4],但是它们没有在蒸馏训练中把 CFG 纳入优化目标,也就是说,蒸馏损失函数并不知道后面会用到 CFG。这一点根据我们的观察,在步数少的时候会严重影响 CLIP score。


为了解决这个问题,我们提出在计算蒸馏损失函数之前,先让 teacher 和 student 模型都进行 CFG,这样损失函数是在经过 CFG 之后的特征上计算,从而显式地考虑了不同 CFG scale 的影响。实验中我们发现,完全使用 CFG-aware Distillation 尽管可以提高 CLIP score, 但 FID 也明显变差。我们进而提出了一个随机采样方案来混合原来的 Step Distillation 损失函数和 CFG-aware Distillation 损失函数,实现了二者的优势共存,既显著提高了 CLIP score,同时 FID 也没有变差。这一步骤,实现进一步推理阶段加速 6.25 倍,实现总加速约 46 倍。


除了以上两个主要贡献,文中还有对 VAE decoder 的剪枝加速以及蒸馏流程上的精心设计,具体内容请参考论文。


实验结果


SnapFusion 对标 SD-v1.5 text to image 功能,目标是实现推理时间大幅缩减并维持图像质量不降,最能说明这一点的是下图:




该图是在 MS COCO’14 验证集上随机选取 30K caption-image pairs 测算 CLIP score 和 FID。CLIP score 衡量图片与文本的语义吻合程度,越大越好;FID 衡量生成图片与真实图片之间的分布距离(一般被认为是生成图片多样性的度量),越小越好。图中不同的点是使用不同的 CFG scale 获得,每一个 CFG scale 对应一个数据点。从图中可见,我们的方法(红线)可以达到跟 SD-v1.5(蓝线)同样的最低 FID,同时,我们方法的 CLIP score 更好。值得注意的是,SD-v1.5 需要 1.4min 生成一张图片,而 SnapFusion 仅需要 1.84s,这也是目前我们已知最快的移动端 Stable Diffusion 模型!


下面是一些 SnapFusion 生成的样本:




更多样本请参考文章附录。


除了这些主要结果,文中也展示了众多烧蚀分析(Ablation Study)实验,希望能为高效 SD 模型的研发提供参考经验:


(1)之前 Step Distillation 的工作通常采用渐进式方案 [4, 5],但我们发现,在 SD 模型上渐进式蒸馏并没有比直接蒸馏更有优势,且过程繁琐,因此我们在文中采用的是直接蒸馏方案。




(2)CFG 虽然可以大幅提升图像质量,但代价是推理成本翻倍。今年 CVPR’23 Award Candidate 的 On Distillation 一文 [5] 提出 w-conditioning,将 CFG 参数作为 UNet 的输入进行蒸馏(得到的模型叫做 w-conditioned UNet),从而在推理时省却 CFG 这一步,实现推理成本减半。但是我们发现,这样做其实会造成图片质量下降,CLIP score 降低(如下图中,四条 w-conditioned 线 CLIP score 均未超过 0.30, 劣于 SD-v1.5)。而我们的方法则可以减少步数,同时将 CLIP score 提高,得益于所提出的 CFG-aware 蒸馏损失函数!尤其值得主要的是,下图中绿线(w-conditioned, 16 steps)与橙线(Ours,8 steps)的推理代价是一样的,但明显橙线更优,说明我们的技术路线比 w-conditioning [5] 在蒸馏 CFG guided SD 模型上更为有效。




(3)既有 Step Distillation 的工作 [4, 5] 没有将原有的损失函数和蒸馏损失函数加在一起,熟悉图像分类知识蒸馏的朋友应该知道,这种设计直觉上来说是欠优的。于是我们提出把原有的损失函数加入到训练中,如下图所示,确实有效(小幅降低 FID)。




总结与未来工作


本文提出 SnapFusion,一种移动端高性能 Stable Diffusion 模型。SnapFusion 有两点核心贡献:(1)通过对现有 UNet 的逐层分析,定位速度瓶颈,提出一种新的高效 UNet 结构(Efficient UNet),可以等效替换原 Stable Diffusion 中的 UNet,实现 7.4x 加速;(2)对推理阶段的迭代步数进行优化,提出一种全新的步数蒸馏方案(CFG-aware Step Distillation),减少步数的同时可显著提升 CLIP score,实现 6.25x 加速。总体来说,SnapFusion 在 iPhone 14 Pro 上实现 2 秒内出图,这是目前已知最快的移动端 Stable Diffusion 模型。


未来工作:


1.SD 模型在多种图像生成场景中都可以使用,本文囿于时间,目前只关注了 text to image 这个核心任务,后期将跟进其他任务(如 inpainting,ControlNet 等等)。




  1. 本文主要关注速度上的提升,并未对模型存储进行优化。我们相信所提出的 Efficient UNet 仍然具备压缩的空间,结合其他的高性能优化方法(如剪枝,量化),有望缩小存储,并将时间降低到 1 秒以内,离端上实时 SD 更进一步。




参考文献


[1] Denoising Diffusion Implicit Models, ICLR’21


[2] Pseudo Numerical Methods for Diffusion Models on Manifolds, ICLR’22


[3] DPM-Solver: A Fast ODE Solver for Diffusion Probabilistic Model Sampling in Around 10 Steps, NeurIPS’22


[4] Progressive Distillation for Fast Sampling of Diffusion Models, ICLR’22


[5] On Distillation of Guided Diffusion Models, CVPR’23


[6] http://www.qualcomm.com/news/onq/20…


[7] Speed Is All You Need: On-Device Acceleration of Large Diffusion Models via GPU-Aware Optimizations, CVPR’23 Workshop


作者:机器之心
链接:https://juejin.cn/post/7244452476191850557
来源:稀土掘金
收起阅读 »

使用脚本更新 macOS 壁纸,让你每天看到不同的美景🖼️

在macOS系统中,我们可以轻松地更换桌面壁纸。但是,如果你每天都想要一张新的壁纸,手动更换就会变得十分繁琐。幸运的是,我们可以使用bash脚本和unsplash API自动更新壁纸。 步骤1:获取unsplash API密钥 首先,你需要注册一个unspla...
继续阅读 »

在macOS系统中,我们可以轻松地更换桌面壁纸。但是,如果你每天都想要一张新的壁纸,手动更换就会变得十分繁琐。幸运的是,我们可以使用bash脚本和unsplash API自动更新壁纸。


步骤1:获取unsplash API密钥


首先,你需要注册一个unsplash账户,并申请一个API密钥。这个API密钥将允许你通过编程方式访问unsplash图片库。


步骤2:编写bash脚本


创建一个新的文本文件,然后在其中添加以下代码:

#!/bin/bash

# set the unsplash API access key
access_key="YOUR_UNSPLASH_API_ACCESS_KEY"

# define the query to search for wallpaper images
query="nature"

# search for a random wallpaper image
result=$(/usr/bin/curl -s -H "Authorization: Client-ID $access_key" "https://api.unsplash.com/photos/random?query=$query")

# extract the image URL from the JSON response
image_url=$(echo "$result" | /opt/homebrew/bin/jq -r '.urls.full')

# download the image
/usr/bin/curl -s "$image_url" > ~/Pictures/wallpaper.jpg

# set the image as the desktop wallpaper
osascript -e "tell application \"Finder\" to set desktop picture to POSIX file \"$HOME/Pictures/wallpaper.jpg\""

这段代码会使用unsplash API搜索与“nature”相关的随机图片,并将其下载到“~/Pictures/wallpaper.jpg”文件中。然后,它会使用AppleScript将下载的图片设置为桌面壁纸。


步骤3:运行bash脚本


将文件保存为“update-wallpaper.sh”,然后打开终端并导航到该文件所在的目录。运行以下命令以使脚本可执行:

chmod +x update-wallpaper.sh

现在,你可以通过在终端中输入以下命令来运行脚本:

./update-wallpaper.sh

步骤4:设置定时任务

脚本依赖:curl、jq、bash、unsplash,使用 which 获取路径,然后替换脚本里的curl和jq。

which curl
which jq

你可以将该脚本设置为定时任务,以便每天自动更新壁纸。打开“终端”并输入以下命令以编辑cron定时任务:

crontab -e

然后,添加以下行:

0 9 * * * /path/to/update-wallpaper.sh

这将在每天上午9点运行该脚本。

现在,你可以坐下来,放松一下,让你的macOS自动更新壁纸。享受吧!

作者:FreeCultureBoy
链接:https://juejin.cn/post/7226301946839089211
来源:稀土掘金

收起阅读 »

完全免费白嫖 GPT-4 的终极方案!

GPT-4 目前是世界上最强的多模态大模型,能力甩 GPT-3.5 好几条街。 大家都希望早日用上 GPT-4,不过目前体验 GPT-4 的渠道非常有限,要么就是开通 ChatGPT 尊贵的 Plus 会员,即使你开了会员,也是有限制的,每 3 小时只能发送 ...
继续阅读 »


GPT-4 目前是世界上最强的多模态大模型,能力甩 GPT-3.5 好几条街。


大家都希望早日用上 GPT-4,不过目前体验 GPT-4 的渠道非常有限,要么就是开通 ChatGPT 尊贵的 Plus 会员,即使你开了会员,也是有限制的,每 3 小时只能发送 25 条消息。。。


要么就去 OpenAI 官网申请 GPT-4 的 API,但是目前申请到 API 的小伙伴非常少,你以为申请到 API 就可以用了吗?GPT-4 的 API 价格超级无敌贵,是 GPT-3.5 价格的 30 倍,你敢用吗?😄


然而,但是,既然我写了这篇文章,肯定是要告诉那一个惊天大幂幂的!


现在完全免费白嫖 GPT-4 的机会来了,不仅可以白嫖,还可以直接作为 API 来调用!


不仅能够作为 API 调用,我还接入了公众号给大家白嫖,你说气人不气人?



下面言归正传,开始手把手教大家如何免费白嫖 GPT-4


gpt4free-ts 介绍


GPT4Free 大家应该都知道吧?它上线几周就在 GitHub 上揽收了接近 4w 的 Star。原因就在于其提供了对 GPT-4 及 GPT-3.5 免费且几乎无限制的访问。该项目通过对各种调用了 OpenAI API 网站的第三方 API 进行逆向工程,达到使任何人都可以免费访问该流行 AI 模型的目的。


这就相当于什么?假设地主家有一个粮仓,你往他家的粮仓偷偷插了一根管子,不停地向外抽米,他全然不知,所以你也不用交钱,一切费用由地主承担


现在接入 GPT-4 的第三方网站就相当于那个地主,懂了吧?


但是这个项目并没有封装 API,而且目前也不太能用了。


作为开发者,我们想要的肯定是 API 啊!这就要提到今天的主角了:gpt4free-ts


这个项目是用 TypeScript 写的,相当于 GPT4Free 的 TypeScript 版本,但是更方便部署,而且封装了 API,简直就是开发者的福音,就他了!


这个项目向多个地主家的粮仓插了管子,其中最强大的地主就是 forefront.ai,这个地主家的粮仓里就包含了 GPT-4 这个香饽饽,而且还有 Claude,就嫖他了!


除了 forefront 之外,它接的粮仓还挺多的。。



大批量注册临时邮箱


forefront 的 GPT-4 模型是有限制的,每个账号每 3 小时内只能发送 5 条消息


所以接下来需要用到一个非常神奇的服务叫 RapidAPI你可以通过这个 API 来获取无穷无尽的临时邮箱,然后再用这些无穷无尽的临时邮箱去注册无穷无尽的 forefront 账号。


这么一说,你是不是就悟了?哈哈哈


首先你需要在这里注册一个账号并登录:rapidapi.com/calvinlovel…


然后需要在 Pricing 页面开启订阅:



一般情况下订阅免费套餐即可,一天可以调用 100 次。


如果你有更高的需求,可以考虑订阅更高级的套餐(比如你的用户数量特别多)。


订阅完了之后,你就能看到 API Key 了。这个 Key 我们后面会用到。



Sealos 云操作系统介绍


单机操作系统大家应该都知道吧?Windows、macOS、Linux 这些都属于单机操作系统,为什么叫单机操作系统呢?因为他的内存啊,CPU 啊,都在一台机器上,你不可能用其他机器的内存和 CPU。


那么什么是云操作系统呢?就是把一群机器的 CPU 和内存看成一个整体,然后给用户提供一个交互界面,用户可以通过这个交互界面来操作所有的资源。


懂 K8s 的玩家可能要说了:这个我懂,K8s 就可以!


如果我们的目标愿景是一个云操作系统,K8s 充其量只能是这个云操作系统的内核,就像 Linux 内核一样。完整的云操作系统需要一个像 Windows 和 Ubuntu 操作系统那样的交互界面,也就是操作系统发行版


对于云操作系统来说,Sealos 就是那个发行版。



链接:cloud.sealos.io




有人可能会把云操作系统理解成“Web 界面”,但其实不是,Sealos 云操作系统完全是类似于 Windows 和 macOS 桌面的那种逻辑,并不是 Web 界面。我只需要点几下鼠标,一个应用就装好了,老夫并不知道什么容器什么 K8s。


数据库也一样,小鼠标一点,一个分布式数据库就装好了。


我知道,这时候云原生玩家要坐不住了,您别着急,看到桌面上的终端了没?



终端只是这个云操作系统中的一个 App 而已。同理,容器管理界面仍然可以作为云操作系统的 App,我管你是 Kubernetes Dashboard、Rancher、KubeSphere 还是 Kuboard,都可以作为 App 装在这个云操作系统中。这时候对于云原生专家而言,仍然可以命令行咔咔秀操作,也可以通过各种管理界面来管理容器。


云操作系统嘛,就是要什么人都能用才行,不管你是什么角色,都能在这个操作系统里找到你想要的 App 去完成你的使命


安装 gpt4free-ts


接下来才是这篇文章的重头戏。


我要教大家如何在 Sealos 中点几下鼠标就能安装一个 gpt4free-ts 集群


没错,就是 gpt4free-ts 集群。


什么叫集群?就是说我要运行一群 gpt4free-ts 实例,然后前面加一个负载均衡作为对外的 API 入口。


下面的步骤非常简单,楼下的老奶奶都会,是真的,当时我就在楼下看她操作


首先进入 Sealos 云操作系统的界面:**cloud.sealos.io**。


然后打开桌面上的应用管理 App:



点击「新建应用」:



在启动参数中,按照以下方式进行设置:

1.应用名称随便写,比如 gpt4free。
2.镜像名称是:xiangsx/gpt4free-ts:latest
CPU 和内存需要根据应用的实际情况来填写。这个应用运行之后默认会启动两个 Chrome 浏览器来模拟登录 forefront,每次对话会从里面取一个账号来使用,次数用完了会自动注册新账号(因为每个账号每 3 小时只能发送 5 条信息)。我们可以通过环境变量来修改启动的浏览器数量,所以需要根据你的浏览器数量来确定 CPU 和内存。 我自己把浏览器数量设置为 3,所以需要的内存和 CPU 比较多(后面会告诉你怎么设置环境变量)。
3.实例数根据自己的实际需求填写,我需要接入公众号,粉丝比较多,一个实例才 3 个账号(因为我一个实例跑了 3 个浏览器),根本不够用,所以我开了 3 个实例。
4.容器暴露端口指定为 3000。
5.打开外网访问。



继续往下,展开高级设置,点击「编辑环境变量」:



填入以下环境变量:

rapid_api_key=<rapid_api_key>
DEBUG=0
POOL_SIZE=3


⚠️注意:请将 <rapid_api_key> 替换为你自己的 key。



其中 POOL_SIZE 就是浏览器数量,每个浏览器会登录一个 forefront 账号。你可以根据自己的需要调整浏览器数量,并根据浏览器数量调整 CPU 和内存。如果你不知道怎么调整合适,建议无脑跟着本文操作。



继续,点击「新增存储卷」:



容量只需 1G,挂载路径设置为 /usr/src/app/run




这个存储的作用是为了保存已登录的账号。已经注册的账号 3 个小时以后还可以重新使用,不用再浪费邮箱去注册新账号。



最终点击右上角的「部署应用」,即可完成部署:



最终要等待所有的实例都处于 Running 状态,才算是启动成功了。



点击右边的复制按钮,便可复制 API 的外网地址:



我们来测一下这个 API:



完美!打完收工!


接入微信公众号


什么?你想将这个 API 接入自己的公众号?换个形式吧!直接来看直播吧,我们会通过直播活动手把手教你如何将 GPT-4 免费接入公众号、网页等各种前端。


活动链接:forum.laf.run/d/684


当然,直播过程不会直接教你如何接入公众号,而是“授你🫵以渔”,告诉你如何使用 Laf 来通过各种姿势调用这个 API,最终你也接入公众号也罢,网页前端也罢,那都不是事儿~

作者:米开朗基杨
链接:https://juejin.cn/post/7241790368949190693
来源:稀土掘金
收起阅读 »

雷军写的代码上热搜了!

就在昨天,「雷军写的代码」相关话题先后上了一波热搜和热榜。 出于好奇,第一时间点进去围观了一波。 原来雷总马上要在8月14日举办他的2023年度演讲了,并且也放出了对应的演讲海报。 这个海报可以说暗藏玄机,放大后仔细一看,好家伙,密密麻麻全都是代码这是...
继续阅读 »

就在昨天,「雷军写的代码」相关话题先后上了一波热搜和热榜。




出于好奇,第一时间点进去围观了一波。



原来雷总马上要在8月14日举办他的2023年度演讲了,并且也放出了对应的演讲海报。



这个海报可以说暗藏玄机,放大后仔细一看,好家伙,密密麻麻全都是代码这是!



看一下代码细节,都是类似MOVJMPPUSHLOOP等这些指令。


这不就是自己当年上学时学得瑟瑟发抖痛哭涕零的汇编语言嘛。。



这一瞬间就让我想起了当年微博上的这张图:



早在十几年前,微博上就曾流传过雷军早年所写的一段完整的汇编代码。



当时雷军也转发过,并表示这个程序的第一个版本是他1989年写的,怀念当初写程序的快乐时光。



在网上经常会看到关于雷军代码水平到底如何的讨论帖子。


可以说,看看雷军早年写的这段代码相信心里基本就有答案了。


在早年那个机器硬件水平和性能都十分受限的年代,为了满足某些需求,可以说对编码的程序员提出了非常苛刻的要求。


开发者往往只能使用非常底层的编程语言去实现某些程序,同时还需要把代码优化做到极致,这本就非常考验程序员的基本功和编程底子。


各方面信息都显示,作为一个程序员来说,雷军不仅仅是合格,说他是非常厉害的大神那也丝毫没有毛病。


就像这次热搜话题下稚晖君大佬的一篇动态所言,雷军作为老一代程序员的代表和创业楷模,确实值得敬佩。



文章的最后,我们也找到了当年雷军所写的这段汇编代码的完整版,这里也分享给大家。


咳咳,前方高能!!!


作者:CodeSheep
链接:https://juejin.cn/post/7265679390242537512
来源:稀土掘金
收起阅读 »

吐槽大会,来瞧瞧资深老前端写的垃圾代码

忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑。 人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了...
继续阅读 »

忍无可忍,不吐不快。


本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


知道了什么是烂代码,才能写出好代码。


别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。

优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


---------------------------------------------更新------------------------------------------------


集中回答一下评论区的问题:


1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


文件命名千奇百怪


同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。




组件职责不清


还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?




条件渲染逻辑置于底层


这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。




滥用、乱用 TS


项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。




留下大量无用注释代码和报错代码


感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。






丑陋的、隐患的、无效的、混乱的 css


丑陋的:没有空格,没有换行,没有缩进


隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合




一个文件 6 个槽点


槽点1:代码的空行格式混乱,影响代码阅读


槽点2:空函数,写了函数名不写函数体,但是还调用了!


槽点3:函数参数过多不优化


槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名




变态的链式取值和赋值


都懒得说了,各位观众自己看吧。




代码拆分不合理或者不拆分导致代码行数超标


能写出这么多行数的代码的绝对是人才。


尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。




这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。




杂七杂八的无用 js、md、txt 文件


在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


实在受不了干脆建个文件夹放一块,看起来也要舒服多了。




less、scss 混用


这是最奇葩的。




特殊变量重命名


这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。

const G = window;
const doc = G.document;

混乱的 import


规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


总而言之,css 文件一般是最后引入,不能阻塞 js 的优先引入。




写在最后


就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。希望大家引以为戒,不然小心被刀。


要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


不过说这么多,成事在人。


不过我写了一篇讲解如何写出简洁清晰代码的文章,我看不仅是一年前端需要学习,六七年的老前端也需要看看。

作者:北岛贰
链接:https://juejin.cn/post/7265505732158472249
来源:稀土掘金
收起阅读 »

接口测试神器:ApiKit

想给大家分享一款技术人必备的接口测试神器:ApiKit,应该是我目前用过,算得上良心的接口工具1.背景 作为互联网行业技术从业者,接口调试是必不可少的一项技能,通常我们都会选择使用 Postman 这类工具来进行接口调试,在接口调试方面 Postman 做的确...
继续阅读 »

想给大家分享一款技术人必备的接口测试神器:ApiKit,应该是我目前用过,算得上良心的接口工具

1.背景


作为互联网行业技术从业者,接口调试是必不可少的一项技能,通常我们都会选择使用 Postman 这类工具来进行接口调试,在接口调试方面 Postman 做的确实非常出色。


但是在整个软件开发过程中,接口调试只是其中的一部分,还有很多事情 Postman 是无法完成的,或者无法高效完成,比如:接口文档定义、Mock 数据、接口自动化测试等等。


今天给大家推荐的一款神器: ApiKit=API 管理 + Mock + 自动化测试 + 异常监控 + 团队协作

1.1聊一聊接口管理的现状


对于接口管理的现状来说,目前行业大部分采取的解决方案有如下几种:

1.使用 **Swagger**管理接口文档。 

2. 使用 Postman 调试接口。

3.使用 **RAP或Easy Mock**来进行 **Mock**数据。 

4. 使用 JMeter 做接口自动化测试。


而上述的接口管理手段,咋一看,貌似没有什么问题,但仔细分析,不难发现,当中存在的问题还真不少,比如要维护不同工具,并且这些工具之间数据一致性非常困难、非常低效。这里不仅仅是工作量的问题,更大的问题是多个系统之间数据不一致,导致协作低效,频繁出问题,开发人员、测试人员工作起来也痛苦不堪。


设想一下这样的一个协作流程(官方示例):



1. 开发人员在Swagger定义好文档后,接口调试的时候还需要去 Postman 再定义一遍。 2. 前端开发Mock 数据的时候又要去RAPEasy Mock定义一遍,手动设置好 Mock 规则。 3. 测试人员需要去 JMeter定义一遍。 4. 前端根据 RAPEasy Mock定义 Mock 出来的数据开发完,后端根据 Swagger定义的接口文档开发完,各自测试测试通过了,本以为可以马上上线,结果一对接发现各种问题:原来开发过程中接口变更,只修改了 Swagger,但是没有及时同步修改 RAPEasy Mock。 5. 同样,测试在 JMeter 写好的测试用例,真正运行的时候也会发现各种不一致。 6. 时间久了,各种不一致会越来越严重。

ApiKit介绍

官方对ApiKit定位是,API 管理 + Mock + 自动化测试 + 异常监控 + 团队协作 

结合 API 设计、文档管理、自动化测试、监控、研发管理和团队协作的一站式 API 生产平台,从个人开发者到跨国企业用户,Apikit 帮助全球超过50万开发者和10万家企业更快、更好且更安全地开发和使用 API 

概括来讲,ApiKit 常用功能分为五类: 

1.智能且强大的 Mock 前端团队可以在 API 还没开发完成的情况下,借助 Mock API 实现预对接,加速开发进程。测试团队可以通过 Mock API 解耦不必要的系统,完成集成测试 

2.快速生成和管理所有 API 文档 无论您使用什么语言开发,Apikit 都可以帮您统一规范地管理起来,并提供强大的文档管理、协作、测试、分享功能 

3. 零代码自动化测试 Apikit 提供了 API 测试功能,支持自动生成测试数据,能够通过Javascript 对请求报文、返回结果等进行加解密、签名等处理;提供强大、易用的企业级 API 自动化测试解决方案,5分钟快速上手,提高 95% 以上回归测试效率,人人皆可使用的“零代码”自动化测试平台; 

4. 领先的 API 团队协作功能 无论您使用什么语言开发,Apikit 都可以帮您统一规范地管理起来,并提供强大的文档管理、协作、测试、分享功能 

5.还有更多的 Devops 功能 API 异常监控,对接CI/CD、DevOps 平台,支持主流IM ,也可通过自由拓展。 

ApiKit 小试牛刀 

接下来,带着大家,简单体验一下ApiKit的使用。

Apikit 有三种客户端,你可以依据自己的情况选择。三种客户端的数据是共用的,因此你可以随时切换不同的客户端。

我们推荐使用新推出的 Apikit PC 客户端,PC端拥有线上产品所有的功能,并且针对本地测试、自动化测试以及使用体验等方面进行了强化,可以提供最佳的使用感受。 



我们建议对本地测试有需求的用户使用PC端,可满足更多本地测试需求。



发起 API 测试


进入 API 文档详情页,点击上方 测试 标签,进入 API 测试页,系统会根据API文档自动生成测试界面并且填充测试数据。




填写请求参数


首先填写好请求参数。



请求头部


您可以输入或导入请求头部。批量导入的数据格式为 key : value ,一行一条header信息,如:

Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/json
Date: Mon, 30 Dec 2019 20:49:45 GMT

请****求体


请求体提供了五种类型:

  1. Form-data(表单)

  2. JSON

  3. XML

  4. Raw(自定义文本类型数据)

  5. Binary(字节流、文件参数)


产品中提供了的 JSON 和 XML 编辑器,当您已经在 API 文档中定义好 API 的请求数据结构时,只需要在测试界面填写各个字段的值,系统会自动转换为相应的 JSON 和 XML 结构的请求数据。


Query 参数


Query参数指的是地址栏中跟在问号?后面的参数,如以下地址中的 user_name 参数:

/user/login?user_name=jackliu

批量导入的数据格式为 ?key=value ,通过&分隔多个参数,如:

api.eolinker.com/user/login?user_name=jackliu&user_password=hello

REST参数

REST参数指的是地址栏被斜杠/分隔的参数,如以下地址中的user_name、user_password参数。

/user/login/{user_name}/{user_password}

注意,只需要在URL中使用 {} 将REST参数括起来,下方的请求参数名中不需要使用 {} 。


处理脚本


脚本分为 前置脚本后置脚本 两种,分别对应 API 请求前 和 返回数据后 的两个阶段。您可以通过编写 Javascript 代码,在 API 前置脚本中改变请求参数,或者是在 API 后置脚本中改变返回结果。


脚本常用于以下几种情况:

1.API 请求前对请求参数进行复制、加解密等操作,比如进行Body进行整体签名 

2. API 返回结果后对结果进行解密等


发起的API请求会依次经过以下流程。其中如果您没有编写相应的API脚本,则会略过API脚本处理阶段。



管****理 Cookie


当您测试需要 Cookie 的 API 时,可以先进行一次 API 登录或者在 Cookie 管理里添加所需的 Cookie 信息,系统会自动将 Cookie 储存起来,下次测试其他相同域名的 API 时会自动传递 Cookie 请求参数。




查看测试结果


填写好请求参数后,点击测试按钮即可得到测试报告,报告包括以下内容:

1.返回头部 

2. 返回内容 

3.实际请求头部 

4. 实际请求内容 

5.请求时间分析




快速生成mock


在高级mock页面,选择添加为mock,可快速生成mock。



将测试用例请求参数和返回参数自动带到mock的请求报文和响应报文中。



ApiKit 更多特性


新建 API 文档



团队协作,API分享



高级mock



创建自动化测试



API 异常警告



环境管理



前后置脚本



创建项目



APIHub




关于 ApiKit 的更多功能,值得你来尝试体验!


传送门:


http://www.eolink.com/?utm_source…


小结


虽然 ApiKit 目前有些功能还并不完善,但整的来说,ApiKit 还是不错的,也为接口开发调试测试提供了一种效率更佳的的解决方案。

作者:CV_coder
链接:https://juejin.cn/post/7237024604962766909
来源:稀土掘金
收起阅读 »

Mac效率神器Alfred Workflows

Alfred 是 Mac 上一款著名的效率应用,强大的功能和众多的扩展能让你在实际操作中大幅提升工作效率, 这里简单介绍下 Alfred 的工作流 背景&效果展示 作为一名程序员经常会遇到时间戳转时间、时间转时间戳的情况,以前都会打开网页在线工具进行...
继续阅读 »

Alfred 是 Mac 上一款著名的效率应用,强大的功能和众多的扩展能让你在实际操作中大幅提升工作效率,
这里简单介绍下 Alfred 的工作流



背景&效果展示


作为一名程序员经常会遇到时间戳转时间、时间转时间戳的情况,以前都会打开网页在线工具进行转换。
每次打开浏览器找到网址,然后复制内容进行转换都需要 5秒以上的时间。
有什么快捷的方式能帮助我们快速的进行这个操作吗,这里我想到的 Alfred 的工作流。
Alfred工作流可以直接脚本开发,下面是工作流开发完的效果。


1)首先唤起Alfred输入框,这里看自己设置的快捷键了
2)输入这里工作流对应的keyword (tm或tmt)然后空格输入需要转换的内容
3)回车键将转换之后的内容复制到剪切板


这个简单的工作流可以实现linux类型的时间戳转换成 yyyy-MM-dd HH:mm:ss 类型的时间字符串,
也可以将 yyyy-MM-dd HH:mm:ss 类型的时间字符串转换成时间。
转换完的内容会自动放到剪切板里面,可以直接使用 command + v 进行粘贴,也可以使用Alfred的历史剪切板进行复制。


工作流开发


1、创建空的工作流


为了简单快捷这里使用的是python开发的这个功能。
开发这个工作流首先需要再 Alfred 面板上创建一个空的工作流
Alfred -> Preferences -> Workflows -> 左下角的 + -> Blank Workflows
如下图: 



2、添加流程节点


这里使用的是 Script Filter类型的节点,可以支持keyword触发。
节点配置如下,keyword 为触发的命令关键字。下面是运行命令的配置 Script输入框中写需要运行的命令,
这里使用 {query} 方式将转换的内容传递给python脚本。 



复制到剪切板的节点如下,创建好节点用线连接就行。 



3、python脚本开发


脚本的位置放到当前工作流的根目录就行,这样不行写绝对路径,也方便工作流的导出。
1. 打开工作流根目录:右键选中工作流,点 open in find 或者 open in terminal 打开工作流根目录:右键选中工作流,点 open in find 或者 open in terminal 


2.Alfred官方提供了一个python类库方便开发工作流。在工作流根目录执行命令

pip install --target=. Alfred-Workflow

3.创建脚本 timestamp_2_time.py,然后开发对应的代码即可 。代码里面有注释大家可以看下

# -*- coding: utf-8 -*-
import sys
from datetime import datetime
from workflow import Workflow, ICON_CLOCK # 导包

def main(wf):
query = wf.args[0] # 获取传入的参数,这里能获取到需要转换的呢绒
if not query:
return
# 时间戳转时间字符串的方法
d = datetime.fromtimestamp(int(query) / 1000)
str1 = d.strftime("%Y-%m-%d %H:%M:%S")
'''
调用框架的方法添加运行的结果
可选参数是标题、副标题,arg是下一个节点的入参,icon是这个item展示的图标
如果有多个结果可以放多个,然后通过上下键选择
'''
wf.add_item(title=query, subtitle=str1, arg=str1, valid=True, icon=ICON_CLOCK)
# 展示结果内容list
wf.send_feedback()


if __name__ == '__main__':
'''构造 Workflow 对象,运行完退出
'''
wf = Workflow()
sys.exit(wf.run(main))

通过这几行简单的代码实现了时间戳转换成时间的小功能,
相比于以前的使用网页的形式,这个工作流可以将时间缩短到1秒,每次为你省下 4 秒钟的时间 😂😂😂


debug


开发的时候可能会遇到bug,可以通过下图方式打开运行日志查问题。 



开发好的工作流要使用直接导入就行


tmwf.tar
下载之后 tar -xvf tmwf.tar 解压导入就行。
这里需要注意下,我本地的python路径是 /usr/local/bin/python 大家需要换成自己的python路径。

作者:程序员大鹏
链接:https://juejin.cn/post/7252541723149238330
来源:稀土掘金
收起阅读 »

rpc比http好吗,缪论?

是什么,如何理解 RPC(Remote Procedure Call) 直译就是远程过程调用 HTTP(HyperText Transfer Protorl) 直译就是超文本传输协议 RPC和HTTP都是 请求-响应协议,但是因为出现的时机、设计理念、约定协议...
继续阅读 »

是什么,如何理解


RPC(Remote Procedure Call) 直译就是远程过程调用


HTTP(HyperText Transfer Protorl) 直译就是超文本传输协议


RPC和HTTP都是 请求-响应协议,但是因为出现的时机、设计理念、约定协议、效率、应用范围、使用规则等不同,所以是不同的名字,本质都是为了分布式系统间的通信而生,是一种应用层通信(请求-响应)协议(从OSI网络模型来看)。

RPC是 Bruce Jay Nelson 在1981年创造的术语,HTTP是在1990年左右产生的(可以参看维基百科)


RPC协议 和 RPC,到底叫什么?RPC协议=RPC


HTTP协议、HTTP,到底叫什么?HTTP协议=HTTP


RPC|HTTP只是大家的简称

1、HTTP协议不仅仅只有协议,还有超文本,传输,以及很多功能(比如编解码、面试经常背的各种参数的作用)

2、RPC协议也不仅仅只有协议,还有 编解码,服务注册发现,负载均衡等


RPC协议本质上定义了一种通信的流程,而具体的实现技术是没有约束的,每一种RPC框架都有自己的实现方式,我认为HTTP也是RPC的一种实现方式


协议直白来讲是一种约定,rpc和http都是为了服务器间的通信而生,都需要制定一套标准协议来进行通信。不过HTTP比较火,是一个全世界的统一约定,使用比较广泛。但通用也意味着冗余,所以后来又产生了很多RPC框架(自定义协议,具备优秀的性能等)


我们可以自定义RPC请求/响应 包含的消息头和消息体结构,自定义编解码方式,自定义网络通信方式,只要clientserver消息的发送和解析能对应即可,这些问题确认下来,一个RPC框架就设计出来了


下面先从请求过程看一下RPC和HTTP都会经历哪些阶段,然后再分阶段去做对比

一次请求的过程



从请求链路可以看到,最核心的只有三层:编解码、协议、网络通信


下面会从这3个角度去对比HTTP和RPC


HTTP VS RPC自定义协议


HTTP和RPC 2个关键词不具备可比较性,因为RPC包含了HTTP。


但是RPC自定义协议(thrift, protobuf, dubbo, kitex-thrift等) 是RPC的具体实现,HTTP也是RPC的具体实现,它们是具备可比较性的


编解码(序列化)



  • 序列化: 指将程序运行过程中的动态内存数据(java的class、go的struct)转化为硬盘中静态二进制数据的过程,以方便网络传输。

  • 反序列化:指将硬盘中静态二进制数据转化为程序运行过程中的动态内存数据的过程,以方便程序计算。


HTTP/1.1 一般用json


自定义RPC协议 一般用 thrift、protobuf


kitex序列化协议


协议层


编码之后,数据转换成字节流,但是RPC通信时,每次请求发送的数据大小不是固定的,那么为了区分消息的边界,避免粘包、半包等现象,我们需要定义一种协议,来使得接收方能够正确地读出不定长的内容。简单点说,通信协议就是约定客户端和服务器端传输什么数据,以及如何解析数据。


可参考

1、kitex:概览,传输协议

2、dubbo:triple 协议,概览


可以思考一下 序列化、传输协议、网络通信的关系,下面以kitex为例进行分析


kitex codec 接口定义kitex thrift 序列化实现kitex ttheader协议,kitex 发送请求核心代码


可以发现 Encode中,先根据message构造出header,写入out,然后再把data(实际的业务数据)写到out。

encode函数完全遵守 ttheader协议去构造数据。

最后再把out通过网络库发送出去

网络通信层

网络通信层主要提供一个易用的网络库,封装了操作系统提供的socket api。


HTTP的长连接和TCP长连接不是一个东西,需要注意下,TCP Keepalive是操作系统实现的功能,并不是TCP协议的一部分,需要在操作系统下进行相关配置(只能保证网络没问题,不能代表服务没问题)


其中 HTTP2 拥有多路复用、优先级控制、头部压缩等优势


可以参考


kitex:连接类型


RPC自定义协议 和 HTTP的使用场景


公司内部的微服务,对客户端提供的服务 适合用RPC,更好的性能


对外服务、单体服务、为前端提供的服务适合用HTTP


我的思考


rpc在编解码、协议层、网络通信 都比HTTP有更大的优势,那为啥不把HTTP换成RPC呢

1、人的认知,HTTP已经深入人心(或者说生态好,通用性强),几乎所有的机器、浏览器和语言默认都会支持。但是自定义RPC协议 可能很多人都没听过(比如kitex、dubbo等),还让别人支持,根本不可能。

  • 需要建设全局的DNS等等,HTTP链路中的组件都需要换成 自定义的那一套,成本极高。
  • 但是公司内部可以搞成一套,可以极大提高性能,何乐而不为。

  • 我见过的案例是很多时候并没有深入思考为什么用,而是大家都这么用,我也这么用。

2、浏览器只支持 http协议。而且浏览器不支持自定义编解码的解析
      为啥大家面向浏览器/前端 不用自定义编解码?
     http不仅可以传输json、还可以传输二进制、图片等。所以协议层可以用http,编解码用protobuf/thrift也是可行的。
  • 公司内部实际案例:服务端和客户端交互时,为了提高性能,采用protobuf编解码数据,使用http协议传输数据。

  • 但是每次请求/响应数据都是不可读的。服务端会把protobuf编码前的数据转为json,用于打印log/存储,方便排查问题。

3、RPC框架 可以自定义负载均衡,重试机制,高可用,流量控制等策略。这些是HTTP不能支持的
  • 我理解是协议层用的http,但是内部的运行机制还是自定义的。http只是定义了传输数据的格式。举个例子:http的流量控制其实用的是 tcp的滑动窗口,http协议本身不具备这些功能。但是rpc是可以自己加这些功能的。这些功能必然有数据传输,这个传输协议用的http。

作者:cli
链接:https://juejin.cn/post/7264454873588449336
来源:稀土掘金

收起阅读 »

SpringBoot获取不到用户真实IP怎么办

今天周六,Binvin来总结一下上周开发过程中遇到的一个小问题,项目部署后发现服务端无法获取到客户端真实的IP地址,这是怎么回事呢?给我都整懵逼了,经过短暂的思考,我发现了问题的真凶,那就是我们使用了Nginx作的请求转发,这才导致了获取不到客户端真实的IP地...
继续阅读 »
今天周六,Binvin来总结一下上周开发过程中遇到的一个小问题,项目部署后发现服务端无法获取到客户端真实的IP地址,这是怎么回事呢?给我都整懵逼了,经过短暂的思考,我发现了问题的真凶,那就是我们使用了Nginx作的请求转发,这才导致了获取不到客户端真实的IP地址,害,看看我是怎么解决的吧!

问题原因

客户端请求数据时走的是Nginx反向代理,默认情况下客户端的真实IP地址会被其过滤,使得SpringBoot程序无法直接获得真实的客户端IP地址,获取到的都是Nginx的IP地址。

解决方案:

通过更改Nginx配置文件将客户端真实的IP地址加到请求头中,这样就能正常获取到客户端的IP地址了,下面我一步步带你看看如何配置和获取。
修改Nginx配置文件

#这个参数设置了HTTP请求头的Host字段,host表示请求的Host头,也就是请求的域名。通过这个设置,Nginx会将请求的Host头信息传递给后端服务。
proxy_set_header Host $host;
#这个参数设置了HTTP请求头的X−Real−IP字段,remote_addr表示客户端的IP地址。通过这个设置,Nginx会将客户端的真实IP地址传递给后端服务
proxy_set_header X-Real-IP $remote_addr;
#这个参数设置了HTTP请求头的 X-Forwarded-For字段,"X-Forwarded-For"是一个标准的HTTP请求头,用于表示HTTP请求经过的代理服务器链路信息,proxy_add_x_forwarded_for表示添加额外的服务器链路信息。
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

修改后我的nginx.conf中的server如下所示

server {
listen 443 ssl;
server_name xxx.com;

ssl_certificate "ssl证书pem文件";
ssl_certificate_key "ssl证书key文件";
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;

location / {
root 前端html文件目录;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# 关键在下面这个配置,上面的配置自己根据情况而定就行
location /hello{
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

SpringBoot代码实现

第一种方式:在代码中直接通过X-Forwarded-For获取到真实IP地址

@Slf4j
public class CommonUtil {
/**
* <p> 获取当前请求客户端的IP地址 </p>
*
* @param request 请求信息
* @return ip地址
**/
public static String getIp(HttpServletRequest request) {
if (request == null) {
return null;
}
String unknown = "unknown";
// 使用X-Forwarded-For就能获取到客户端真实IP地址
String ip = request.getHeader("X-Forwarded-For");
log.info("X-Forwarded-For:" + ip);
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
log.info("Proxy-Client-IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
log.info("WL-Proxy-Client-IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
log.info("HTTP_X_FORWARDED_FOR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED");
log.info("HTTP_X_FORWARDED:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_CLUSTER_CLIENT_IP");
log.info("HTTP_X_CLUSTER_CLIENT_IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
log.info("HTTP_CLIENT_IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_FORWARDED_FOR");
log.info("HTTP_FORWARDED_FOR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_FORWARDED");
log.info("HTTP_FORWARDED:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_VIA");
log.info("HTTP_VIA:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("REMOTE_ADDR");
log.info("REMOTE_ADDR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
log.info("getRemoteAddr:" + ip);
}
return ip;
}

第二种方式:在application.yml文件中加以下配置,直接通过request.getRemoteAddr()并可以获取到真实IP

server:
port: 8090
tomcat:
#Nginx转发 获取客户端真实IP配置
remoteip:
remote-ip-header: X-Real-IP
protocol-header: X-Forwarded-Proto

作者:BivinCode
链接:https://juejin.cn/post/7266040474321027124
来源:稀土掘金

收起阅读 »

69.9K Star,最强开源内网穿透工具:frp

作为一名开发者,有很多场景需要用到内网穿透,比如:我们在接入一些大平台做第三方应用时,在本地开发微信公众号工具的时候需要让微信平台能否访问到本地提供的接口。除此之外,还有很多其他场景,也会用到,比如:把放在家里的NAS或服务器暴露到公网上,这样在外面的时候也可...
继续阅读 »

作为一名开发者,有很多场景需要用到内网穿透,比如:我们在接入一些大平台做第三方应用时,在本地开发微信公众号工具的时候需要让微信平台能否访问到本地提供的接口。除此之外,还有很多其他场景,也会用到,比如:把放在家里的NAS或服务器暴露到公网上,这样在外面的时候也可以随时随地的访问。


说到内网传统,TJ君第一个想到的是国内最早的一款知名软件:花生壳。但是今天不是要推荐它,而是要推荐一个更牛的开源项目:frp!该项目目前已经收获了69.9 K Star,在GitHub上获得了极大的认可!


下载安装

frp目前已经提供了大部分操作系统的支持版本,通过这个链接:github.com/fatedier/fr… 就可以下载到适合你使用的安装。


以Windows的包为例,解压后可以获得这些内容:


frps是服务端程序,frpc是客户端程序。ini文件就是对应的配置文件。


首发 blog.didispace.com/tj-opensour…,转载请注明出处


暴露内网服务


内网穿透的玩法有很多,这里列举一个比较常见的例子。


比如:我要暴露一个只有自己能访问到服务器。那么可以这样配置:


配置 frps.ini,并启动服务端 frps

[common]
bind_port = 7000

在需要暴露到外网的机器上部署 frpc,配置如下:

[common]
server_addr = x.x.x.x
server_port = 7000

[secret_ssh]
type = stcp
# 只有 sk 一致的用户才能访问到此服务
sk = abcdefg
local_ip = 127.0.0.1
local_port = 22

在想要访问内网服务的机器上也部署 frpc,配置如下:

[common]
server_addr = x.x.x.x
server_port = 7000

[secret_ssh_visitor]
type = stcp
# stcp 的访问者
role = visitor
# 要访问的 stcp 代理的名字
server_name = secret_ssh
sk = abcdefg
# 绑定本地端口用于访问 SSH 服务
bind_addr = 127.0.0.1
bind_port = 6000

把frpc也都启动起来之后,通过 SSH 就可以访问内网机器了

ssh -oPort=6000 test@127.0.0.1

其他支持


除了上面的玩法之外,frp还有很多玩法,比如:

1、自定义域名访问内网的 Web 服务

2、转发 DNS 查询请求

3、转发 Unix 域套接

4、对外提供简单的文件访问服务

5、为本地 HTTP 服务启用 HTTPS

6、点对点内网穿透


篇幅有限,具体如何配置这里就不多说了,有需要的读者可以直接查看官方文档,均有详细的服务端客户端配置案例。


最后,奉上相关链接:


开源地址:github.com/fatedier/fr…
文档地址:gofrp.org/docs/

作者:程序猿DD
链接:https://juejin.cn/post/7263283712224395321
来源:稀土掘金
收起阅读 »

你的野心距离成功,就差一个机会

这几天,深圳的几个达友张罗着找时间线下聚聚。 这两天在看《狂飙》。 自从开始写公众号后,业余时间不是在码字,就是在思考怎么码字。 这么热门的片子,加一起才看 6 集。 里面的张颂文,演技的确赞。 不过东哥想的是,演技这么好,怎么出道20年才火...
继续阅读 »

这几天,深圳的几个达友张罗着找时间线下聚聚。





这两天在看《狂飙》。


自从开始写公众号后,业余时间不是在码字,就是在思考怎么码字。


这么热门的片子,加一起才看 6 集。


里面的张颂文,演技的确赞。


不过东哥想的是,演技这么好,怎么出道20年才火起来?


他缺的,是什么?







人怎么能成功?简单地说



成功 = 能力 × 机会



能力是修炼内功,让自己变得更专业,成为头部的专家,是自己应该搞定的事情。


机会这事儿,就朦胧多了。


酒香不怕巷子深,是物质稀缺时代的事情。


整个镇子只有一家酿酒,巷子再深都有酒鬼登门。


而当前世界已经进入到了产能过剩、需求不足的时代。


国民总时间恒定,大家在存量的池子里杀的头破血流。


没机会展现的才华,只能被风沙埋没。


机会,越来越重要。


你与成功之间,可能就差一个机会。


比如张颂文,没机会拿到好剧本好角色,就只能是个小演员。


46 岁,买不起房子,没存款,感觉自己好失败。


每天都被拒绝,甚至侮辱地拒绝,让他滚蛋。


所以我们不能只死磕自己苦练内功,坐等被赏识被发现。


傻傻的,感动自己。


要为自己的野心,创造机会。


怎么创造呢?




去机会更多的地方。 在水多的地方打井,选鱼多的地方捞鱼。


最关键的选择,一个是城市,一个是赛道。


做金融,就到上海和深圳,搞互联网,就去北京和深圳。


东哥做香港保险,一个很大的优势就是所在的城市。


从福田站到香港西九龙,高铁15分钟。


市中心到市中心。


赛道选择上,做和钱近的工作,做可积累的工作。


能和人打交道,就别和机器打交道;能做销售,就别做售后。


销售看到的都是机会和钱,售后看到的都是负面问题。




向上链接,寻找大节点,利用好高能级的关系


人和人能量密度不同,大节点就是个人崛起的发动机。


达叔曾谈到过,在他写公众号的过程中,曾被欧神、医业观察的星哥、凯叔药械升职记的凯叔推荐。


三次推荐,引来了大批流量,成就了达叔的崛起。


在职场,就是要发现身边的强者,做深度绑定,成为强者权力结构件的一部分。


多花心思,多花钱,努力走进领导的小圈子,成为他身边的人。


利用他的势能,实现职业和财富的崛起。


就像《人民的名义》里面,高育良提到汉大帮的时候说



主观上说,我从没想过把人民赋予的权力向任何一位学生私相授受,但客观上也许私相授受了。
做了这么多年的法学教授,教了这么多年书,学生少不了,对自己学生呢,谁都不可能没感情,用人时就难免有偏爱。






进入不了核心圈子,就注定是个边缘人物,最后沦为炮灰。


如果在食物、资源匮乏的时候,你不坐在桌子旁,大概率就得躺在桌子上。


需要注意的是,链接的能级差不能太大。


基层员工,就别总想着去链接总经理董事长。


除非你是王思聪,他是王健林。


县官不如现管,链接好那个直接自己升职加薪的人,最有价值。




多和人互动,无论是线上还是线下。


人和人之间的互动永远是这个世界的最核心的算法,剩下全是工具。


职场中,多和实权派互动,和业务部门互动,和给公司赚钱最多的部门互动。


最忌讳的,就是整天对着电脑研究计算模型。


上班一天,接触的人不过办公室里这三五个,和固定的几个窗口,加一起超不过10个人。


能深入沟通和交流的,只有个位数。


除非你能成为公司最顶级的技术大牛,非你不可的那种,否则随时都可能被干掉。


东哥在自己的在每一篇文章后,都会附上了自己的个人微信,也是希望能和大家链接。


短视频时代,能潜下心来阅读文字本身就很可贵。


尤其还是东哥这种枯燥乏味谈赚钱的文章。


但有趣的是,很多人加微信以后一声不吱,甚至我主动询问也不回应。


这样的链接很难有实际价值。


只有沟通彼此的链接才会有价值,无论这个价值是信息、资源还是项目。


没有互动,彼此就仅仅是微信系统里面的几个号码。




多尝试,多拓展。机会是干出来的,不是想出来的。


就像猫王和村上春树的故事。


猫王是二十世纪收入最高的歌星,在卖唱及作明星之前是一位货车司机,每月的收入只数百美元。


工余之暇,他去试唱,被唱片公司相中,然后一举成名。


每年的收入以千万美元计。


村上春树则是在看棒球比赛的时候,突然想要写本小说。


比赛一结束,他就立马赶到文具店买了笔和纸,开始创作他的第一本小说《且听风吟》。


人生有无数种可能。当前的状态,不代表永远。


状态之外,要不断向外拓展,比如猫王的试镜,村上春树的小说。


只有不断地试,才会发现身上其他可能。


否则就只能,守着安静的沙漠 等待花开,看着别人的快乐默默感慨。


尤其自媒体,轻资产,风险可控,是时代给予我们的拓展机会。


多想想,自己有什么东西,可以不断打磨放大?


写公众号这几个月,和各路大 V 沟通,大开眼界。


医生讲医疗,房 V 讲房产都是常规操作。


搞心理学的做星座内容,看桃花运、分析财运,居然也有不少客户。


擅长做饭的可以做美食博主,身材好的可以做服装博主。


你擅长的是什么,能打出什么机会?




用无限游戏的思维方式,多推销自己。


无论做什么,总有一双眼睛在背后默默看着你。


合作的过程就是展现自己的机会。


工作中做汇报做提案,与同事沟通方案,最高任务是什么?


有人会说是把事情搞定,所以应该对事不对人。


对项目推进有利的,就要据理力争。


实际上,这样的工作是低效的,有时候甚至会起反作用。


同样的事情,不同的人沟通,常会有完全相反的结果。


对事不对人,现实中不存在。


理念和现实冲突时,错的永远是理念,不会是现实


沟通的最高任务,不是推销自己对项目的主张,而是推销自己这个人


从这个角度想,我们就不会局限于一城一地之得失,一朝一夕之荣辱。


而会站在一个更高层面,整个职业生涯的高度,来看待当下的问题。


项目成败的因素有很多,不一定是我们可以控制的。


但项目可以失败,人不可以失败


每一次沟通,都要展现自己足够靠谱,足够专业,是更长远合作的好伙伴。


所以要利用好每一次沟通,甚至是每一次刁难。


都是在给自己种善因。




资源也好,技术也罢,多分享,多展现。


不要藏着掖着,害怕教会徒弟饿死师傅。


太阳下面没有新鲜事,你能找到,别人也能找到。


真正的强者,你打压不了。


与其彼此竞争拼个头破血流,不如投资强者。


在他发展壮大的必然道路上,助他一臂之力。


也助以后的自己一臂之力。


你能成就多少强者,就有多大能量。


就像春秋时期齐桓公,想拜鲍叔牙为相,同时还要宰了对手公子纠的谋士管仲。


谁知鲍叔牙对他说:“我的才能只能让齐国平安,如果您要称霸天下,一定得拜管仲为相。”


齐桓公从之,尽释前嫌,拜管仲为国相,终成王霸之业,鲍叔牙也成就了自己千古美名。




结合多动手和多分享的思路,打造自己的产品。


就像伟大的程序员 Linus 说的



Talk is cheap. Show me the code.
被扯没用的,给我看代码。



产品是创作能力的背书,是自控能力的表现,是你最大的筹码。


自己的作品,可以理解为一种社交货币。


打磨作品就是往里钱,在将来的某个时刻,可以兑换它。


陌生人介绍的时候,就可以直接把作品丢出来。


有了作品,就有了各种合作的可能。


东哥这么小一个号,都有人过来谈合作,有时会一些意外的机会。




最后一点,给生活增加一些随机性。


平时晚上在家读书写作充电。下班时,朋友邀请参加一个陌生的聚会,去不去?


马上出发。


这就是生活中的随机性,说不定在聚会过程中会有新的收获。


一个漂亮的妞做女朋友,或一个合作的机会赚点钱。


这就是《黑天鹅》里面提到的正面黑天鹅


努力把自己暴露在可能发生正向意外的环境里。


用时间上微小的损失,换正面黑天鹅出现的可能,进而突破路径依赖。


就像这几天,深圳的几个达友张罗着找时间线下聚聚。


这群人里面有做医疗的,做芯片贸易的,做生物的,还有东哥是卖保险的。


彼此之间可能交流什么,完全未知。


但也就是因为这种未知,才能让自己突破日常的局限,也就更值钱。




供给过剩的时代,无数人在存量的池子里杀得头破血流。


给自己多创造一些机会,杀出来的可能就大一些

  • 去鱼多的地方捞鱼,挤到机会多的地方;
  • 向上链接,发现强者,进入到强者的核心圈子;
  • 多和人互动,不要沉迷于技术;
  • 基于自身优势多对外拓展,寻找机会;
  • 无限游戏思维,办事儿的时候笑面挑战,推销自己;
  • 多分享,尤其挖掘并投资潜力股;
  • 打造自己的作品,积累社交货币;
  • 给生活增加一些随机性;


避免像张颂文,满腹才华,却走了漫长的二十年。


你距离成功,也许只缺一个机会。


那就造一个出来。



作者:jetorz
来源:mdnice.com/writing/39186f5978a84668b24442c684d01fa3
收起阅读 »

孩子是双眼皮还是单眼皮?来自贝叶斯算法的推测

问题描述 最近家里有了宝宝,孩子他妈很希望孩子早日长出双眼皮,并因为他至今是单眼皮而有些担心。虽然我小时候也是单眼皮,后来才显现出双眼皮,但不排除孩子长大后仍是单眼皮的概率。为此我感到需要计算一下孩子是单眼皮基因的概率。 我家的情况是这样,宝爸宝妈、爷爷奶...
继续阅读 »

问题描述


最近家里有了宝宝,孩子他妈很希望孩子早日长出双眼皮,并因为他至今是单眼皮而有些担心。虽然我小时候也是单眼皮,后来才显现出双眼皮,但不排除孩子长大后仍是单眼皮的概率。为此我感到需要计算一下孩子是单眼皮基因的概率。


我家的情况是这样,宝爸宝妈、爷爷奶奶、姥姥姥爷都是双眼皮。


查了一下资料,双眼皮是显性基因,因此除非宝爸宝妈都是杂合性基因且都贡献单眼皮片段,孩子才能是单眼皮。


这里做一下假设,全部人群中有3/4是双眼皮,双眼皮人群中纯合基因有1/2。即



其中S表示双眼皮,D表示单眼皮,C表示纯合基因,Z表示杂合基因。


祖父辈人基因类型的后验概率


父母双方的情况是对等的,因此只挑选其中一方进行计算。以父亲为例,爷爷奶奶可能的基因类型组合有:CC,CZ和ZZ。先验概率为:



已知父亲是双眼皮,则爷爷奶奶是CC组合的后验概率为:



类似地可以算出



进行归一化后得到:



这里之所以要进行归一化,是因为在计算过程中对概念进行了替换,我们利用了爷爷奶奶都是双眼皮的信息。


父母基因类型的概率


仍然以父亲为对象进行计算,其是纯合基因的概率为:



是杂合基因的概率为:



剩下1/20的概率是表现为单眼皮的概率,需要排除掉。也就是说,在观察到爷爷奶奶父亲都是双眼皮的情况下,父亲是纯合基因的概率为






































,是杂合基因的概率为







































孩子双眼皮的概率


综合以上,孩子是双眼皮的概率为:



可见这个概率是很高的。


压力测试


在之前的计算里面,由于没有一般人群的统计数据,我们假设全部人群中有3/4是双眼皮,双眼皮人群中纯合基因有1/2。这里我们换一组数据,假设全部人群中有1/4是双眼皮,双眼皮人群中纯合基因有1/4,看看这样会对结果造成多大影响。


这种情况下



父母任意一方是纯合基因的概率为







































,是杂合基因的概率为









































最终得到孩子是双眼皮的概率是



可见概率依然非常高。“六个钱包”都是双眼皮是一个非常可靠的信号。


作者:Kelly1024
来源:mdnice.com/writing/f71210a4999f4bd2a674f9bead00551c
收起阅读 »

原来,我们的代码就是这样被污染的...

最近 CR 了这样一段代码: 我们团队的变量命名规范是小写驼峰,但是这里可以看到,一个 http api 接口请求的工具函数的入参却是下划线。这是一个内部项目,前后端都是我们团队开发的。这个项目的代码随处都能看到这样的不符合规范的痕迹,而且屡禁不止。我不禁在...
继续阅读 »

最近 CR 了这样一段代码:


插图1.png


我们团队的变量命名规范是小写驼峰,但是这里可以看到,一个 http api 接口请求的工具函数的入参却是下划线。这是一个内部项目,前后端都是我们团队开发的。这个项目的代码随处都能看到这样的不符合规范的痕迹,而且屡禁不止。我不禁在想,为什么会这样?


先探究一下这个问题的表面原因,让我们从底层开始:



  • DB 表结构按照团队规范,字段名是下划线分隔的

  • node 服务定义 entity 的时候,直接复用 DB 表结构字段名

  • service 模块在查询 DB 之后,直接回吐结果,所以 service 类返回的数据结构的变量名也是下划线的

  • node api 的 controller 通过调用 service 模块的函数,获取数据处理之后,也直接回吐下划线的变量名的数据结构

  • 因为 node api 的接口协议是下划线的,所以 web 页面请求 http api 的出入参也是下划线的,如上图所示

  • 因为 http api 返回的结果是下划线的,然后页面逻辑直接使用,最后,页面逻辑也出现了大量下划线


这是一段挺长的链路,只要我们在后面的任意一个环节处理一下变量名的转换,都能避免这个问题,但是并没有。


插图2.png


为什么会这样?稍微探究一下这里的深层原因,也是挺有意思的。


首先,项目启动时没有严格把控代码质量。这里的原因有很多,比如项目工期紧、主要以完成功能为主、内部项目不需要要求这么严格、项目启动时是直接 copy 另外一个项目的,那个项目也是这样写的等等。但是,这些统统都是借口!我是要负主要责任的。


其次,没有开发同学想过要去优化这里的代码。参与这个项目的同学并不是不知道如何去解决这个问题,但就是没人想要去解决这个问题。大家更多的是选择“入乡随俗”,别人这么写,我也这么写吧。


最后,Code Review 没有严抓。问题都是越早处理成本越小,如果在早期我们就开始严抓 Code Review 的话,说不定就能及时改善这个问题了。


这个案例是一个非常真实的破窗效应案例,这里面有不少地方值得我们深思的。


首先,它会污染整个项目。如果我们在项目的一开始就没有把控好代码质量,那我们的项目代码很快就会被污染。最开始的开发同学可能知道历史原因,自然不会觉得不自然。后续加入的维护者看到项目代码是这样子的,就会误以为这个项目的代码规范就是这样子的,于是也“入乡随俗”,最终整个项目的代码就被污染,破烂不堪。


其次,它会污染依赖的项目。比如这里就是 node 项目先被污染,对外提供的 api 也受到污染,然后调用这些 api 的 web 项目也被污染了。很多时候,api 的接口协议都是由后端开发来定,如果碰到一些缺乏经验的后端开发,前端同学会收到一些很奇怪的接口协议,比如字段命名规范不统一,冗余字段,设计不合理等等,由于前端的话语权较弱的缘故,很可能会被动接受,如果处理不当,前端项目的代码就会受到污染了。


最后,它会污染整个开发团队!初创的开发同学就不说了;后续的维护者看到代码是这样子的,不是错误认知这个项目的代码规范,就是错误认知团队的代码规范;更为糟糕的是,阅读项目源码的其他同学也会受到污染:“哦,原来别人也是这样写代码的”。噩梦由此而生...


插图3.png


破窗就像病毒一样,快速并疯狂地污染整个项目代码,然后传染给开发团队,最后扩散到其他团队。这个病毒传染的范围很广,速度很快,一不留神,整个团队就会沦陷。那我们应该怎样医治呢? 主要有 3 个方向:


首先,增强个人免疫力。形成个人的编码风格,然后坚持它,这是治本的良方。良好的编码风格是不会损坏个人的编码效率的,反而会有助于提升个人的研发效率,主要体现在以下几个方面:



  • 减少低级错误

  • 提升代码的可阅读性,提升代码的可维护性,从而提升团队协作效率

  • 形成“肌肉记忆”,提升编码效率

  • 避免返工,主要体现在糟糕的设计问题和编码风格冲突问题


其次,做好防护,避免传播。比如我们这个简单的案例,在整个链路的任何一个环节,都能轻松处理这个问题,这样就不会传播到后面的环节了。很多人碰到一些历史破窗代码时,可能会觉得修复这些破窗成本太大了,那就不要去修复,只需要保证自己写的新代码是良好的,也是一个不错的方案。已经中毒的人我们没有能力医治的时候,起码,我们可以认真做好防护,避免病毒进一步传播!


最后,对症下药医治。只有千日做贼,那有千日防贼,我们总是需要想办法医治这个病,而治病的药方就是重构。这里就不深入讲了,重构也是个大学问,只需要知道,越早重构,成本越低,比如在 Code Review 的时候就要严抓这些问题,并跟踪修复情况。


不知道,你的“免疫力”修炼得怎么样了?



【讨论问题】


除了这里的案例,还有很多其他类型的破窗,你都碰到了哪些?


欢迎在评论区分享你的想法,一起讨论。


作者:潜龙在渊灬
来源:juejin.cn/post/7252888158828642360
收起阅读 »

为什么大部分人做不了架构师?

腾小云导读成为架构师,是许多程序员的职业梦想。然而其中只有少数有着丰厚编码积累、超强自驱力和独到思维的程序员才能最终成为架构师。其实,日常工作中小到某个功能的开发,大到整个业务系统的设计,都可以看到架构设计的影子。《从0开始学架构》一书是颇受程序员欢迎的架构设...
继续阅读 »

腾小云导读

成为架构师,是许多程序员的职业梦想。然而其中只有少数有着丰厚编码积累、超强自驱力和独到思维的程序员才能最终成为架构师。其实,日常工作中小到某个功能的开发,大到整个业务系统的设计,都可以看到架构设计的影子。《从0开始学架构》一书是颇受程序员欢迎的架构设计入门教程。接下来本文作者将提取该书籍之精髓,结合自身经验分享架构设计常见方法以及高可用、高性能、可扩散架构模式的实现思路,将架构设计思维“为我所用”、提升日常研效。希望对你有帮助~

目录

1 基本概念与设计方法

2 高性能架构模式

2.1 存储高性能

2.2 计算高性能

3 高可用架构模式

3.1 理论方法

3.2 存储高可用

3.3 计算高可用

4 可扩展架构模式

5 总结

之前本栏目《腾讯专家10年沉淀:后海量时代的架构设计》、《工作十年,在腾讯沉淀的高可用系统架构设计经验》两篇文章中,两位腾讯的开发者结合自身经历,分享了架构设计的实践经验。而本期,本栏目特邀腾讯云对《从0开始学架构》一书提取精髓,并结合亲身经验做分享。

01、基本概念与设计方法

在讲解架构思想之前,我们先统一介绍一下基本概念的含义,避免每个人对系统、框架、架构这些名词的理解不一致导致的误解。下面是《从0开始学架构》作者对每个名词的定义。其作用域仅限本文范畴,不用纠结其在其他上下文中的意义。

系统:系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。 子系统:子系统也是由一群有关联的个体所组成的系统,多半会是更大系统中的一部分。模块:从业务逻辑的角度来拆分系统后,得到的单元就是“模块”。划分模块的主要目的是职责分离。组件:从物理部署的角度来拆分系统后,得到的单元就是“组件”。划分组件的主要目的是单元复用。框架:是一整套开发规范,是提供基础功能的产品。架构:关注的是结构,是某一套开发规范下的具体落地方案,包括各个模块之间的组合关系以及它们协同起来完成功能的运作规则。
由以上定义可见,所谓架构,是为了解决软件系统的某个复杂度带来的具体问题,将模块和组件以某种方式有机组合,基于某个具体的框架实现后的一种落地方案。

而讨论架构时,往往只讨论到系统与子系统这个顶层的架构。

可见,要进行架构选型,首先应该知道自己要解决的业务和系统复杂点在哪里,是作为秒杀系统有瞬间高并发,还是作为金融科技系统有极高的数据一致性和可用性要求等。

一般来说,系统的复杂度来源有以下几个方面:

高性能

如果业务的访问频率或实时性要求较高,则会对系统提出高性能的要求。

如果是单机系统,需要利用多进程、多线程技术。

如果是集群系统,则还涉及任务拆分、分配与调度,多机器状态管理,机器间通信,当单机性能达到瓶颈后,即使继续加机器也无法继续提升性能,还是要针对单个子任务进行性能提升。

高可用

如果业务的可用性要求较高,也会带来高可用方面的复杂度。高可用又分为计算高可用和存储高可用。

针对计算高可用,可以采用主备(冷备、温备、热备)、多主的方式来冗余计算能力,但会增加成本、可维护性方面的复杂度。

针对存储高可用,同样是增加机器来冗余,但这也会带来多机器导致的数据不一致问题,如遇到延迟、中断、故障等情况。难点在于怎么减少数据不一致对业务的影响。

既然主要解决思路是增加机器来做冗余,那么就涉及到了状态决策的问题。即如果判断当前主机的状态是正常还是异常,以及异常了要如何采取行动(比如切换哪台做主机)。

对主机状态的判断,多采用机器信息采集或请求响应情况分析等手段,但又会产生采集信息这一条通信链路本身是否正常的问题,下文会具体展开讨论。事实上,状态决策本质上不可能做到完全正确。

而对于决策方式,有以下几种方式:

独裁式:存在一个独立的决策主体来收集信息并决定主机,这样的策略不会混乱,但这个主体本身存在单点问题。 协商式:两台备机通过事先指定的规则来协商决策出主机,规则虽然简单方便,但是如果两台备机之间的协商链路中断了,决策起来就会很困难,比如有网络延迟且机器未故障、网络中断且机器未故障、网络中断其机器已故障,多种情况需要处理。民主式:如果有多台备机,可以使用选举算法来投票出主机,比如 Paxos 就是一种选举算法,这种算法大多数都采取多数取胜的策略,算法本身较为复杂,且如果像协商式一样出现连接中断,就会脑裂,不同部分会各自决策出不同结果,需要规避。

可扩展性

众所周知在互联网行业只有变化才是永远不变的,而开发一个系统基本都不是一蹴而就的,那应该如何为系统的未来可能性进行设计来保持可扩展性呢?

这里首先要明确的一个观点就是,在做系统设计时,既不可能完全不考虑可扩展性,也不可能每个设计点都考虑可扩展性,前者很明显,后者则是为了避免舍本逐末,为了扩展而扩展,实际上可能会为不存在的预测花费过多的精力。

那么怎么考虑系统的未来可能性从而做出相应的可扩展性设计呢?这里作者给出了一个方法:只预测两年内可能的变化,不要试图预测五年乃至十年的变化。因为对于变化快的行业来说,预测两年已经足够远了,再多就可能计划赶不上变化。而对变化慢的行业,则预测的意义更是不大。

要应对变化,主要是将变与不变分隔开来。

这里可以针对业务,提炼变化层和稳定层,通过变化层将变化隔离。比如通过一个 DAO 服务来对接各种变化的存储载体,但是上层稳定的逻辑不用知晓当前采用何种存储,只需按照固定的接口访问 DAO 即可获取数据。

也可以将一些实现细节剥离开来,提炼出抽象层,仅在实现层去封装变化。比如面对运营上经常变化的业务规则,可以提炼出一个规则引擎来实现核心的抽象逻辑,而具体的规则实现则可以按需增加。

如果是面对一个旧系统的维护,接到了新的重复性需求,而旧系统并不支持较好的可扩展性,这时是否需要花费时间精力去重构呢?作者也提出了《重构》一书中提到的原则:事不过三,三则重构。

简而言之,不要一开始就考虑复杂的做法去满足可扩展性,而是等到第三次遇到类似的实现时再来重构,重构的时候采取上述说的隔离或者封装的方案。

这一原则对新系统开发也是适用的。总而言之就是,不要为难以预测的未来去过度设计,为明确的未来保留适量的可扩展性即可。

低成本

上面说的高性能、高可用都需要增加机器,带来的是成本的增加,而很多时候研发的预算是有限的。换句话说,低成本往往并不是架构设计的首要目标,而是设计架构时的约束限制。

那如何在有限的成本下满足复杂性要求呢?往往只有“创新”才能达到低成本的目标。举几个例子:

NoSQL 的出现是为解决关系型数据库应对高并发的问题。 全文搜索引擎的出现是为解决数据库 like 搜索效率的问题。Hadoop 的出现是为解决文件系统无法应对海量数据存储与计算的问题。Facebook 的 HipHop PHP 和 HHVM 的出现是为解决 PHP 运行低效问题。新浪微博引入 SSD Cache 做 L2 缓存是为解决 Redis 高成本、容量小、穿透 DB 的问题。Linkedin 引入 Kafka 是为解决海量事件问题。
上述案例都是为了在不显著增加成本的前提下,实现系统的目标。

这里还要说明的是,创造新技术的复杂度本身就是很高的,因此一般中小公司基本都是靠引入现有的成熟新技术来达到低成本的目标;而大公司才更有可能自己去创造新的技术来达到低成本的目标,因为大公司才有足够的资源、技术和时间去创造新技术。

安全

安全是一个研发人员很熟悉的目标,从整体来说,安全包含两方面:功能安全和架构安全。

功能安全是为了“防小偷”,即避免系统因安全漏洞而被窃取数据,如 SQL 注入。常见的安全漏洞已经有很多框架支持,所以更建议利用现有框架的安全能力,来避免重复开发,也避免因自身考虑不够全面而遗漏。在此基础上,仍需持续攻防来完善自身的安全。

架构安全是为了“防强盗”,即避免系统被暴力攻击导致系统故障,比如 DDOS 攻击。这里一方面只能通过防火墙集运营商或云服务商的大带宽和流量清洗的能力进行防范,另一方面也需要做好攻击发现与干预、恢复的能力。

规模

架构师在宣讲时往往会先说自己任职和设计过的大型公司的架构,这是因为当系统的规模达到一定程度后,复杂度会发生质的变化,即所谓量变引起质变。

这个量,体现在访问量、功能数量及数据量上。

访问量映射到对高性能的要求。功能数量需要视具体业务会带来不同的复杂度。而数据量带来的收集、加工、存储、分析方面的挑战,现有的方案基本都是基于 Google 的三篇大数据论文的理论:

Google File System 是大数据文件存储的技术理论。Google Bigtable 是列式数据存储的技术理论。Google MapReduce 是大数据运算的技术理论。
经过上面的分析可以看到,复杂度来源很多,想要一一应对,似乎会得到一个复杂无比的架构,但对于架构设计来说,其实刚开始设计时越简单越好,只要能解决问题,就可以从简单开始再慢慢去演化,对应的是下面三条原则:

合适原则:不需要一开始就挑选业界领先的架构,它也许优秀,但可能不那么适合自己,比如有很多目前用不到的能力或者大大超出诉求从而增加很多成本。其实更需要考虑的是合理地将资源整合在一起发挥出最大功效,并能够快速落地。简单原则:有时候为了显示出自身的能力,往往会在一开始就将系统设计得非常复杂,复杂可能代表着先进,但更可能代表着“问题”,组件越多,就越可能出故障,越可能影响关联着的组件,定位问题也更加困难。其实只要能够解决诉求即可。演化原则:不要妄想一步到位,没有人可以准确预测未来所有发展,软件不像建筑,变化才是主题。架构的设计应该先满足业务需求,适当的预留扩展性,然后在未来的业务发展中再不断地迭代,保留有限的设计,修复缺陷,改正错误,去除无用部分。这也是重构、重写的价值所在。

即使是 QQ、淘宝这种如今已经非常复杂的系统,刚开始时也只是一个简单的系统,甚至淘宝都是直接买来的系统,随着业务发展也只是先加服务器、引入一些组件解决性能问题,直到达到瓶颈才去重构重写,重新在新的复杂度要求下设计新的架构。

明确了设计原则后,当面对一个具体的业务,我们可以按照如下步骤进行架构设计:

识别复杂度:无论是新设计一个系统还是接手一个混乱的系统,第一步都是先将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题。复杂度的主要来源上文已经说过,可以按照经验或者排查法进行分析。方案对比:先看看业界是否有类似的业务,了解他们是怎么解决问题的,然后提出3~5个备选方案,不要只考虑做一个最优秀的方案,一个人的认知范围常常是有限的,逼自己多思考几个方案可以有效规避因为思维狭隘导致的局限性,当然也不要过多,不用给出非常详细的方案,太消耗精力。备选方案的差异要比较明显,才有扩宽思路和对比的价值。设计详细方案:当多个方案对比得出最终选择后,就可以对目标方案进行详细的设计,关键细节需要比较深入,如果方案本身很复杂,也可以采取分步骤、分阶段、分系统的实现方式来降低实现复杂度。当方案非常庞大的时候,可以汇集一个团队的智慧和经验来共同设计,防止因架构师的思维盲区导致问题。

02、高性能架构模式

2.1 存储高性能

互联网业务大多是数据密集型的业务,其对性能的压力也常常来自于海量用户对数据的高频读写压力上。因此解决高性能问题,首先要解决数据读写的存储高性能问题。

读写分离

在大多数业务中,用户查询和修改数据的频率是不同的,甚至是差别很大的,大部分情况下都是读多写少的,因此可以将对数据的读和写操作分开对待,对压力更大的读操作提供额外的机器分担压力,这就是读写分离。

读写分离的基本实现是搭建数据库的主从集群,根据需要提供一主一从或一主多从。

注意是主从不是主备,从和备的差别在于从机是要干活的。

通常在读多写少的情况下,主机负责读写操作,从机只负责读操作,负责帮主机分担读操作的压力。而数据会通过复制机制(如全同步、半同步、异步)同步到从机,每台服务器都有所有业务数据。

既然有数据的同步,就一定存在复制延迟导致的从机数据不一致问题,针对这个问题有几种常见的解法,如:

写操作后同一用户一段时间内的读操作都发给主机,避免数据还没同步到从机,但这个逻辑容易遗漏。读从机失败后再读一次主机,该方法只能解决新数据未同步的问题,无法解决旧数据修改的问题(不会读取失败),且二次读取主机会给主机带来负担,容易被针对性攻击。关键读写操作全部走主机,从机仅负责非关键链路的读,该方法是基于保障关键业务的思路。
除了数据同步的问题之外,只要涉及主从机同时支持业务访问的,就一定需要制定请求分配的机制。上面说的几个问题解法也涉及了一些分配机制的细节。具体到分配机制的实现来说,有两种思路:

程序代码封装:实现简单,可对业务定制化,但每个语言都要自己实现一次,且很难做到同步修改,因此适合小团队。中间件封装:独立出一套系统管理读写的分配,对业务透明,兼容 SQL 协议,业务服务器就无需做额外修改适配。需要支持多语言、完整的 SQL 语法,涉及很多细节,容易出 BUG,且本身是个单点,需要特别保障性能和可用性,因此适合大公司。
分库分表

除了高频访问的压力,当数据量大了以后,也会带来数据库存储方面的压力。此时就需要考虑分库分表的问题。分库分表既可以缓解访问的压力,也可以分散存储的压力。

先说分库,所谓分库,就是指业务按照功能、模块、领域等不同,将数据分散存储到不同的数据库实例中。

比如原本是一个 MySQL 数据库实例,在库中按照不同业务建了多张表,大体可以归类为 A、B 两个领域的数据。现在新建一个库,将原库中 A 领域的数据迁移到新的库中存储,还是按需建表,而 B 领域的数据继续留在原库中。

分库一方面可以缓解访问和存储的压力,另一方面也可以增加抗风险能力,当一个库出问题后,另一个库中的数据并不会受到影响,而且还能分开管理权限。

但分库也会带来一些问题:原本同一个库中的不同表可以方便地进行联表查询,分库后则会变得很复杂。由于数据在不同的库中,当要操作两个库中的数据时,无法使用事务操作,一致性也变得更难以保障。而且当增加备库来保障可用性的时候,成本是成倍增加的。

基于以上问题,初创的业务并不建议在一开始就做这种拆分,会增加很多开发时的成本和复杂度,拖慢业务的节奏。

再说分表。所谓分表,就是将原本存储在一张表里的数据,按照不同的维度,拆分成多张表来存储。

按照诉求与业务的特性不同,可以采用垂直分表或水平分表的方式。

垂直分表相当于垂直地给原表切了一刀,把不同的字段拆分到不同的子表中,这样拆分后,原本访问一张表可以获取的所有字段,现在则需要访问不同的表获取。

垂直分表适合将表中某些不常用又占了大量空间的列(字段)拆分出去,可以提升访问常用字段的性能。

但相应的,当真的需要的字段处于不同表中时,或者要新增记录存储所有字段数据时,要操作的表变多了。

水平分表相当于横着给原表切了一刀,那么原表中的记录会被分散存储到不同的子表中,但是每张子表的字段都是全部字段。

水平分表适合表的量级很大以至影响访问性能的场景,何时该拆分并没有绝对的指标,一般记录数超过千万时就需要警觉了。

不同于垂直分表依然能访问到所有记录,水平分表后无法再在一张表中访问所有数据了,因此很多查询操作会受到影响,比如 join 操作就需要多次查询后合并结果,count 操作也需要计算多表的结果后相加,如果经常用到 count 的总数,可以额外维护一个总数表去更新,但也会带来数据一致性的问题。

值得特别提出的是范围查询,原本的一张表可以通过范围查询到的数据,分表后也需要多次查询后合并数据,如果是业务经常用到的范围查询,那建议干脆就按照这种方式来分表,这也是分表的路由方式之一:范围路由。

所谓路由方式是指:分表后当新插入记录时,如何判断该往哪张表插入。常用的插入方式有以下三种:

范围路由:按照时间范围、ID 范围或者其他业务常用范围字段路由。这种方式在扩充新的表时比较方便,直接加表给新范围的数据插入即可,但是数量和冷热分布可能是不均匀的。 Hash 路由:根据 Hash 运算来路由新记录插入的表,这种方式需要提前就规划好分多少张表,才能决定 Hash 运算方式。但表数量其实很难预估,导致未来需要扩充新表时很麻烦,但数据在不同表中的分布是比较均匀的。配置路由:新增一个路由表来记录数据 id 和表 id 的映射,按照自定义的方式随时修改映射规则,设计简单,扩充新表也很方便。但每次操作表都需要额外操作一次路由表,其本身也成为了单点瓶颈

无论是垂直分表还是水平分表,单表切分为多表后,新的表即使在同一个数据库服务器中,也可能带来可观的性能提升,如果性能能够满足业务要求,可以不拆分到多台数据库服务器,毕竟分库也会引入很多复杂性的问题;如果单表拆分为多表后,单台服务器依然无法满足性能要求,那就不得不再次进行业务分库的设计了。

NoSQL 数据库

上面发分库分表讨论的都是关系型数据库的优化方案,但关系型数据库也有其无法规避的缺点,比如无法直接存储某种结构化的数据、扩展表结构时会锁表影响线上性能、大数据场景下 I/O 较高、全文搜索的功能比较弱等。

基于这些缺点,也有很多新的数据库框架被创造出来,解决其某方面的问题。

比如以 Redis 为代表的的 KV 存储,可以解决无法存储结构化数据的问题;以 MongoDB 为代表的的文档数据库可以解决扩展表结构被强 Schema 约束的问题;以 HBase 为代表的的列式数据库可以解决大数据场景下的 I/O 问题;以 ES 为代表的的全文搜索引擎可以解决全文检索效率的问题等。

这些数据库统称为 NoSQL 数据库,但 NoSQL 并不是全都不能写 SQL,而是 Not Only SQL 的意思。

NoSQL 数据库除了聚焦于解决某方面的问题以外也会有其自身的缺点,比如 Redis 没有支持完整的 ACID 事务、列式存储在更新一条记录的多字段时性能较差等。因此并不是说使用了 NoSQL 就能一劳永逸,更多的是按需取用,解决业务面临的问题。

关于 NoSQL 的更多了解,推荐大家可以看看《NoSQL 精粹》这本书。

缓存

如果 NoSQL 也解决不了业务的高性能诉求,那么或许你需要加点缓存

缓存最直接的概念就是把常用的数据存在内存中,当业务请求来查询的时候直接从内存中拿出来,不用重新去数据库中按条件查询,也就省去了大量的磁盘 IO 时间。

一般来说缓存都是通过 Key-Value 的方式存储在内存中,根据存储的位置,分为单机缓存和集中式缓存。单机缓存就是存在自身服务器所在的机器上,那么势必会有不同机器数据可能不一致,或者重复缓存的问题,要解决可以使用查询内容做路由来保障同一记录始终到同一台机器上查询缓存。集中式缓存则是所有服务器都去一个地方查缓存,会增加一些调用时间。

缓存可以提升性能是很好理解的,但缓存同样有着它的问题需要应对或规避。数据时效性是最容易想到的问题,但也可以靠同时更新缓存的策略来保障数据的时效性,除此之外还有其他几个常见的问题。

如果某条数据不存在,缓存中势必查不到对应的 KEY,从而就会请求数据库确认是否有新增加这条数据,如果始终没有这条数据,而客户端又反复频繁地查询这条数据,就会变相地对数据库造成很大的压力,换句话说,缓存失去了保护作用,请求穿透到了数据库,这称为缓存穿透。

应对缓存穿透,最好的手段就是把“空值”这一情况也缓存下来,当客户端下次再查询时,发现缓存中说明了该数据是空值,则不会再问询数据库。但也要注意如果真的有对应数据写入了数据库,应当能及时清除”空值“缓存。

为了保障缓存的数据及时更新,常常都会根据业务特性设置一个缓存过期时间,在缓存过期后,到再次生成期间,如果出现大量的查询,会导致请求都传递到数据库,而且会多次重复生成缓存,甚至可能拖垮整个系统,这就叫缓存雪崩,和缓存穿透的区别在于,穿透是面对空值的情况,而雪崩是由于缓存重新生成的间隔期大量请求产生的连锁效应。

既然是缓存更新时重复生成所导致的问题,那么一种解法就是在缓存重新生成前给这个 KEY 加锁,加锁期间出现的请求都等待或返回默认值,而不去都尝试重新生成缓存。

另一种方法是干脆不要由客户端请求来触发缓存更新,而是由后台脚本统一更新,同样可以规避重复请求导致的重复生成。但是这就失去了只缓存热点数据的能力,如果缓存因空间问题被清除了,也会因为后台没及时更新导致查不到缓存数据,这就会要求更复杂的后台更新策略,比如主动查询缓存有效性、缓存被删后通知后台主动更新等。

虽说在有限的内存空间内最好缓存热点数据,但如果数据过热,比如微博的超级热搜,也会导致缓存服务器压力过大而崩溃,称之为缓存热点问题。

可以复制多份缓存副本,来分散缓存服务器的单机压力,毕竟堆机器是最简单有效。此处也要注意,多个缓存副本不要设置相同的缓存过期时间,否则多处缓存同时过期,并同时更新,也容易引起缓存雪崩,应该设置一个时间范围内的随机值来更新缓存。

2.2 计算高性能

讲完存储高性能,再讲计算高性能,计算性能的优化可以先从单机性能优化开始,多进程、多线程、IO 多路复用、异步 IO 等都存在很多可以优化的地方,但基本系统或框架已经提供了基本的优化能力,只需使用即可。

负载均衡

如果单机的性能优化已经到了瓶颈,无法应对业务的增长,就会开始增加服务器,构建集群。对于计算来说,每一台服务器接到同样的输入,都应该返回同样的输出,当服务器从单台变成多台之后,就会面临请求来了要由哪一台服务器处理的问题,我们当然希望由当前比较空闲的服务器去处理新的请求,这里对请求任务的处理分配问题,就叫负载均衡。

负载均衡的策略,从分类上来说,可以分为三类:

DNS 负载均衡:通过 DNS 解析,来实现地理级别的均衡,其成本低,分配策略很简单,可以就近访问来提升访问速度,但 DNS 的缓存时间长,由于更新不及时所以无法快速调整,且控制权在各域名商下,且无法根据后端服务器的状态来决定分配策略。 硬件负载均衡:直接通过硬件设备来实现负载均衡,类似路由器路由,功能和性能都很强大,可以做到百万并发,也很稳定,支持安全防护能力,但是同样无法根据后端服务器状态进行策略调整,且价格昂贵。软件负载均衡:通过软件逻辑实现,比如 nginx,比较灵活,成本低,但是性能一般,功能也不如硬件强大。

一般来说,DNS 负载均衡用于实现地理级别的负载均衡;硬件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡。

所以部署起来可以按照这三层去部署,第一层通过 DNS 将请求分发到北京、上海、深圳的机房;第二层通过硬件负载均衡将请求分发到当地三个集群中的一个;第三层通过软件策略将请求分发到具体的某台服务器去响应业务。

就负载均衡算法来说,多是我们很熟悉的算法,如轮询、加权轮询、负载最低优先、性能最优优先、Hash 分配等,各有特点,按需采用即可。

03、高可用架构模式

3.1 理论方式

CAP 与 BASE
在说高可用之前,先来说说 CAP 理论,即:

在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。

大家可能都知道 CAP 定理是什么,但大家可能不知道,CAP 定理的作者(Seth Gilbert & Nancy Lynch)其实并没有详细解释 CAP 三个单词的具体含义,目前大家熟悉的解释其实是另一个人(Robert Greiner)给出的。而且他还给出了两版有所差异的解释。

书中第二版解释算是对第一版解释的加强,他要加强的点主要是:

CAP 描述的分布式系统,是互相连结并共享数据的节点的集合。因为其实并不是所有的分布式系统都会互连和共享数据。CAP 理论是在涉及读写操作的场景下的理论,而不是分布式系统的所有功能。一致性只需要保障客户端读操作能读到最新的写操作结果,并不要求时时刻刻分布式系统的数据都是一致的,这是不现实的,只要保障客户读到的一致即可。可用性要求非故障的节点在合理的时间内能返回合理的响应,所谓合理是指非错误、非超时,即使数据不是最新的数据,也是合理的“旧数据”,是符合可用性的。分区容错性要求网络分区后系统能继续履行职责,不仅仅要求系统不宕机,还要求能发挥作用,能处理业务逻辑。比如接口直接返回错误其实也代表系统在运行,但却没有履行职责。
在分布式系统下,P(分区容忍)是必须选择的,否则当分区后系统无法履行职责时,为了保障 C(一致性),就要拒绝写入数据,也就是不可用了

在此基础上,其实我们能选择的只有 C+P 或者 A+P,根据业务特性来选择要优先保障一致性还是可用性。

在选择保障策略时,有几个需要注意的点:

CAP 关注的其实是数据的粒度,而不是整个系统的粒度,因此对于系统内的不同数据(对应不同子业务),其实是可以按照业务特性采取不同的 CAP 策略的。CAP 实际忽略了网络延迟,也就是允许数据复制过程中的短时间不一致,如果某些业务比如金融业务无法容忍这一点,那就只能对单个对象做单点写入,其他节点备份,无法做多点写入。但对于不同的对象,其实可以分库来实现分布式。当没有发生分区现象时,也就是不用考虑 P 时,上述限制就不存在,此时应该考虑如何保障 CA。当发生分区后,牺牲 CAP 的其中一个并不代表什么都不用做,而是应该为分区后的恢复 CA 做准备,比如记录分区期间的日志以供恢复时使用。

随 CAP 的一个退而求其次,也更现实的追求,是 BASE 理论,即基本可用,保障核心业务的可用性;软状态,允许系统存在数据不一致的中间状态;最终一致性,一段时间后系统应该达到一致。

FMEA 分析法

要保障高可用,我们该怎么下手呢?俗话说知己知彼才能有的放矢,因此做高可用的前提是了解系统存在怎样的风险,并且还要识别出风险的优先级,先治理更可能发生的、影响更大的风险。说得简单,到底怎么做?业界其实已经提供了排查系统风险的基本方法论,即 FMEA(Failure mode and effects analysis)——故障模式与影响分析。

FMEA 的基本思路是,面对初始的架构设计图,考虑假设其中某个部件发生故障,对系统会造成什么影响,进而判断架构是否需要优化。

具体来说,需要画一张表,按照如下步骤逐个列出:

功能点:列出业务流程中的每个功能点。故障模式:量化描述该功能可能发生怎样的故障,比如 MySQL 响应时间超过3秒。故障影响:量化描述该每个故障可能导致的影响,但不用非常精确,比如20%用户无法登录。严重程度:设定标准,给每个影响的严重程度打分。故障原因:对于每个故障,考虑有哪些原因导致该故障。故障概率:对于每个原因,考虑其发生的概率,不用精确,分档打分即可。风险程度:=严重程度 * 故障概率,据此就可以算出风险的处理优先级了,肯定是程度分数越高的越应该优先解决。已有措施、解决措施、后续规划:用于梳理现状,思考未来的改进方案等。
基于上面这套方法论,我们可以有效地对系统的风险进行梳理,找出需要优先解决的风险点,从而提高系统的可用性。

除了 FMEA,其实还有一种应用更广泛的风险分析和治理的理论,即 BCP——业务连续性计划,它是一套基于业务规律的规章流程,保障业务或组织在面对突发状况时其关键业务功能可以持续不中断。

相比 FMEA,BCP 除了评估风险及重要程度,还要求详细地描述应对方案、残余风险、灾备恢复方案,并要求进行相应故障的培训和演习安排,尽最大努力保障业务连续性。

知道风险在哪、优先治理何种风险之后,就可以着手优化架构。和高性能架构模式一样,高可用架构也可以从存储和计算两个方面来分析。

3.2 存储高可用

存储高可用的本质都是通过将数据复制到多个存储设备,通过数据冗余的方式来提高可用性。

双机架构

让我们先从简单的增加一台机器开始,即双机架构。

当机器变成两台后,根据两台机器担任的角色不同,就会分成不同的策略,比如主备、主从、主主。

主备复制的架构是指一台机器作为客户端访问的主机,另一台机器纯粹作为冗余备份用,当主机没有故障时,备机不会被客户端访问到,仅仅需要从主机同步数据。这种策略很简单,可以应对主机故障情况下的业务可用性问题,但在平常无法分担主机的读写压力,有点浪费。

主从复制的架构和主备复制的差别在于,从机除了复制备份数据,还需要干活,即还需要承担一部分的客户端请求(一般是分担读操作)。当主机故障时,从机的读操作不会受到影响,但需要增加读操作的请求分发策略,且和主备不同,由于从机直接提供数据读,如果主从复制延迟大,数据不一致会对业务造成更明显的影响。

对于主备和主从两种策略,如果主机故障,都需要让另一台机器变成主机,才能继续完整地提供服务,如果全靠人工干预来切换,会比较滞后和易错,最好是能够自动完成切换,这就涉及双机切换的策略。

在考虑双机切换时,要考虑什么?首先是需要感知机器的状态,是两台机器直连传递互相的状态,还是都传递给第三方来仲裁?所谓状态要包含哪些内容才能定义一台主机是故障呢?是发现一次问题就切换还是多观察一会再切换?切换后如果主机恢复了是切换回来还是自动变备机呢?需不需要人工二次确认一下

这些问题可能都得根据业务的特性来得出答案,此处仅给出三种常见的双机切换模式:

互连式:两台机器直接连接传递信息,并根据传递的状态信息判断是否要切换主机,如果通道本身发生故障则无法判断是否要切换了,可以再增加一个通道构成双通道保障,不过也只是降低同时故障的概率。中介式:通过第三方中介来收集机器状态并执行策略,如果通道发生断连,中介可以直接切换其他机器作为主机,但这要求中介本身是高可用的,已经有比较成熟的开源解决方案如 zookeeper、keepalived。模拟式:备机模拟成客户端,向主机发送业务类似的读写请求,根据响应情况来判断主机的状态决定是否要切换主机,这样可以最真实地感受到客户端角度下的主机故障,但和互连式不同,能获取到的其他机器信息很少,容易出现判断偏差。
最后一种双机架构是主主复制,和前面两种只有一主的策略不同,这次两台都是主机,客户端的请求可以达到任何一台主机,不存在切换主机的问题。但这对数据的设计就有了严格的要求,如果存在唯一 ID、严格的库存数量等数据,就无法适用,这种策略适合那些偏临时性、可丢失、可覆盖的数据场景。

数据集群

采用双机架构的前提是一台主机能够存储所有的业务数据并处理所有的业务请求,但机器的存储和处理能力是有上限的,在大数据场景下就需要多台服务器来构成数据集群。

如果是因为处理能力达到瓶颈,此时可以增加从机帮主机分担压力,即一主多从,称为数据集中集群。这种集群方式需要任务分配算法将请求分散到不同机器上去,主要的问题在于数据需要复制到多台从机,数据的一致性保障会比一主一从更为复杂。且当主机故障时,多台从机协商新主机的策略也会变得复杂。这里有开源的 zookeeper ZAB 算法可以直接参考。

如果是因为存储量级达到瓶颈,此时可以将数据分散存储到不同服务器,每台服务器负责存储一部分数据,同时也备份一部分数据,称为数据分散集群。数据分散集群同样需要做负载均衡,在数据分区的分配上,hadoop 采用独立服务器负责数据分区的分配,ES 集群通过选举一台服务器来做数据分区的分配。除了负载均衡,还需要支持扩缩容,此外由于数据是分散存储的,当部分服务器故障时,要能够将故障服务器的数据在其他服务器上恢复,并把原本分配到故障服务器的数据分配到其他正常的服务器上,即分区容错性。

数据分区 

数据集群可以在单台乃至多台服务器故障时依然保持业务可用,但如果因为地理级灾难导致整个集群都故障了(断网、火灾等),那整个服务就不可用了。面对这种情况,就需要基于不同地理位置做数据分区。

做不同地理位置的数据分区,首先要根据业务特性制定分区规则,大多还是按照地理位置提供的服务去做数据分区,比如中国区主要存储中国用户的数据。

既然分区是为了防灾,那么一个分区肯定不止存储自身的数据,还需要做数据备份。从数据备份的策略来说,主要有三种模式:

集中式:存在一个总备份中心,所有的分区数据都往这个总中心备份,设计起来简单,各个分区间没有联系,不会互相影响,也很容易扩展新的分区。但总中心的成本较高,而且总中心如果出故障,就要全部重新备份。互备式:每个分区备份另一个分区的数据,可以形成一个备份环,或者按地理位置远近来搭对备份,这样可以直接利用已有的设备做数据备份。但设计较复杂,各个分区间需要联系,当扩展新分区时,需要修改原有的备份线路。独立式:每个分区配备自己的备份中心,一般设立在分区地理位置附近的城市,设计也简单,各个分区间不会影响,扩展新分区也容易。但是成本会很高,而且只能防范城市级的灾难。

3.3 计算高可用

从存储高可用的思路可以看出,高可用主要是通过增加机器冗余来实现备份,对计算高可用来说也是如此。通过增加机器,分担服务器的压力,并在单机发生故障的时候将请求分配到其他机器来保障业务可用性。

因此计算高可用的复杂性也主要是在多机器下任务分配的问题,比如当任务来临(比如客户端请求到来)时,如何选择执行任务的服务器?如果任务执行失败,如何重新分配呢?这里又可以回到前文说过的负载均衡相关的解法上。

计算服务器和存储服务器在多机器情况下的架构是类似的,也分为主备、主从和集群。

主备架构下,备机仅仅用作冗余,平常不会接收到客户端请求,当主机故障时,备机才会升级为主机提供服务。备机分为冷备和温备。冷备是指备机只准备好程序包和配置文件,但实际平常并不会启动系统。温备是指备机的系统是持续启动的,只是不对外提供服务,从而可以随时切换主机。

主从架构下,从机也要执行任务,由任务分配器按照预先定义的规则将任务分配给主机和从机。相比起主备,主从可以发挥一定的从机性能,避免成本空费,但任务的分配就变得复杂一些。

集群架构又分为对称集群和非对称集群。

对称集群也叫负载均衡集群,其中所有的服务器都是同等对待的,任务会均衡地分配到每台服务器。此时可以采用随机、轮询、Hash 等简单的分配机制,如果某台服务器故障,不再给它分配任务即可。

非对称集群下不同的服务器有不同的角色,比如分为 master 和 slave。此时任务分配器需要有一定的规则将任务分配给不同角色的服务器,还需要有选举策略来在 master 故障时选择新的 master。这个选举策略的复杂度就丰俭由人了。

异地多活

讲存储高可用已经说过数据分区,计算高可用也有类似的高可用保障思路,归纳来说,它们都可以根据需要做异地多活,来提高整体的处理能力,并防范地区级的灾难。异地多活中的”异地“,就是指集群部署到不同的地理位置,“活”则强调集群是随时能提供服务的,不同于“备”还需要一个切换过程。

按照规模,异地多活可以分为同城异区、跨城异地和跨国异地。显而易见,不同模式下能够应对的地区级故障是越来越高的,但同样的,距离越远,通信成本与延迟就越高,对通信通道可用性的挑战也越高。因此跨城异地已经不适合对数据一致性要求非常高的业务,而跨国异地往往是用来给不同国家的用户提供不同服务的。

由于异地多活需要花费很高的成本,极大地增加系统复杂度,因此在设计异地多活架构时,可以不用强求为所有业务都做异地多活,可以优先为核心业务实现异地多活。尽量保障绝大部分用户的异地多活,对于没能保障的用户,通过挂公告、事后补偿、完善失败提示等措施进行安抚、提升体验。毕竟要做到100%可用性是不可能的,只能在能接受的成本下尽量逼近,所以当可用性达到一定瓶颈后,补偿手段的成本或许更低。

在异地部署的情况下,数据一定会冗余存储,物理上就无法实现绝对的实时同步,且距离越远对数据一致性的挑战越大,虽然可以靠减少距离、搭建高速专用网络等方式来提高一致性,但也只是提高而已,因此大部分情况下, 只需考虑保障业务能接受范围下的最终一致性即可。

在同步数据的时候,可以采用多种方式,比如通过消息队列同步、利用数据库自带的同步机制同步、通过换机房重试来解决同步延迟问题、通过 session id 让同一数据的请求都到同一机房从而不用同步等。

可见,整个异地多活的设计步骤首先是对业务分级,挑选出核心业务做异地多活,然后对需要做异地多活的数据进行特征分析,考虑数据量、唯一性、实时性要求、可丢失性、可恢复性等,根据数据特性设计数据同步的方案。最后考虑各种异常情况下的处理手段,比如多通道同步、日志记录恢复、用户补偿等,此时可以借用前文所说的 FMEA 等方法进行分析。

接口级故障

前面讨论的都是较为宏观的服务器、分区级的故障发生时该怎么办,实际上在平常的开发中,还应该防微杜渐,从接口粒度的角度,来防范和应对接口级的故障。应对的核心思路依然是优先保障核心业务和绝大部分用户可用。

对于接口级故障,有几个常用的方法:限流、排队、降级、熔断。其中限流和排队属于事前防范的措施,而降级和熔断属于接口真的故障后的处理手段。

限流的目的在于控制接口的访问量,避免被高频访问冲垮。

从限流维度来说,可以基于请求限流,即限制某个指标下、某个时间段内的请求数量,阈值的定义需要基于压测和线上情况来逐步调优。还可以基于资源限流,比如根据连接数、文件句柄、线程数等,这种维度更适合特殊的业务。

实现限流常用的有时间窗算法和桶算法。

时间窗算法分为固定时间窗和滑动时间窗。

固定时间窗通过统计固定时间周期内的量级来决定限流,但存在一个临界点的问题,如果在两个时间窗的中间发生超大流量,而在两个时间窗内都各自没有超出限制,就会出现无法被限流拦截的接口故障。因此滑动时间窗采用了部分重叠的时间统计周期来解决临界点问题。

桶算法分为漏桶和令牌桶。

漏桶算法是将请求放入桶中,处理单元从桶里拿请求去进行处理,如果桶堆满了就丢弃掉新的请求,可以理解为桶下面有个漏斗将请求往处理单元流动,整个桶的容量是有限的。这种模式下流入的速率取决于请求的频率,当桶内有堆积的待处理请求时,流出速率是匀速的。漏桶算法适用于瞬时高并发的场景(如秒杀),处理可能慢一点,但可以缓存部分请求不丢弃。

令牌桶算法是在桶内放令牌,令牌数是有限的,新的请求需要先到桶里拿到令牌才能被处理,拿不到就会被丢弃。和漏桶匀速流出处理不同,令牌桶还能通过控制放令牌的速率来控制接收新请求的频率,对于突发流量,可靠累计的令牌来处理,但是相对的处理速度也会突增。令牌桶算法适用于控制第三方服务访问速度的场景,防止压垮下游。

除了限流,还有一种控制处理速度的方法就是排队。当新请求到来后先加入队列,出队端通过固定速度出队处理请求,避免处理单元压力过大。队列也有长度限制,其机制和漏桶算法差不多。

如果真的事前防范真的被突破了,接口很可能或已经发生了故障,还能做什么呢?

一种手段是熔断,即当处理量达到阈值,就主动停掉外部接口的访问能力,这其实也是一种防范措施,对外的表现虽然是接口访问故障,但系统内部得以被保护,不会引起更大的问题,待存量处理被消化完,或者外部请求减弱,或完成扩容后,再开放接口。熔断的设计主要是阈值,需要按照业务特点和统计数据制定。

当接口故障后(无论是被动还是主动断开),最好能提供降级策略。降级是丢车保帅,放弃一下非核心业务,保障核心业务可用,或者最低程度能提供故障公告,让用户不要反复尝试请求来加重问题了。比起手动降级,更好的做法也是自动降级,需要具备检测和发现降级时机的机制。

04、可扩展架构模式

再回顾一遍互联网行业的金科玉律:只有变化才是不变的。在设计架构时,一开始就要抱着业务随时可能变动导致架构也要跟着变动的思想准备去设计,差别只在于变化的快慢而已。因此在设计架构时一定是要考虑可扩展性的。

在思考怎样才是可扩展的时候,先想一想平常开发中什么情况下会觉得扩展性不好?大都是因为系统庞大、耦合严重、牵一发而动全身。因此对可扩展架构设计来说,基本的思想就是拆分。

拆分也有多种指导思想,如果面向业务流程来谈拆分,就是分层架构;如果面向系统服务来谈拆分,就是 SOA、微服务架构;如果面向系统功能来拆分,就是微内核架构。

分层架构

分层架构是我们最熟悉的,因为互联网业务下,已经很少有纯单机的服务,因此至少都是 C/S 架构、B/S 架构,也就是至少也分为了客户端/浏览器和后台服务器这两层。如果进一步拆分,就会将后台服务基于职责进行自顶向下的划分,比如分为接入层、应用层、逻辑层、领域层等。

分层的目的当然是为了让各个层次间的服务减少耦合,方便进行各自范畴下的优化,因此需要保证各层级间的差异是足够清晰、边界足够明显的,否则当要增加新功能的时候就会不知道该放到哪一层。各个层只处理本层逻辑,隔离关注点。

额外需注意的是一旦确定了分层,请求就必须层层传递,不能跳层,这是为了避免架构混乱,增加维护和扩展的复杂度,比如为了方便直接跨层从接入层调用领域层查询数据,当需要进行统一的逻辑处理时,就无法切面处理所有请求了。

SOA 架构

SOA 架构更多出现在传统企业中,其主要解决的问题是企业中 IT 建设重复且效率低下,各部门自行接入独立的 IT 系统,彼此之间架构、协议都不同,为了让各个系统的服务能够协调工作,SOA 架构应运而生。

其有三个关键概念:服务、ESB 和松耦合。

服务是指各个业务功能,比如原本各部门原本的系统提供的服务,可大可小。由于各服务之间无法直接通信,因此需要 ESB,即企业服务总线进行对接,它将不同服务连接在一起,屏蔽各个服务的不同接口标准,类似计算机中的总线。松耦合是指各个服务的依赖需要尽量少,否则某个服务升级后整个系统无法使用就麻烦了。

这里也可以看出,ESB 作为总线,为了对接各个服务,要处理各种不同的协议,其协议转换耗费了大量的性能,会成为整个系统的瓶颈。

微服务

微服务是近几年最耳熟能详的架构,其实它和 SOA 有一些相同之处,比如都是将各个服务拆分开来提供能力。但是和 SOA 也有一些本质的区别,微服务是没有 ESB 的,其通信协议是一致的,因此通信管道仅仅做消息的传递,不理解内容和格式,也就没有 ESB 的问题。而且为了快速交付、迭代,其服务的粒度会划分地更细,对自动化部署能力也就要求更高,否则部署成本太大,达不到轻量快速的目的。

当然微服务虽然很火,但也不是解决所有问题的银弹,它也会有一些问题存在。如果服务划分的太细,那么互相之间的依赖关系就会变得特别复杂,服务数量、接口量、部署量过多,团队的效率可能大降,如果没有自动化支撑,交付效率会很低。由于调用链太长(多个服务),因此性能也会下降,问题定位会更困难,如果没有服务治理的能力,管理起来会很混乱,不知道每个服务的情况如何。

因此如何拆分服务就成了每个使用微服务架构的团队的重要考量点。这里也提供一些拆分的思路:

三个火枪手原则:考虑每三个人负责一个服务,互相可以形成稳定的人员备份,讨论起来也更容易得出结论,在此基础上考虑能负责多大的一个服务。基于业务逻辑拆分:最直观的就是按逻辑拆分,如果职责不清,就参考三个火枪手原则确定服务大小。基于稳定性拆分:按照服务的稳定性分为稳定服务和变动服务,稳定服务粒度可以粗一些,变动服务粒度可以细一些,目的是减少变动服务之间的影响,但总体数量依然要控制。基于可靠性拆分:按照可靠性排序,要求高的可以拆细一些,由前文可知,服务越简单,高可用方案就会越简单,成本也会越低。优先保障核心服务的高可用。基于性能拆分:类似可靠性,性能要求越高的,拆出来单独做高性能优化,可有效降低成本。
微服务架构如果没有完善的基础设施保障服务治理,那么也会带来很多问题,降低效率,因此根据团队和业务的规模,可以按以下优先级进行基础设施的支持:

优先支持服务发现、服务路由、服务容错(重试、流控、隔离),这些是微服务的基础。接着支持接口框架(统一的协议格式与规范)、API 网关(接入鉴权、权限控制、传输加密、请求路由等),可以提高开发效率。然后支持自动化部署、自动化测试能力,并搭建配置中心,可以提升测试和运维的效率。最后支持服务监控、服务跟踪、服务安全(接入安全、数据安全、传输安全、配置化安全策略等)的能力,可以进一步提高运维效率。
微内核架构
最后说说微内核架构,也叫插件化架构,顾名思义,是面向功能拆分的,通常包含核心系统和插件模块。在微内核架构中,核心系统需要支持插件的管理和链接,即如何加载插件,何时加载插件,插件如何新增和操作,插件如何和核心引擎通信等。

举一个最常见的微内核架构的例子——规则引擎,在这个架构中,引擎是内核,负责解析规则,并将输入通过规则处理后得到输出。而各种规则则是插件,通常根据各种业务场景进行配置,存储到数据库中。

05、总结

人们通常把某项互联网业务的发展分为四个时期:初创期、发展期、竞争期和成熟期。

在初创期通常求快,系统能买就买,能用开源就用开源,能用的就是好的,先要活下来;到了发展期开始堆功能和优化,要求能快速实现需求,并有余力对一些系统的问题进行优化,当优化到顶的时候就需要从架构层面来拆分优化了;进入竞争期后,经过发展期的快速迭代,可能会存在很多重复造轮子和混乱的交互,此时就需要通过平台化、服务化来解决一些公共的问题;最后到达成熟期后,主要在于补齐短板,优化弱项,保障系统的稳定。

在整个发展的过程中,同一个功能的前后要求也是不同的,随着用户规模的增加,性能会越来越难保障,可用性问题的影响也会越来越大,因此复杂度就来了。

对于架构师来说,首要的任务是从当前系统的一大堆纷繁复杂的问题中识别出真正要通过架构重构来解决的问题,集中力量快速突破,但整体来说,要徐徐图之,不要想着用重构来一次性解决所有问题。

对项目中的问题做好分类,划分优先级,先易后难,才更容易通过较少的资源占用,较快地得到成果,提高士气。然后再循序渐进,每个阶段控制在 1~3 个月,稳步推进。

当然,在这个过程中,免不了和上下游团队沟通协作,需要注意的是自己的目标和其他团队的目标可能是不同的,需要对重构的价值进行换位思考,让双方都可以合作共赢,才能借力前进。

还是回到开头的那句话,架构设计的主要目的是为了解决软件系统复杂度带来的问题。首先找到主要矛盾在哪,做到有的放矢,然后再结合知识、经验进行设计,去解决面前的问题。

祝各位开发者都成为一名合格的架构师。以上便是本次分享的全部内容,如果觉得内容有用,欢迎转发分享。

-End-

原创作者|Cloudox


作者:腾讯云开发者
链接:https://juejin.cn/post/7251779626682023994
来源:稀土掘金

收起阅读 »

《Thinking In Java》作者:不要使用并发!

前言 今天纯粹就是带你们来读读书的~ 最近除了工作,特地买回了自己很喜欢的作者新发售的一本书《On Java》,作者是我的老朋友布鲁斯·埃克尔,在Java领域很有名,你可能没听过他的名字,但极有可能听过他的另一本书《Thinking In Java》,我想很...
继续阅读 »

前言



今天纯粹就是带你们来读读书的~


最近除了工作,特地买回了自己很喜欢的作者新发售的一本书《On Java》,作者是我的老朋友布鲁斯·埃克尔,在Java领域很有名,你可能没听过他的名字,但极有可能听过他的另一本书《Thinking In Java》,我想很多Java工程师都读过这本书,可以说是Java编程思想的良心之作。


虽然布鲁斯是我的老朋友,但我不得不吐槽一下,大概通读了一遍《On Java》之后,我心里大体认为是不如《Thinking In Java》的,可能和写小说一样,读者的要求高了,而作者的年纪大了。


我认识布鲁斯很多年了,他是个比较幽默风趣的人,经常在书中直言不讳某编程语言的垃圾之处,同时又对该语言的未来做一点展望,算是一个很中肯且典型的直男程序猿。


最后说一点,我认识他,他不认识我。





正文



我着重看了自己比较感兴趣的并发编程这一块,想知道这位大佬对于目前Java并发编程是否有新的看法和意见,不出我所料,他没讲什么重要的东西,但是好像又讲了,带着吐槽批判式的口吻,陈列了他喜欢和讨厌Java并发编程的地方。


所以我把一些我觉得有意思的地方画出来,分享给大家,看看一个资深Java大佬对并发编程的理解。



1、大佬的并发定律


111.png



其实看到作者研究出的这4条定律时,我还是挺意外的,第一句就点题了,不要使用并发。


仔细想想好像也对……再琢磨一下咦有感觉……最后回忆一下这些年参与的项目……哇擦好有道理!


接下来3条基本算是总纲了,后面的内容都是对这几条的说明。





2、你已埋下的隐患


222.png



这里就是对2、3条的具体说明了,有些话我觉得略显啰嗦,我把对于程序员来讲比较重要的一句话画出来了。


你很容易写出一个看起来运行正常但实际上有问题的并发程序。


看到这句话的时候是不是已经开始默默打开自己的IDEA了,然后审视了一遍自己提交的代码?


别看了,你埋的炸弹还少么,能看出花来吗。


看清楚作者后面那句:你这个问题只有满足最罕见的条件时,才会将自己暴露出来。


我可以这么说,在座绝大部分同行去了下一家公司干活,可能上一家公司的新同事才会在你毫不知情的时候默默踩到你埋的地雷然后被炸个粉碎,而你在新公司也正在踩别人的雷,出来混都是要还的。





3、别否认你就是这种人


333.png



看到这里的时候,我忍不住亲了布鲁斯一口,他痛快的描述出了我一直以来在工作中说不清道不明的烦躁,因为你总会遇到这样的人,同时很难发现自己到底是不是这样的人。


我在工作前3年其实如履薄冰,感觉自己什么都学了,但去了公司发现什么都不会,怀揣着自我否定一点点完成别人布置的任务,直到工作5年以后才有一种醍醐灌顶的感觉,理解了自己做的是什么,接下来要学习哪个方向,以前学到那么多东西究竟是怎么串联起来的,这是一种打通任督二脉的满足感。


等到工作8年之后,才真正开始回头看Java语言,对以前烦厌欲呕的Java基础提起莫名的兴趣,同时喜欢看书,写案例,尝试阅读别人的源码等等,此时我才真正有自己一只腿迈进Java领域的意识。


同时,在工作中会对许多能力一般但沟通较为偏执的同事产生抵触情绪,我有时会认为这是一种大人看小孩耍脾气的感觉,这个只有在工作多年之后才会产生,作者很准确的阐述出了我描绘不出的这种解释。


同样的,我认为在这个成长的过程中,我一定也成为过别人心中眼高手低的人。


我在这里能分享给大家的经验就是,在工作中多学习少争论,多和厉害的人走近一点,虚心把对方的东西都学过来,长此以往你会进步神速,这不是你在网上学习能得到的,一定是在工作中。





4、高级Javaer都有过的想法


444.png



这里我为什么专门画出来,因为很多高级javaer一定有过类似的想法,就是发现了Java并不擅长做并发编程,是否可以用其他语言来完成,而Java只做他自己擅长的事。


至少我以前就想过,可现实层面我认为是异想天开的,尤其是工作中,基本都是团队开发,这种想法就已经几乎被pass掉了,同时为了某一个领域的实现专门引入一门编程语言甚至体系,得不偿失,毕竟Java不擅长但却成熟,光是网上卖课郎告诉你的就有N种诸如《Java千亿级高并发解决方案》、《Java万亿级电商实战》等等这样的受用终生的鬼东西。


而你辛辛苦苦跟着学完后,发现玛德用不上,就像你学了《九阴真经》后以为可以当武林盟主最终却进了铁匠铺,而铁匠铺老板还不想听你鬼扯只想让你每天加班多打几把武器。


图片中我还画了个圈,我想不少人应该知道这门语言,还蛮有名的,就是国内不太火,这有编程历史因素在里面,其实还有一门语言也蛮适合的,而且这几年也挺火,我想你也猜到了,我觉得5年+的Java工程师都应该关注甚至学习一下。





5、我和大佬不谋而合


555.png



这是接近尾声的部分了,也是这位作者熟悉的笔法,发泄完自己的情绪后又开始对Java的某新版本极尽赞美,典型的被PUA了。


但不得不说,Java8我也认为是革命性的版本,在这个版本发布以前,作为Java工程师你甚至不会想到它敢做到这个地步,就像布鲁斯书中讲的,这是史诗般的魔法。


你可以在Java8的版本里发现一些其他语言的影子,这没什么,天下语言一大抄,发展到一定程度,已经是避免不了的趋势了。


重要的是,这个版本给Java上油了,为后续的版本提供了活力,而Java17作为官方长久支持版本的其中一个非常重要的版本,你可以发现有其他框架给它背书,比如SpringBoot3只支持Java17,而Jenkins也宣布在新版本放弃Java8并且该团队更推荐Java17,IDEA后续新版本可能也会放弃Java8,这明显就是小圈子,有利益的勾连,但对Java本身发展不是坏事。


所以,Java8的核心技术点最应该学习,如果现在还一点不会,赶紧学吧,我认为这是后续版本的基础了,lambada表达式、stream流不必说了,是Java8版本的核心技术,CompletableFuture作为Java8并发编程中最大的改进要花时间好好学习,这也是本书作者所提到的,而且后面专门花了一个大章来讲CompletableFuture。


作者虽然一直强调不要使用并发,但却对Java8的并发编程工具花了较大篇幅,我个人认为他更多的是一种见猎心喜,可是我们面试经常会问到这个工具类相关的东西,看一下大佬对该工具的理解还是很有用的。





总结



《On Java》这本书说实话,我觉得没有作者的《Thinking In Java》写得好,可能有多种原因导致。


我说下我觉得不好的主要感受在哪里,一是有些地方翻译的不好,会给你带来困惑,二是作者给出的一些案例有自己的风格,而且例子我没觉得那么通俗易懂。


但总体上还是值得一看,尤其是他穿插了很多和其他如C/C++、GO等语言的比较,还包含了自己对Java的理解,尤其是一些编程思想很直接,最后给出了林林总总有接近70条的编程指南,我认为对于初学者树立未来工作中的编程思想是很有用的。


这位作者的文字中弥漫着一股浓烈的不推荐使用并发编程的味道,我觉得是他多年工作的心得,所以大家在往后的工作中不妨可以借鉴下大佬的思维。


好了,我今天也就是带你读了下书,读的还开心吗。







本人原创文章纯手打,觉得有一滴滴帮助的话就请点个赞和收藏吧~


本人长期分享工作中的感悟、经验及实用案例,喜欢的话也可以进入个人主页关注一下哦~


作者:程序员济癫
来源:juejin.cn/post/7147523943321042980
收起阅读 »

个人支付项目,已稳定收款 100+

对,没错,又趁着周末两天 + 几个工作日晚上熬夜开发了个支付项目出来,赞赏平台。我对这个项目的定位非常简单,就是一个买卖平台。平台内容由我来发布,免费内容大家只需注册即可观看,如需付费则支付相关费用方可查看。下面是项目运行首页下面是项目登录注册页下面是商品支付...
继续阅读 »

对,没错,又趁着周末两天 + 几个工作日晚上熬夜开发了个支付项目出来,赞赏平台。

我对这个项目的定位非常简单,就是一个买卖平台。平台内容由我来发布,免费内容大家只需注册即可观看,如需付费则支付相关费用方可查看。

下面是项目运行首页


下面是项目登录注册页


下面是商品支付页面


虽然项目整体规模较小但也算是五脏俱全,有认证相关、有支付相关、也有分布式问题相关。对于没有做过个人项目特别是没做过支付项目的小伙伴来说,用来练手或者写在简历上都是未尝不可的。


那下面我来具体项目中几个重要的业务点。


1、网关认证


以前我们开发项目要进行认证基本都是通过在服务中写个拦截器,然后配置拦截器拦截所有的请求,最终通过拦截器的逻辑进行认证。这中方法不是不可以,但我觉得不好,如果我们项目中有三个微服务以上,那么这个拦截器的认证逻辑就会存在于每个微服务中,这是我认为的不好的点。


那我是怎么做的呢!


对,在网关服务里做认证动作。将认证动作迁移,因为我的个人项目是通过网关进行请求转发,所以,所有的请求都会先进入网关,再进入各个具体的业务服务,那问题就好办了。我直接通过实现网关的 GlobalFilter 接口拦截所有的请求,通过实现该接口进行认证逻辑处理,完成本平台的认证、续约、限流等功能。


下面来看看请求流程图


2、支付逻辑


支付功能可以说是本项目的重中之重,需要有非常强的健壮性。因为我是一位个人开发者,所以不能对接需要有营业执照的支付功能,最终我选择了支付宝的当面付这一个功能。


当面付的好处很多,第一它不需要营业执照,第二对接也非常简单而且有支付宝封装的SDK,所以本人再对接的过程中没有费多少力气就把接口打通了。


主要就是对接了当面付的三个接口:

  1. 获取支付二维码接口

  2. 支付成功的回调接口

  3. 订单状态回查接口


当然,这三个接口的代码量也是很大的,所以本人为了通用就又对他做了一层封装,使得项目调用支付功能就更加简单了。如下就可以完成一个支付功能的完整逻辑:


是不是很简单,如果需要代码的可以看文章末尾哦!


下面我来介绍一下本项目中付费内容的整个业务流程。


1、用户获取付费商品详情


2、点击查看内容,这里就有两种结果了

  • 第一种结果:商品已支付,直接显示内容给用户观看

  • 第二种结果:商品未支付,提示用户付款查看


3、当显示第二种结果时,如果用户点击付款,则进入后续流程


4、服务器请求支付宝第三方,获取对应金额的支付二维码,并将返回的二维码和用户绑定生成一个未支付的订单,最终将这个待支付二维码返回给页面


5、页面显示二维码之后,用户就需要进行扫码付款(打开支付宝APP扫码付款)


6、用户付款成功之后,支付宝第三方会自动回调第四步我给支付宝的回调地址。回调接口的逻辑就是将订单状态改为已支付并做一些后续的流程操作。


7、为了防止回调接口出问题,还写了一个定时任务,定时回查订单表中未支付订单的状态,循环请求支付宝询问支付支付成功并执行支付成功的相应回调逻辑。


支付业务流程图


3、手写分布式锁


相信分布式锁大家都不陌生,无非就是向中间件中放入一个标志量,存在即表示已锁,反之则未锁执行相关逻辑。


说都会说,但要真正自己手写而且做到高可用确是一个非常困难的问题。其中非常关键的一点就是如何解锁,如何做到业务执行完成百分之百解锁,那我再项目中是如何考虑的呢!


我先来简单的说一下思路:


1、定义一个分布式锁注解,用来标注那些方法需要分布式锁加持


2、定义一个切面,逻辑就是给加上了分布式注解的方法进行增强


3、增强的逻辑为:加锁、生成续约任务、执行业务逻辑、解锁


4、另起一个延迟线程池,每隔一定时间遍历一次续约任务集合,判断任务是否需要进行续约(这个逻辑判断很多如:续约次数过多、业务已执行完毕、是否需要续约等等)


具体业务流程如图(我画的比较多)


当然,为了方便你们理解,我还出了相关视频,地址:



http://www.bilibili.com/video/BV1jP…



以上,就是赞赏平台项目中三个比较大的亮点,不论是写在简历上还是当作个人项目都是一个非常不错的选择,那我也把这个项目搭建起来了,地址如下:



admire.j3code.cn



需要项目代码 + 视频 + 详细文档的,我都放在这个平台上了,自取即可。


我是J3code(三哥),咱们下回见

作者:J3code
链接:https://juejin.cn/post/7199820362954588197
来源:稀土掘金
收起阅读 »

为了摸鱼,我开发了一个工具网站

  大家好,我是派大星,由于前段时间实习入职,所以把时间以及精力都放在熟悉公司业务以及从工作中提升自己的业务逻辑,空余时间也是放在了学习新技术上,到目前为止也是参与了公司3个项目的开发团队中,参与过程中犯过错,暴露出了很多的不足,丧失过信心,学生时期...
继续阅读 »


  大家好,我是派大星,由于前段时间实习入职,所以把时间以及精力都放在熟悉公司业务以及从工作中提升自己的业务逻辑,空余时间也是放在了学习新技术上,到目前为止也是参与了公司3个项目的开发团队中,参与过程中犯过错,暴露出了很多的不足,丧失过信心,学生时期所带的傲气也是被一点一点的慢慢的打磨掉,正是因为这些,带给我的成长是巨大的。好了,闲言少叙,下面让我们进入今天的主题。


创作背景


       因为”懒“得走路,所以发明了汽车飞机,因为”懒“得干苦力活,所以发明了机器帮助我们做,很早之前看到过这个梗,而我这次同样也是因为”懒“,所以才开发出了这个工具。这里先卖个关子,先不说这个工具的作用,容我向大家吐槽一下这一段苦逼的经历。在我实习刚入职不久,就迎来了自己第一个任务,由于自己对所参与的项目的业务并不太了解,所以只能先做一些类似测试的工作,比如就像这次,组长给了我一份Json 文件,当我打开文件后看到数据都是一些地区名称,但当我随手的将滚动条往下一拉,瞬间发现不对劲,因为这个小小的文件行数竟然达到了1w+❗❗❗❗


不禁让我脊背发凉,但是这时我的担心还没达到最坏的地步,毕竟我还对具体的任务不了解。但当组长介绍任务内容,主要是让我将这些数据添加到数据库对应的表中,由于没有sql脚本,只有这个json 文件,需要手动去操作,而且给我定的任务周期是两天。听到这个时间时内心的慌张瞬间消失了,因为在之前我就了解过Navicat支持Json格式的文件直接导入数据,一个这么简单的任务给我两天时间,这不是非要让我带薪学习。


当我接下任务自信打开Navicat的导入功能时发现了一个重要问题,虽然它支持字段映射,但是给的Json数据是省市区地址名称,里面包含着各种嵌套,说实话到想到这里我已经慌了,而且也测试了一下字段只能单个的批量导入,而且不支持嵌套的类型,突然就明白为什么 给我两天的时间。


这时候心里只能默默祈祷已经有大神开发出了能处理这种数据的工具网站,但是经过一个小时的艰苦奋斗,但最终依旧是没有结果,网上有很多JsonSQl的工具网站,但是很多都支持简单支持一下生成创建表结构的语句,当场心如死灰,跑路的心都有了。但最终还是咬着牙 手动初始化数据,其过程中的“趣味” 实属无法用语言表达……

 上述就是这个工具的开发背景,也是怕以后再给我分配这么“有趣” 的任务。那么下面就给大家分享一下我自制的 Json转译SQL 工具,而且它也是一个完全免费的工具网站,同时这次也是将项目进行了 开源分享,大家也可以自己用现成的代码完成本地部署测试,感兴趣的同学可以自行拉取代码!



开源地址:github.com/pdxjie/sql-…



项目简介


Sql-Translation (简称ST)是一个 Json转译SQL 工具,在同类工具的基础上增强了功能,为节省时间、提高工作效率而生。并且遵循 “轻页面、重逻辑” 的原则,由极简页面来处理复杂任务,且它不仅仅是一个项目,而是以“降低时间成本、提高效率”为目标的执行工具。


技术选型

前端:

Vue
AntDesignUI组件库
MonacoEditor 编辑器
sql-formatter SQL格式化

后端:

SpringBoot
FastJson

项目特点

1.内置主键:JSON块如果包含id字段,在选择建表操作模式时内部会自动为id设置primary key
2.支持JSON数据生成建表语句:按照内置语法编写JSON,支持生成创建表的SQL语句
3.支持JSON数据生成更新语句:按照内置语法编写JSON,支持生成创更新的SQL语句,可配置单条件、多条件更新操作
4.支持JSON数据生成插入语句:按照内置语法编写JSON,支持生成创插入的SQL语句,如果JSON中包含 多层 (children)子嵌套,可按照相关语法指定作为父级id的字段
5.内置操作语法:该工具在选取不同的操作模式时,内置特定的使用语法规范
6.支持字段替换:需转译的JSON中字段与对应的SQL字段不一致时可以选择字段替换
7.界面友好:支持在线编辑JSON代码,支持代码高亮、语法校验、代码格式化、查找和替换、代码块折叠等,体验良好

解决痛点

下面就让我来给大家介绍一下Sql-Translation 可以解决哪些痛点问题:

需要将大量JSON中的数据导入到数据库中,但是JSON中包含大量父子嵌套关系 ——> 可以使用本站

在进行JSON数据导入数据库时,遇到JSON字段与数据库字段不一致需要替换字段时 ——> 可以使用本站

根据Apifox工具来实现更新或新增接口(前提是对接口已经完成了设计工作),提供了Body体数据,而且不想手动编写SQL时 ——> 可以使用本站

对上述三点进行进行举例说明(按照顺序):

第一种情况:

{
"id": "320500000",
"text": "苏州工业园区",
"value": "320500000",
"children": [
{
"id": "320505006",
"text": "斜塘街道",
"value": "320505006",
"children": []
},
{
"id": "320505007",
"text": "娄葑街道",
"value": "320505007",
"children": []
},
....
]
}

第二种情况:


第三种情况


以上内容就是该工具的简单介绍,由于该工具内置了部分语法功能,想要了解本工具全部工具以及想要动手操作的的同学请点击前往操作文档 ,该操作文档中包含了具体的语法介绍以及每种转换的具体示例数据 提供测试使用。


地址传送门



如果感兴趣的同学还希望可以到源码仓库给作者点个star⭐ 作为支持,非常感谢!


作者:派同学
链接:https://juejin.cn/post/7168285867160076295
来源:稀土掘金
收起阅读 »

宽表为什么横行?

宽表在BI业务中比比皆是,每次建设BI系统时首先要做的就是准备宽表。有时系统中的宽表可能会有上千个字段,经常因为“过宽”超过了数据库表字段数量限制还要再拆分。 为什么大家乐此不疲地造宽表呢?主要原因有两个。 一是为了提高查询性能。现代BI通常使用关系数据库作为...
继续阅读 »

宽表在BI业务中比比皆是,每次建设BI系统时首先要做的就是准备宽表。有时系统中的宽表可能会有上千个字段,经常因为“过宽”超过了数据库表字段数量限制还要再拆分。


为什么大家乐此不疲地造宽表呢?主要原因有两个。


一是为了提高查询性能。现代BI通常使用关系数据库作为后台,而SQL通常使用的HASH JOIN算法,在关联表数量和关联层级变多的时候,计算性能会急剧下降,有七八个表三四层级关联时就能观察到这个现象,而BI业务中的关联复杂度远远超过这个规模,直接使用SQL的JOIN就无法达到前端立等可取的查询需要了。为了避免关联带来的性能问题,就要先将关联消除,即将多表事先关联好采用单表存储(也就是宽表),再查询的时候就可以不用再关联,从而达到提升查询性能的目的。


二是为了降低业务难度。因为多表关联尤其是复杂关联在BI前端很难表达和使用。如果采用自动关联(根据字段类型等信息匹配)当遇到同维字段(如一个表有2个以上地区字段)时会“晕掉”不知道该关联哪个,表间循环关联或自关联的情况也无法处理;如果将众多表开放给用户来自行选择关联,由于业务用户无法理解表间关系而几乎没有可用性;分步关联可以描述复杂的关联需求,但一旦前一步出错就要推倒重来。所以,无论采用何种方式,工程实现和用户使用都很麻烦。但是基于单表来做就会简单很多,业务用户使用时没有什么障碍,因此将多表组织成宽表就成了“自然而然”的事情。


不过,凡事都有两面性,我们看到宽表好处而大量应用的同时,其缺点也不容忽视,有些缺点会对应用产生极大影响。下面来看一下。


宽表的缺点


数据冗余容量大


宽表不符合范式要求,将多个表合并成一个表会存在大量冗余数据,冗余程度跟原表数据量和表间关系有关,通常如果存在多层外键表,其冗余程度会呈指数级上升。大量数据冗余不仅会带来存储上的压力(多个表组合出来的宽表数量可能非常多)造成数据库容量问题,在查询计算时由于大量冗余数据参与运算还会影响计算性能,导致虽然用了宽表但仍然查询很慢。


数据错误


由于宽表不符合三范式要求,数据存储时可能出现一致性错误(脏写)。比如同一个销售员在不同记录中可能存储了不同的性别,同一个供应商在不同记录中的所在地可能出现矛盾。基于这样的数据做分析结果显然不对,而这种错误非常隐蔽很难被发现。


另外,如果构建的宽表不合理还会出现汇总错误。比如基于一对多的A表和B表构建宽表,如果A中有计算指标(如金额),在宽表中就会重复,基于重复的指标再汇总就会出现错误。


灵活性差


宽表本质上是一种按需建模的手段,根据业务需求来构建宽表(虽然理论上可以把所有表的组合都形成宽表,但这只存在于理论上,如果要实际操作会发现需要的存储空间大到完全无法接受的程度),这就出现了一个矛盾:BI系统建设的初衷主要是为了满足业务灵活查询的需要,即事先并不知道业务需求,有些查询是在业务开展过程中逐渐催生出来的,有些是业务用户临时起意的查询,这种灵活多变的需求采用宽表这种要事先加工的解决办法极为矛盾,想要获得宽表的好就得牺牲灵活性,可谓鱼与熊掌不可兼得。


可用性问题


除了以上问题,宽表由于字段过多还会引起可用性低的问题。一个事实表会对应多个维表,维表又有维表,而且表之间还可能存在自关联/循环关联的情况,这种结构在数据库系统中很常见,基于这些结构的表构建宽表,尤其要表达多个层级的时候,宽表字段数量会急剧增加,经常可能达到成百上千个(有的数据库表有字段数量限制,这时又要横向分表),试想一下,在用户接入界面如果出现上千个字段要怎么用?这就是宽表带来的可用性差的问题。


总体来看,宽表的坏处在很多场景中经常要大于好处,那为什么宽表还大量横行呢?


因为没办法。一直没有比宽表更好的方案来解决前面提到的查询性能和业务难度的问题。其实只要解决这两个问题,宽表就可以不用,由宽表产生的各类问题也就解决了。


SPL+DQL消灭宽表


借助开源集算器SPL可以完成这个目标。


SPL(Structured Process Language)是一个开源结构化数据计算引擎,本身提供了不依赖数据库的强大计算能力,SPL内置了很多高性能算法,尤其是对关联运算做了优化,对不同的关联场景采用不同的手段,可以大幅提升关联性能,从而不用宽表也能实时关联以满足多维分析时效性的需要。同时,SPL还提供了高性能存储,配合高效算法可以进一步发挥性能优势。


只有高性能还不够,SPL原生的计算语法不适合多维分析应用接入(生成SPL语句对BI系统改造较大)。目前大部分多维分析前端都是基于SQL开发的,但SQL体系(不用宽表时)在描述复杂关联计算上又很困难,基于这样的原因,SPL设计了专门的类SQL查询语法DQL(Dimensional Query Language)用于构建语义层。前端生成DQL语句,DQL Server将其转换成SPL语句,再基于SPL计算引擎和存储引擎完成查询返回给前端,实现全链路BI查询。需要注意的是,SPL只作为计算引擎存在,前端界面仍要由用户自行实现(或选用相应产品)。



SPL:关联实现技术


SPL如何不用宽表也能实现实时关联以满足性能要求的目标?


在BI业务中绝大部分的JOIN都是等值JOIN,也就是关联条件为等式的 JOIN。SPL把等值关联分为外键关联和主键关联。外键关联是指用一个表的非主键字段,去关联另一个表的主键,前者称为事实表,后者称为维表,两个表是多对一的关系,比如订单表和客户表。主键关联是指用一个表的主键关联另一个表的主键或部分主键,比如客户表和 VIP 客户表(一对一)、订单表和订单明细表(一对多)。


这两类 JOIN 都涉及到主键,如果充分利用这个特征采用不同的算法,就可以实现高性能的实时关联了。


不过很遗憾,SQL 对 JOIN 的定义并不涉及主键,只是两个表做笛卡尔积后再按某种条件过滤。这个定义很简单也很宽泛,几乎可以描述一切。但是,如果严格按这个定义去实现 JOIN,理论上没办法在计算时利用主键的特征来提高性能,只能是工程上做些有限的优化,在情况较复杂时(表多且层次多)经常无效。


SPL 改变了 JOIN 的定义,针对这两类 JOIN 分别处理,就可以利用主键的特征来减少运算量,从而提高计算性能。


外键关联


和SQL不同,SPL中明确地区分了维表和事实表。BI系统中的维表都通常不大,可以事先读入内存建立索引,这样在关联时可以少计算一半的HASH值。


对于多层维表(维表还有维表的情况)还可以用外键地址化的技术做好预关联。即将维表(本表)的外键字段值转换成对应维表(外键表)记录的地址。这样被关联的维表数据可以直接用地址取出而不必再进行HASH值计算和比对,多层维表仅仅是多个按地址取值的时间,和单层维表时的关联性能基本相当。


类似的,如果事实表也不大可以全部读入内存时,也可以通过预关联的方式解决事实表与维表的关联问题,提升关联效率。


预关联可以在系统启动时一次性读入并做好,以后直接使用即可。


当事实表较大无法全内存时,SPL 提供了外键序号化方法:将事实表中的外键字段值转换为维表对应记录的序号。关联计算时,用序号取出对应维表记录,这样可以获得和外键地址化类似的效果,同样能避免HASH值的计算和比对,大幅提升关联性能。


主键关联


有的事实表还有明细表,比如订单和订单明细,二者通过主键和部分主键进行关联,前者作为主表后者作为子表(还有通过全部主键关联的称为同维表,可以看做主子表的特例)。主子表都是事实表,涉及的数据量都比较大。


SPL为此采用了有序归并方法:预先将外存表按照主键有序存储,关联时顺序取出数据做归并,不需要产生临时缓存,只用很小的内存就可以完成计算。而SQL采用的HASH分堆算法复杂度较高,不仅要计算HASH值进行对比,还会产生临时缓存的读写动作,运算性能很差。


HASH 分堆技术实现并行困难,多线程要同时向某个分堆缓存数据,造成共享资源冲突;某个分堆关联时又会消费大量内存,无法实施较大的并行数量。而有序归则易于分段并行。数据有序时,子表就可以根据主表键值进行同步对齐分段以保证正确性,无需缓存,且因为占用内存很少可以采用较大的并行数,从而获得更高性能。


预先排序的成本虽高,但是一次性做好即可,以后就总能使用归并算法实现 JOIN,性能可以提高很多。同时,SPL 也提供了在有追加数据时仍然保持数据整体有序的方案。


对于主子表关联SPL还可以采用更有效的存储形式将主子表一体化存储,子表作为主表的集合字段,其取值是由与该主表数据相关的多条子表记录构成。这相当于预先实现了关联,再计算时直接取数计算即可,不需要比对,存储量也更少,性能更高。


存储机制


高性能离不开有效的存储。SPL也提供了列式存储,在BI计算中可以大幅降低数据读取量以提升读取效率。SPL列存采用了独有的倍增分段技术,相对传统列存分块并行方案要在很大数据量时(否则并行会受到限制)才会发挥优势不同,这个技术可以使SPL列存在数据量不很大时也能获得良好的并行分段效果,充分发挥并行优势。


SPL还提供了针对数据类型的优化机制,可以显著提升多维分析中的切片运算性能。比如将枚举型维度转换成整数,在查询时将切片条件转换成布尔值构成的对位序列,在比较时就可以直接从序列指定位置取出切片判断结果。还有将多个标签维度(取值是或否的维度,这种维度在多维分析中大量存在)存储在一个整数字段中的标签位维度技术(一个整数字段可以存储16个标签),不仅大幅减少存储量,在计算时还可以针对多个标签同时做按位计算从而大幅提升计算性能。


有了这些高效机制以后,我们就可以在BI分析中不再使用宽表,转而基于SPL存储和算法做实时关联,性能比宽表还更高(没有冗余数据读取量更小,更快)。


不过,只有这些还不够,SPL原生语法还不适合BI前端直接访问,这就需要适合的语义转换技术,通过适合的方式将用户操作转换成SPL语法进行查询。


这就需要DQL了。


DQL:关联描述技术


DQL是SPL之上的语义层构建工具,在这一层完成对于SPL数据关联关系的描述(建模)再为上层应用服务。即将SPL存储映射成DQL表,再基于表来描述数据关联关系。



通过对数据表关系描述以后形成了一种以维度为中心的总线式结构(不同于E-R图中的网状结构),中间是维度,表与表之间不直接相关都通过维度过渡。



基于这种结构下的关联查询(DQL语句)会很好表达。比如要根据订单表(orders)、客户表(customer)、销售员表(employee)以及城市表(city)查询:本年度华东的销售人员,在全国各销售区的销售额


用SQL写起来是这样的:


SELECT
ct1.area,o.emp_id,sum(o.amount) somt
FROM
orders o
JOIN customer c ON o.cus_id = c.cus_id
JOIN city ct1 ON c.city_id = ct1.city_id
JOIN employee e ON o.emp_id = e.emp_id
JOIN city ct2 ON e.city_id = ct2.city_id
WHERE
ct2.area = 'east' AND year(o.order_date)= 2022
GROUP BY
ct1.area, o.emp_id

多个表关联要JOIN多次,同一个地区表要反复关联两次才能查到销售员和客户的所在区域,对于这种情况BI前端表达起来会很吃力,如果将关联开放出来,用户又很难理解。


那么DQL是怎么处理的呢?


DQL写法:


SELECT
cus_id.city_id.area,emp_id,sum(amount) somt
FROM
orders
WHERE
emp_id.city_id.area == "east" AND year(order_date)== 2022
BY
cus_id.city_id.area,emp_id

DQL不需要JOIN多个表,只基于orders单表查询就可以了,外键指向表的字段当成属性直接使用,有多少层都可以引用下去,很好表达。像查询客户所在地区通过cus_id.city_id.area一直写下去就可以了,这样就消除了关联,将多表关联查询转化成单表查询。


更进一步,我们再基于DQL开发BI前端界面就很容易,比如可以做成这样:



用树结构分多级表达多层维表关联,这样的多维分析页面不仅容易开发,普通业务用户使用时也很容易理解,这就是DQL的效力。


总结一下,宽表的目的是为了解决BI查询性能和前端工程实现问题,而宽表会带来数据冗余和灵活性差等问题。通过SPL的实时关联技术与高效存储可以解决性能问题,而且性能比宽表更高,同时不存在数据冗余,存储空间也更小(压缩);DQL构建的语义层解决了多维分析前端工程的实现问题,让实时关联成为可能,,灵活性更高(不再局限于宽表的按需建模),界面也更容易实现,应用范围更广。


SPL+DQL继承(超越)宽表的优点同时改善其缺点,这才是BI该有的样子。


SPL资料


收起阅读 »

IDEA建议:不要在字段上使用@Autowire了!

在使用IDEA写Spring相关的项目的时候,在字段上使用@Autowired注解时,总是会有一个波浪线提示:Field injection is not recommended. 纳尼?我天天用,咋就不建议了,今天就来一探究竟。 众所周知,在Spring里...
继续阅读 »

在使用IDEA写Spring相关的项目的时候,在字段上使用@Autowired注解时,总是会有一个波浪线提示:Field injection is not recommended. 纳尼?我天天用,咋就不建议了,今天就来一探究竟。



众所周知,在Spring里面有三种可选的注入方式:构造器注入、Setter方法注入、Field注入,我们先来看下这三种注入方式的使用场景。


构造器注入


如下所示,使用构造器注入,可以将属性字段设置为final,在Aservice进行实例化时,BService对象必须得提前初始化完成,所以使用构造器注入,能够保证被注入的对象一定不为null。构造器注入适用于对象之间强依赖的场景,但是无法解决的循环依赖问题(因为必须互相依赖对方初始化完成,当然会产生冲突无法解决)。关于循环依赖,推荐阿里的一篇文章 一文详解Spring Bean循环依赖


@Service
public class AService {
   private final BService bService;

   @Autowired  //spring framework 4.3之后可以不用在构造方法上标注@Autowired
   public AService(BService bService) {
       this.bService = bService;
  }
}

Setter 方法注入


使用Setter方法进行注入时,Spring会在执行默认的无参构造函数实例化Bean对象之后,调用Setter方法来注入依赖。使用Setter方法注入可以将 required 属性设置为 false,表示若注入的Bean对象不存在,直接跳过注入,不会报错。


@Service
public class AService {
   private  BService bService;

   @Autowired(required = false)
   public void setbService(BService bService) {
       this.bService = bService;
  }
}

Field注入


一眼看去,Field注入简洁美观,被大家普遍大量使用。Spring容器会在对象实例化完成之后,通过反射设置需要注入的的字段。


@Service
public class AService {
   @Autowired
   private  BService bService;
}

为什么IDEA不推荐使用Field注入


经查阅各方资料,我找到了如下几个比较重要的原因:



  • 可能导致空指针异常:如果创建对象不使用Spring容器,而是直接使用无参构造方法new一个对象,此时使用注入的对象会导致空指针。

  • 不能使用final修饰字段:不使用final修饰,会导致类的依赖可变,进而可能会导致一些不可预料的异常。通常情况下,此时可以使用构造方法注入来声明强制依赖的Bean,使用Setter方法注入来声明可选依赖的Bean。

  • 可能更容易违反单一职责原则:个人认为这点是一个很重要的原因。因为使用字段注入可以很轻松的在类上加入各种依赖,进而导致类的职责过多,但是往往开发者对此不能轻易察觉。而如果使用构造方法注入,当构造方法的参数过多时,就会提醒你,你该重构这个类了。

  • 不利于写单元测试:在单元测试中,使用Field注入,必须使用反射的方式来Mock依赖对象。


那么替代方案是什么呢?其实上面已经提到了,当我们的类有强依赖于其他Bean时,使用构造方法注入;可选依赖时,使用Setter方法注入(需要自己处理可能出现的引用对象不存在的情况)。


Spring官方的态度


Spring 官方文档在依赖注入这一节其实没有讨论字段注入这种方式,重点比较了构造方法注入和Setter注入。可以看到Spring团队强推的还是构造方法注入


构造方法注入还是Setter注入


总结


在Spring中使用依赖注入时,首选构造方法注入,虽然其无法解决循环依赖问题,但是当出现循环依赖时,首选应该考虑的是是否代码结构设计出现问题了,当然,也不排除必须要循环依赖的场景,此时字段注入也有用武之地。


最后想说的是,平时在使用IDEA的过程中,可能会有一些下划线或飘黄提醒,如果多细心观察,可以学习到很多他人已经总结好的最佳实践经验,有助于自己代码功底的提升,共勉!


参考文献:


Spring 官方文档关于依赖注入: docs.spring.io/spring-fram…


StackOverFlow关于避免使用字段注入的讨论:stackoverflow.com/questi

ons/3…

收起阅读 »

怎么做登录(单点登录)功能?

登陆是系统最基础的功能之一。这么长时间了,一直在写业务,这个基础功能反而没怎么好好研究,都忘差不多了。今天没事儿就来撸一下。 以目前在接触和学习的一个开源系统为例,来分析一下登陆该怎么做。代码的话我就直接从里面CV了。 简单上个图(有水印。因为穷所以没开会员...
继续阅读 »

登陆是系统最基础的功能之一。这么长时间了,一直在写业务,这个基础功能反而没怎么好好研究,都忘差不多了。今天没事儿就来撸一下。


以目前在接触和学习的一个开源系统为例,来分析一下登陆该怎么做。代码的话我就直接从里面CV了。



简单上个图(有水印。因为穷所以没开会员)


怎么做登陆(单点登陆)?.png


先分析下登陆要做啥



首先,搞清楚要做什么。


登陆了,系统就知道这是谁,他有什么权限,可以给他开放些什么业务功能,他能看到些什么菜单?。。。这是这个功能的目的和存在的意义。



怎么落实?



怎么实现它?用什么实现?




我们的项目是Springboot + Vue前后端分离类型的。


选择用token + redis 实现,权限的话用SpringSecurity来做。




前后端分离避不开的一个问题就是单点登陆,单点登陆咱们有很多实现方式:CAS中央认证、JWT、token等,咱们这种方式其实本身就是基于token的一个单点登陆的实现方案。


单点登陆我们改天整理一篇OAuth2.0的实现方式,今天不搞这个。



上代码



概念这个东西越说越玄。咱们直接上代码吧。



接口:

@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
   AjaxResult ajax = AjaxResult.success();
   // 生成令牌
   //用户名、密码、验证码、uuid
   String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                                     loginBody.getUuid());
   ajax.put(Constants.TOKEN, token);
   return ajax;
}


用户信息验证交给SpringSecurity



/**
* 登录验证
*/

public String login(String username, String password, String code, String uuid)
{
   // 验证码开关,顺便说一下,系统配置相关的开关之类都缓存在redis里,系统启动的时候加载进来的。这一块儿的代码就不贴出来了
   boolean captchaEnabled = configService.selectCaptchaEnabled();
   if (captchaEnabled)
  {
       //uuid是验证码的redis key,登陆页加载的时候验证码生成接口返回的
       validateCaptcha(username, code, uuid);
  }
   // 用户验证 -- SpringSecurity
   Authentication authentication = null;
   try
  {
       UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
       AuthenticationContextHolder.setContext(authenticationToken);
       // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername。
       //
       authentication = authenticationManager.authenticate(authenticationToken);
  }
   catch (Exception e)
  {
       if (e instanceof BadCredentialsException)
      {
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
           throw new UserPasswordNotMatchException();
      }
       else
      {
           AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
           throw new ServiceException(e.getMessage());
      }
  }
   finally
  {
       AuthenticationContextHolder.clearContext();
  }
   AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
   LoginUser loginUser = (LoginUser) authentication.getPrincipal();
   recordLoginInfo(loginUser.getUserId());
   // 生成token
   return tokenService.createToken(loginUser);
}

把校验验证码的部分贴出来,看看大概的逻辑(这个代码封装得太碎了。。。没全整出来)

/**
* 校验验证码
*/

public void validateCaptcha(String username, String code, String uuid)
{
   //uuid是验证码的redis key
   String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, ""); //String CAPTCHA_CODE_KEY = "captcha_codes:";
   String captcha = redisCache.getCacheObject(verifyKey);
   redisCache.deleteObject(verifyKey);
   if (captcha == null)
  {
       AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
       throw new CaptchaExpireException();
  }
   if (!code.equalsIgnoreCase(captcha))
  {
       AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
       throw new CaptchaException();
  }
}

token生成部分


这里,token



/**
* 创建令牌
*/

public String createToken(LoginUser loginUser)
{
   String token = IdUtils.fastUUID();
   loginUser.setToken(token);
   setUserAgent(loginUser);
   refreshToken(loginUser);

   Map<String, Object> claims = new HashMap<>();
   claims.put(Constants.LOGIN_USER_KEY, token);
   return createToken(claims);
}

刷新token

/**
* 刷新令牌
*/

public void refreshToken(LoginUser loginUser)
{
   loginUser.setLoginTime(System.currentTimeMillis());
   loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
   // 根据uuid将loginUser缓存
   String userKey = getTokenKey(loginUser.getToken());
   redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}

验证token

/**
* 验证令牌
*/

public void verifyToken(LoginUser loginUser)
{
   long expireTime = loginUser.getExpireTime();
   long currentTime = System.currentTimeMillis();
   if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
  {
       refreshToken(loginUser);
  }
}


注意这里返回给前端的token其实用JWT加密了一下,SpringSecurity的过滤器里有进行解析。


另外,鉴权时会刷新token有效期,看下面第二个代码块的注释。



@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
   //...无关的代码删了
   httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
}

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
   @Autowired
   private TokenService tokenService;

   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
       throws ServletException, IOException
  {
       LoginUser loginUser = tokenService.getLoginUser(request);
       if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
      {
           //刷新token有效期
           tokenService.verifyToken(loginUser);
           UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
           authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
           SecurityContextHolder.getContext().setAuthentication(authenticationToken);
      }
       chain.doFilter(request, response);
  }
}


这个登陆方案里用了token + redis,还有JWT,其实用哪一种方案都可以独立实现,并且两种方案都可以用来做单点登陆。


这里JWT只是起到个加密的作用,无它。


作者:harhar
来源:juejin.cn/post/7184266088652210231

收起阅读 »

项目使用redis做缓存,除了击穿,穿透,雪崩,我们还要考虑哪些!!!

大家好,我是小趴菜,相信大家在项目中都是用过redis,比如用来做一个分布式缓存来提高程序的性能 当使用到了redis来做缓存,那么我们就必须要考虑几个问题,除了缓存击穿,缓存穿透,缓存雪崩,那么我们还需要考虑哪些问题呢? 高并发写 对于高并发情况下,比如直播...
继续阅读 »

大家好,我是小趴菜,相信大家在项目中都是用过redis,比如用来做一个分布式缓存来提高程序的性能


当使用到了redis来做缓存,那么我们就必须要考虑几个问题,除了缓存击穿,缓存穿透,缓存雪崩,那么我们还需要考虑哪些问题呢?


高并发写


对于高并发情况下,比如直播下单,直播下单跟秒杀不一样,秒杀是有限定的库存,但是直播下单是可以一直下的,而且是下单越多越好的。比如说我们的库存有10万个,如果这个商品特别火,那么可能一瞬间流量就全都打过来了。虽然我们的库存是提前放到redis中,并不会去访问MySql,那么这时候所有的请求都会打到redis中。


image.png


表面看起来确实没问题,但是你有没有想过,即使你做了集群,但是访问的还是只有一个key,那么最终还是会落到同一台redis服务器上。这时候key所在的那台redid就会承载所有的请求,而集群其它机器根本就不会访问到,这时候你确定你的redis能扛住吗???如果这时候读的请求很多,你觉得你的redis能扛住吗?


所以对于这种情况我们可以采用数据分片的解决方案,比如你有10万个库存,那么这时候可以搞10台redis服务器,每台redis服务器上放1万个库存,这时候我们可以通过用户的ID进行取模,然后将用户流量分摊到10台redis服务器上


image.png


所以对于热点数据来说,我们要做的就是将流量进行分摊,让多台redis分摊承载一部分流量,尤其是对于这种高并发写来讲


高并发读


使用redis做缓存可以说是我们项目中使用到的最多的了,可能由于平时访问量不高,所以我们的redis服务完全可以承载这么多用户的请求


但是我们可以想一下,一次reids的读请求就是一次的网络IO,如果是1万次,10万次呢?那就是10万次的网络IO,这个问题我们在工作中是不得不考虑的。因为这个开销其实是很大的,如果访问量太大,redis很有可能就会出现一些问题


image.png


我们可以使用本地缓存+redis分布式缓存来解决这个问题,对于一些热点读数据,更新不大的数据,我们可以将数据保存在本地缓存中,比如Guava等工具类,当然本地缓存的过期时间要设置的短一点,比如5秒左右,这时候可以让大部分的请求都落在本地缓存,不用去访问redis


如果这时候本地缓存没有,那么再去访问redis,然后将redis中的数据再放入本地缓存中即可


加入了多级缓存,那么就会有相应的问题,比如多级缓存如何保证数据一致性


总结


没有完美的方案,只有最适合自己的方案,当你决定采用了某种技术方案的时候,那么势必会带来一些其它你需要考虑的问题,redis也一样,虽然我们使用它来做缓存可以提高我们程序的性能,但是在使用redis做缓存的时候,有些情况我们也是需要考虑到的,对于用户访问量不高来说,我们直接使用redis完全是够用的,但是我们可以假设一下,如果在高并发场景下,我们的方案是否能够支持我们的业务


作者:我是小趴菜
来源:juejin.cn/post/7264475859659079736
收起阅读 »

聊聊分片技术

今天来聊一聊开发中一个比较常见的概念“分片”技术。这个概念听起来好像是在讲切西瓜,但其实不是!它是指将大型数据或者任务分成小块处理的技术。 就像吃面条一样,太长了不好吃,我们要把它们分成小段,才能更好地享受美味。所以,如果你想让你的程序更加高效,不妨考虑一下...
继续阅读 »

今天来聊一聊开发中一个比较常见的概念“分片”技术。这个概念听起来好像是在讲切西瓜,但其实不是!它是指将大型数据或者任务分成小块处理的技术。


就像吃面条一样,太长了不好吃,我们要把它们分成小段,才能更好地享受美味。所以,如果你想让你的程序更加高效,不妨考虑一下“分片”技术!


1. “分片”技术定义


在计算机领域中,“分片”(sharding)是一种 把大型数据集分割成更小的、更容易管理的数据块的技术


一个经典的例子是数据库分片。


想象一家巨大的电商公司,拥有数百万甚至数十亿的用户,每天进行大量的交易和数据处理。这些数据包括用户信息、订单记录、支付信息等。传统的数据库系统可能无法应对如此巨大的数据量和高并发请求。


在这种情况下,公司可以采用数据库分片技术来解决问题。数据库分片是将一个庞大的数据库拆分成更小的、独立的片(shard)。


每个片都包含数据库的一部分数据,类似于一个小型的数据库。每个片都可以在不同的服务器上独立运行,这样就可以将数据负载分散到多个服务器上,提高了整个系统的性能和可伸缩性。


所以,分片技术提高了数据库的扩展性和吞吐量。


2. 分片技术应用:日志分片


好了,我们已经了解了分片技术的概念和它能够解决的问题。但是,你知道吗?分片技术还有一个非常有趣的应用场景——日志分片。


一个更加具体的应用场景是,手机端日志的记录、存储和上传


在日志分片中,原始的日志文件被分成多个较小的片段,每个片段包含一定数量的日志条目。这样做的好处是可以提高日志的读写效率和处理速度。当我们需要查找特定时间段的日志或者进行日志分析时,只需要处理相应的日志分片,而不需要处理整个大型日志文件。


日志分片还可以帮助我们更好地管理日志文件的存储空间。由于日志文件通常会不断增长,如果不进行分片,日志文件的大小会越来越大,占用大量的存储空间。而通过将日志文件分片存储,可以将存储空间的使用分散到多个较小的文件中,更加灵活地管理和控制存储空间的使用。


所以,分片技术不仅可以让你的日志更高效,还可以让你的存储更优雅哦!


总结一下,在手机端对日志进行分片可以带来如下的好处:





  • 减少数据传输量: 手机端往往有限的网络带宽和数据流量。通过将日志分片,只需要发送关键信息或重要的日志片段,而不是整个日志文件,从而减少了数据传输量,降低了网络负载。





  • 节省存储空间: 手机设备通常有有限的存储空间。通过分片日志,可以只保留最重要的日志片段,避免将大量无用的日志信息保存在设备上,节省存储空间。





  • 提高性能: 小型移动设备的计算能力有限,处理大量的日志数据可能会导致应用程序性能下降。日志分片可以减轻应用程序对处理和存储日志的负担,从而提高应用程序的性能和响应速度。





  • 快速故障排查: 在开发和调试阶段,日志是重要的调试工具。通过分片日志,可以快速获取关键信息,帮助开发者定位和解决问题,而不需要浏览整个日志文件。





  • 节省电池寿命: 日志记录可能涉及磁盘或网络活动,这些活动对手机的电池寿命有一定影响。分片日志可以减少不必要的磁盘写入和网络通信,有助于节省电池能量。





  • 安全性和隐私保护: 对于敏感数据或用户隐私相关的日志,分片可以帮助隔离和保护这些数据,确保只有授权的人员可以访问敏感信息。





  • 容错和稳定性: 如果手机应用程序崩溃或出现问题,分片日志可以确保已经记录的日志信息不会因为应用程序的异常终止而丢失,有助于在重启后快速恢复。




3.日志分片常见的实现方式


常见的日志分片实现方式有 3 种,一种是基于时间的分片,一种是基于大小的分片,还有一种是基于关键事件的分片


3.1 按时间分片


将日志按照时间周期进行分片,例如每天、每小时或每分钟生成一个新的日志文件。伪代码如下:


import logging
from datetime import datetime

# 配置日志记录
logging.basicConfig(filename=f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}.log",
                    level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s - %(message)s')

# 记录日志
logging.debug("This is a debug message.")
logging.info("This is an info message.")
logging.warning("This is a warning message.")
logging.error("This is an error message.")

3.2 按文件大小分片


将日志按照文件大小进行分片,当达到预设的大小后,生成一个新的日志文件。伪代码如下:


import logging
import os

# 设置日志文件的最大大小为5MB
max_log_size = 5 * 1024 * 1024

# 配置日志记录
log_file = "log.log"
logging.basicConfig(filename=log_file,
                    level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s - %(message)s')

# 获取当前日志文件大小
def get_log_file_size(file_path):
    return os.path.getsize(file_path)

# 检查日志文件大小,超过最大大小则创建新的日志文件
def check_log_file_size():
    if get_log_file_size(log_file) > max_log_size:
        logging.shutdown()
        os.rename(log_file, f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}.log")
        logging.basicConfig(filename=log_file,
                            level=logging.DEBUG,
                            format='%(asctime)s - %(levelname)s - %(message)s')

# 记录日志
logging.debug("This is a debug message.")
logging.info("This is an info message.")
logging.warning("This is a warning message.")
logging.error("This is an error message.")

# 检查并切割日志文件
check_log_file_size()

3.3 按关键事件分片


将日志按照特定的关键事件进行分片,例如每次启动应用程序或者每次用户登录都生成一个新的日志文件。伪代码如下:


import logging

# 配置日志记录
logging.basicConfig(filename="log.log",
                    level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s - %(message)s')

# 记录日志
logging.debug("This is a debug message.")
logging.info("This is an info message.")
logging.warning("This is a warning message.")
logging.error("This is an error message.")

# 在关键事件处切割日志
def split_log_on_critical_event():
    logging.shutdown()
    new_log_file = f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}.log"
    os.rename("log.log", new_log_file)
    logging.basicConfig(filename="log.log",
                        level=logging.DEBUG,
                        format='%(asctime)s - %(levelname)s - %(message)s')

# 在关键事件处调用切割函数
split_log_on_critical_event()

这些只是日志分片的简单示例,实际应用中,可能还需要考虑并发写入的处理。不同的应用场景和需求可能会有不同的实现方式,但上述示例可以作为日志分片的入门参考。


4.不同日志分片方式的优缺点


每种日志分片方式都有其优点和缺点,实际工作中选择哪种方式取决于项目需求、系统规模和性能要求。下面是它们各自的优缺点和选择建议:


4.1 按时间分片


优点:





  • 日志文件按时间周期自动切割,管理简单,易于维护和查找。



  • 可以按照日期或时间段快速定位特定时间范围的日志,方便问题排查和分析。


缺点:





  • 如果日志记录非常频繁,生成的日志文件可能会较多,占用较多的磁盘空间。


选择建议:





  • 适用于需要按照时间段来管理和查找日志的场景,如每天生成一个新的日志文件,适合于长期存档和快速回溯日志的需求。


4.2 按文件大小分片


优点:





  • 可以控制单个日志文件的大小,避免单个日志文件过大,减少磁盘空间占用。



  • 可以根据日志记录频率和系统负载自动调整滚动策略,灵活性较高。


缺点:





  • 按文件大小滚动的切割可能不是按照时间周期进行,导致在某个时间段内的日志记录可能分布在多个文件中,查找时稍显不便。


选择建议:





  • 适用于需要控制单个日志文件大小、灵活滚动日志的场景,可以根据日志记录量进行动态调整滚动策略。


4.3 按关键事件分片


优点:





  • 可以根据特定的关键事件或条件生成新的日志文件,使得日志按照业务操作或系统事件进行切割,更符合实际需求。


缺点:





  • 需要在代码中显式触发滚动操作,可能会增加一定的复杂性和代码维护成本。


选择建议:





  • 适用于需要根据特定事件进行日志切割的场景,如应用程序重启、用户登录等。


在实际工作中,通常需要综合考虑项目的实际情况来选择合适的日志分片方式。可以考虑以下因素:





  • 日志记录频率和数据量: 如果日志记录频率很高且数据量大,可能需要按文件大小分片来避免单个日志文件过大。





  • 日志存储要求: 如果需要长期存档日志并快速查找特定时间范围的日志,按时间分片可能更适合。





  • 日志文件管理: 如果希望日志文件按照特定的事件或条件进行切割,按关键事件分片可能更合适。





  • 磁盘空间和性能: 考虑日志文件大小对磁盘空间的占用和日志滚动对系统性能的影响。




所以,实际开发中通常需要根据项目的具体需求和系统规模,选择合适的日志分片方式。


在日志框架中通常可以通过配置来选择适合的滚动策略,也可以根据实际需求自定义一种滚动策略。


作者:有余同学
来源:mdnice.com/writing/e97842c4d3734ad8b8a1dd587342d985
收起阅读 »

Nginx 体系化之虚拟主机分类及配置实现

Nginx,这款备受推崇的高性能 Web 服务器,以其强大的性能和灵活的配置而广受欢迎。在实际应用中,虚拟主机是一项重要的功能,允许我们在单个服务器上托管多个网站。本文将深入探讨 Nginx 虚拟主机的分类和配置实现,帮助您构建一个高效多站点托管平台。 虚拟主...
继续阅读 »

Nginx,这款备受推崇的高性能 Web 服务器,以其强大的性能和灵活的配置而广受欢迎。在实际应用中,虚拟主机是一项重要的功能,允许我们在单个服务器上托管多个网站。本文将深入探讨 Nginx 虚拟主机的分类和配置实现,帮助您构建一个高效多站点托管平台。


虚拟主机的分类


虚拟主机是一种将单个服务器划分成多个独立的网站托管环境的技术。Nginx 支持三种主要类型的虚拟主机:


基于 IP 地址的虚拟主机(常用)


这种类型的虚拟主机是通过不同的 IP 地址来区分不同的网站。每个 IP 地址绑定到一个特定的网站或应用程序。这种虚拟主机适用于需要在同一服务器上为每个网站提供独立的资源和配置的场景。


基于域名的虚拟主机(常用)


基于域名的虚拟主机是根据不同的域名来区分不同的网站。多个域名可以共享同一个 IP 地址,并通过 Nginx 的配置来分发流量到正确的网站。这种虚拟主机适用于在单个服务器上托管多个域名或子域名的情况。


基于多端口的虚拟主机(不常用)


基于多端口的虚拟主机是一种将单个服务器上的多个网站隔离开来的方式。每个网站使用不同的端口号进行访问,从而实现隔离。这种方法特别适用于那些无法使用不同域名或 IP 地址的情况,或者需要在同一服务器上快速托管多个网站的需求。


虚拟主机配置实现


配置文件结构


Nginx 的配置文件通常位于 /etc/nginx/nginx.conf,在该文件中可以找到 http 块。在 http 块内,可以配置全局设置和默认行为。每个虚拟主机都需要一个 server 块来定义其配置。
使用 include 指令简化配置文件,通常情况下将基于 server 的配置文件放到一个文件夹中,由 include 引用即可


http{
include /usr/nginx/server/*.conf # 表示引用 server 下的配置文件
}

基于 IP 地址的虚拟主机实现


创建 IP 配置文件


/usr/nginx/server/ 中创建一个新的配置文件,例如 /usr/nginx/server/ip.conf


配置 IP


在新的配置文件中,为每个网站创建一个 server 块,并在其中指定监听的端口号和网站的根目录。例如:


# 基于 192.168.1.10 代理到百度网站
server {
listen 192.168.1.10:80;
server_name http://www.baidu.com;
root /var/www/baidu;
index index.html;
}
# 基于 192.168.1.11:80 代理到 bing 网站
server {
listen 192.168.1.11:80;
server_name http://www.bing.com;
root /var/www/bing;
index index.html;
}

最佳场景实践



  1. 资源隔离: 每个网站都有独立的 IP 地址、资源和配置,避免了资源冲突和相互影响。

  2. 安全性提升: 基于 IP 地址的虚拟主机可以增强安全性,减少不同网站之间的潜在风险。

  3. 独立访问: 每个网站都有独立的 IP 地址,可以实现独立的访问控制和限制。

  4. 多租户托管: 基于 IP 地址的虚拟主机适用于多租户托管场景,为不同客户提供独立环境。


基于域名的虚拟主机实现


创建 IP 配置文件


/usr/nginx/server/ 中创建一个新的配置文件,例如 /usr/nginx/server/domain.conf


配置 IP


在新的配置文件中,为每个网站创建一个 server 块,并在其中指定监听的域名和网站的根目录。例如:


# 通过 http://www.baidu.com 转发到 80
server {
listen 80;
server_name http://www.baidu.com;
root /var/www/baidu;
index index.html;
}

# 通过 http://www.bing.com 转发到 80
server {
listen 80;
server_name http://www.bing.com;
root /var/www/bing;
index index.html;
}

最佳场景实践


基于域名的虚拟主机为多站点托管提供了高度的定制性和灵活性:



  1. 品牌差异化: 不同域名的虚拟主机允许您为不同品牌或应用提供独立的网站定制,提升用户体验。

  2. 定向流量: 基于域名的虚拟主机可以将特定域名的流量引导至相应的网站,实现定向流量管理。

  3. 子域名托管: 可以将不同子域名配置为独立的虚拟主机,为多个应用或服务提供托管。

  4. SEO 优化: 每个域名的虚拟主机可以针对不同的关键词进行 SEO 优化,提升搜索引擎排名。


基于多端口的虚拟主机


创建多端口配置文件


/usr/nginx/server/ 中创建一个新的配置文件,例如 /usr/nginx/server/domain.conf


配置 IP


在新的配置文件中,为每个网站创建一个 server 块,并在其中指定监听的域名和网站的根目录。例如:


server {
listen 8081;
server_name http://www.baidu.com;
root /var/www/baidu;
index index.html;
}

server {
listen 8082;
server_name http://www.bing.com;
root /var/www/bing;
index index.html;
}

最佳场景实践


基于多端口的虚拟主机为多站点托管提供了更多的灵活性和选择:



  1. 快速设置: 使用多端口可以快速设置多个网站,适用于临时性或开发环境。

  2. 资源隔离: 每个网站都有独立的端口和配置,避免了资源冲突和相互干扰。

  3. 开发和测试: 多端口虚拟主机适用于开发和测试环境,每个开发者可以使用不同的端口进行开发和调试。

  4. 灰度发布: 基于多端口的虚拟主机可以实现灰度发布,逐步引导流量至新版本网站。


重载配置


在添加、修改或删除多端口虚拟主机配置后,使用以下命令重载 Nginx 配置,使更改生效:


nginx -s reload
作者:努力的IT小胖子
来源:juejin.cn/post/7263886796757483580

收起阅读 »

如果按代码量算工资,也许应该这样写

前言 假如有一天我们要按代码量来算工资,那怎样才能写出一手漂亮的代码,同时兼顾代码行数和实际意义呢? 要在增加代码量的同时提高代码质量和可维护性,能否做到呢? 答案当然是可以,这可难不倒我们这种摸鱼高手。 耐心看完,你一定有所收获。 正文 1. 实现更多的...
继续阅读 »

前言


假如有一天我们要按代码量来算工资,那怎样才能写出一手漂亮的代码,同时兼顾代码行数和实际意义呢?


要在增加代码量的同时提高代码质量和可维护性,能否做到呢?


答案当然是可以,这可难不倒我们这种摸鱼高手。


耐心看完,你一定有所收获。


giphy.gif


正文


1. 实现更多的接口:


给每一个方法都实现各种“无关痛痒”的接口,比如SerializableCloneable等,真正做到不影响使用的同时增加了相当数量的代码。


为了这些代码量,其中带来的性能损耗当然是可以忽略的。


public class ExampleClass implements Serializable, Comparable<ExampleClass>, Cloneable, AutoCloseable {

@Override
public int compareTo(ExampleClass other) {
// 比较逻辑
return 0;
}

// 实现 Serializable 接口的方法
private void writeObject(ObjectOutputStream out) throws IOException {
// 序列化逻辑
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 反序列化逻辑
}

// 实现 Cloneable 接口的方法
@Override
public ExampleClass clone() throws CloneNotSupportedException {
// 复制对象逻辑
return (ExampleClass) super.clone();
}

// 实现 AutoCloseable 接口的方法
@Override
public void close() throws Exception {
// 关闭资源逻辑
}

}


除了示例中的SerializableComparableCloneableAutoCloseable,还有Iterable


2. 重写 equals 和 hashcode 方法


重写 equalshashCode 方法绝对是上上策,不仅增加了代码量,还为了让对象在相等性判断和散列存储时能更完美的工作,确保代码在处理对象相等性时更准确、更符合业务逻辑。


public class ExampleClass {
private String name;
private int age;

// 重写 equals 方法
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}

if (obj == null || getClass() != obj.getClass()) {
return false;
}

ExampleClass other = (ExampleClass) obj;
return this.age == other.age && Objects.equals(this.name, other.name);
}

// 重写 hashCode 方法
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}


giphy (2).gif


3. 增加配置项和参数:


不要管能不能用上,梭哈就完了,问就是为了健壮性和拓展性。


public class AppConfig {
private int maxConnections;
private String serverUrl;
private boolean enableFeatureX;

// 新增配置项
private String emailTemplate;
private int maxRetries;
private boolean enableFeatureY;

// 写上构造函数和getter/setter
}

4. 增加监听回调:


给业务代码增加监听回调,比如执行前、执行中、执行后等各种Event,这里举个完整的例子。


比如创建个 EventListener ,负责监听特定类型的事件,事件源则是产生事件的对象。通过EventListener 在代码中增加执行前、执行中和执行后的事件。


首先,我们定义一个简单的事件类 Event


public class Event {
private String name;

public Event(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

然后,我们定义一个监听器接口 EventListener


public interface EventListener {
void onEventStart(Event event);

void onEventInProgress(Event event);

void onEventEnd(Event event);
}

接下来,我们定义一个事件源类 EventSource,在执行某个业务方法时,触发事件通知:


public class EventSource {
private List<EventListener> listeners = new ArrayList<>();

public void addEventListener(EventListener listener) {
listeners.add(listener);
}

public void removeEventListener(EventListener listener) {
listeners.remove(listener);
}

public void businessMethod() {
Event event = new Event("BusinessEvent");

// 通知监听器:执行前事件
for (EventListener listener : listeners) {
listener.onEventStart(event);
}

// 模拟执行业务逻辑
System.out.println("Executing business method...");

// 通知监听器:执行中事件
for (EventListener listener : listeners) {
listener.onEventInProgress(event);
}

// 模拟执行业务逻辑
System.out.println("Continuing business method...");

// 通知监听器:执行后事件
for (EventListener listener : listeners) {
listener.onEventEnd(event);
}
}
}

现在,我们可以实现具体的监听器类,比如 BusinessEventListener,并在其中定义事件处理逻辑:


public class BusinessEventListener implements EventListener {
@Override
public void onEventStart(Event event) {
System.out.println("Event Start: " + event.getName());
}

@Override
public void onEventInProgress(Event event) {
System.out.println("Event In Progress: " + event.getName());
}

@Override
public void onEventEnd(Event event) {
System.out.println("Event End: " + event.getName());
}
}

最后,我们写个main函数来演示监听事件:


public class Main {
public static void main(String[] args) {
EventSource eventSource = new EventSource();
eventSource.addEventListener(new BusinessEventListener());

// 执行业务代码,并触发事件通知
eventSource.businessMethod();

// 移除监听器
eventSource.removeEventListener(businessEventListener);
}
}

如此这般那般,代码量猛增,还顺带实现了业务代码的流程监听。当然这只是最简陋的实现,真实环境肯定要比这个复杂的多。


5. 构建通用工具类:


同样的,甭管用不用的上,定义更多的方法,都是为了健壮性。


比如下面这个StringUtils,可以从ApacheCommons、SpringBoot的StringUtil或HuTool的StrUtil中拷贝更多的代码过来,美其名曰内部工具类。


public class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}

public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}

// 新增方法:将字符串反转
public static String reverse(String str) {
if (str == null) {
return null;
}
return new StringBuilder(str).reverse().toString();
}

// 新增方法:判断字符串是否为整数
public static boolean isInteger(String str) {
try {
Integer.parseInt(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}
}

6. 添加新的异常类型:


添加更多异常类型,对不同的业务抛出不同的异常,每种异常都要单独去处理


public class CustomException extends RuntimeException {
// 构造函数
public CustomException(String message) {
super(message);
}

// 新增异常类型
public static class NotFoundException extends CustomException {
public NotFoundException(String message) {
super(message);
}
}

public static class ValidationException extends CustomException {
public ValidationException(String message) {
super(message);
}
}
}

// 示例:添加不同类型的异常处理
public class ExceptionHandling {
public void process(int value) {
try {
if (value < 0) {
throw new IllegalArgumentException("Value cannot be negative");
} else if (value == 0) {
throw new ArithmeticException("Value cannot be zero");
} else {
// 正常处理逻辑
}
} catch (IllegalArgumentException e) {
// 异常处理逻辑
} catch (ArithmeticException e) {
// 异常处理逻辑
}
}
}


7. 实现更多设计模式:


在项目中运用更多设计模式,也不失为一种合理的方式,比如单例模式、工厂模式、策略模式、适配器模式等各种常用的设计模式。


比如下面这个单例,大大节省了内存空间,虽然它存在线程不安全等问题。


public class SingletonPattern {
// 单例模式
private static SingletonPattern instance;

private SingletonPattern() {
// 私有构造函数
}

public static SingletonPattern getInstance() {
if (instance == null) {
instance = new SingletonPattern();
}
return instance;
}

}

还有下面这个策略模式,能避免过多的if-else条件判断,降低代码的耦合性,代码的扩展和维护也变得更加容易。


// 策略接口
interface Strategy {
void doOperation(int num1, int num2);
}

// 具体策略实现类
class AdditionStrategy implements Strategy {
@Override
public void doOperation(int num1, int num2) {
int result = num1 + num2;
System.out.println("Addition result: " + result);
}
}

class SubtractionStrategy implements Strategy {
@Override
public void doOperation(int num1, int num2) {
int result = num1 - num2;
System.out.println("Subtraction result: " + result);
}
}

// 上下文类
class Context {
private Strategy strategy;

public Context(Strategy strategy) {
this.strategy = strategy;
}

public void executeStrategy(int num1, int num2) {
strategy.doOperation(num1, num2);
}
}

// 测试类
public class StrategyPattern {
public static void main(String[] args) {
int num1 = 10;
int num2 = 5;

// 使用加法策略
Context context = new Context(new AdditionStrategy());
context.executeStrategy(num1, num2);

// 使用减法策略
context = new Context(new SubtractionStrategy());
context.executeStrategy(num1, num2);
}
}

对比下面这段条件判断,高下立判。


public class Calculator {
public static void main(String[] args) {
int num1 = 10;
int num2 = 5;
String operation = "addition"; // 可以根据业务需求动态设置运算方式

if (operation.equals("addition")) {
int result = num1 + num2;
System.out.println("Addition result: " + result);
} else if (operation.equals("subtraction")) {
int result = num1 - num2;
System.out.println("Subtraction result: " + result);
} else if (operation.equals("multiplication")) {
int result = num1 * num2;
System.out.println("Multiplication result: " + result);
} else if (operation.equals("division")) {
int result = num1 / num2;
System.out.println("Division result: " + result);
} else {
System.out.println("Invalid operation");
}
}
}


8. 扩展注释和文档:


如果要增加代码量,写更多更全面的注释也不失为一种方式。


/**
* 这是一个示例类,用于展示增加代码数量的技巧和示例。
* 该类包含一个示例变量 value 和示例构造函数 ExampleClass(int value)。
* 通过示例方法 getValue() 和 setValue(int newValue),可以获取和设置 value 的值。
* 这些方法用于展示如何增加代码数量,但实际上并未实现实际的业务逻辑。
*/

public class ExampleClass {

// 示例变量
private int value;

/**
* 构造函数
*/

public ExampleClass(int value) {
this.value = value;
}

/**
* 获取示例变量 value 的值。
* @return 示范变量 value 的值
*/

public int getValue() {
return value;
}

/**
* 设置示例变量 value 的值。
* @param newValue 新的值,用于设置 value 的值。
*/

public void setValue(int newValue) {
this.value = newValue;
}
}

结语


哪怕是以代码量算工资,咱也得写出高质量的代码,合理合法合情的赚票子。


giphy (1).gif


作者:一只叫煤球的猫
来源:juejin.cn/post/7263760831052906552
收起阅读 »

看了我项目中的商品功能设计,同事也开始悄悄模仿了...

商品功能作为电商系统的核心功能,它的设计可谓是非常重要的。就算不是电商系统中,只要是涉及到需要交易物品的项目,商品功能都具有很好的参考价值。今天就以mall项目中的商品功能为例,来聊聊商品功能的设计与实现。 mall项目简介 这里还是简单介绍下mall项目吧...
继续阅读 »

商品功能作为电商系统的核心功能,它的设计可谓是非常重要的。就算不是电商系统中,只要是涉及到需要交易物品的项目,商品功能都具有很好的参考价值。今天就以mall项目中的商品功能为例,来聊聊商品功能的设计与实现。



mall项目简介


这里还是简单介绍下mall项目吧,mall项目是一套基于 SpringBoot + Vue + uni-app 的电商系统,目前在Github已有60K的Star,包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员等功能,功能很强大!



功能设计



首先我们来看下mall项目中商品功能的设计,主要包括商品管理、添加\编辑商品、商品分类、商品类型、品牌管理等功能,这里的功能同时涉及前台商城和后台管理系统。



商品管理


在mall项目的后台管理系统中,后台管理员可以对商品进行管理,比如添加、编辑、删除、上架等操作。



当商品上架完成后,前台会员在mall项目的前台商城的商品列表中就可以看到对应商品了。



添加/编辑商品


后台管理员在添加/编辑商品时,需要填写商品信息、商品促销、商品属性以及选择商品关联。



之后前台会员在前台商城的商品详情页中就可以查看到对应的商品信息了。



商品分类


后台管理员也可以对商品的分类进行添加、编辑、删除、查询等操作。



这样前台会员在前台商城中就可以按商品分类来筛选查看商品了。



商品类型


后台管理员可以对商品的类型属性进行设置,设置好之后在编辑商品时就可以进行商品属性、参数的设置了。



此时前台会员就可以在前台商城中选择对应属性的商品进行购买了。



品牌管理


后台管理员可以对商品的品牌进行添加、编辑、删除、查询等操作。



此时前台会员就可以在前台商城的品牌详情页中查看到品牌信息以及相关的商品了。



功能整理


对于商品模块的功能,我这里整理了一张思维导图方便大家查看,主要是整理了下有哪些功能以及功能需要涉及哪些字段。



数据库设计


根据我们的功能设计和整理好的思维导图,就可以进行数据库设计了,这里是mall项目商品模块的功能设计图。



接口设计


对于mall项目中商品模块的接口设计,大家可以参考项目的Swagger接口文档,以Pms开头的接口就是商品模块对应的接口。



总结


商品模块作为电商系统的核心功能,涉及到商品SKU和SPU的概念,是一个非常好的参考案例。如果你能掌握商品模块的设计,对于开发一些需要交易的系统来说,会有非常大的帮助!


项目源码地址


github.com/macroz

heng/…

收起阅读 »

getClass方法详解

getClass方法详解 在Java中,getClass()是Object类的一个方法,用于返回对象的运行时类(Runtime Class)。它的函数签名如下: public final Class<?> getC...
继续阅读 »

getClass方法详解


在Java中,getClass()是Object类的一个方法,用于返回对象的运行时类(Runtime Class)。它的函数签名如下:


public final Class<?> getClass()

getClass()方法返回一个Class对象,该对象表示调用该方法的对象的运行时类型。换句话说,它返回一个描述对象所属类的元数据的实例。


以下是关于getClass()方法的详解:





  1. 返回值类型:getClass()方法返回一个Class<?>类型的对象,这里的问号表示通配符,表示可以是任何类型的Class对象。





  2. 作用:getClass()方法用于获取对象的类信息,包括类的名称、父类、接口信息等。





  3. 运行时类型:getClass()方法返回的是调用对象的运行时类型,而不是对象的声明类型。也就是说,如果对象的类型发生了变化(向上转型或者子类重写父类方法),getClass()返回的是实际运行时类型。





  4. 示例代码:


    class Animal {
        // ...
    }

    class Dog extends Animal {
        // ...
    }

    public class Main {
        public static void main(String[] args) {
            Animal animal = new Dog();
            Class<?> clazz = animal.getClass();
            System.out.println(clazz.getName()); // 输出: Dog
        }
    }

    在上面的示例中,getClass()方法被调用时,对象animal的运行时类型是Dog,因此返回的Class对象代表Dog类。




需要注意的是,getClass()方法是继承自Object类的,因此可以在任何Java对象上调用。但是,在使用getClass()方法之前,必须确保对象不为null,否则会抛出NullPointerException异常。


getClass()方法与反射密切相关,是反射的基础之一。


在Java中,反射是指在运行时动态地获取类的信息并操作类或对象的能力。它允许程序在运行时检查和修改类、方法、字段等的属性和行为,而不需要在编译时确定这些信息。


通过调用对象的getClass()方法,我们可以获得对象的运行时类型的Class对象。然后,使用Class对象可以进行以下反射操作:





  1. 实例化对象:通过Class.newInstance()方法可以实例化一个类的对象。





  2. 获取类的构造函数:通过Class.getConstructors()方法可以获取类的所有公共构造函数,通过Class.getDeclaredConstructors()方法可以获取所有构造函数(包括私有构造函数),还可以通过参数类型匹配获取指定的构造函数。





  3. 获取类的方法:通过Class.getMethods()方法可以获取类的所有公共方法,通过Class.getDeclaredMethods()方法可以获取所有方法(包括私有方法),还可以通过方法名和参数类型匹配获取指定的方法。





  4. 获取类的字段:通过Class.getFields()方法可以获取类的所有公共字段,通过Class.getDeclaredFields()方法可以获取所有字段(包括私有字段),还可以通过字段名匹配获取指定的字段。





  5. 调用方法和访问字段:通过Method.invoke()方法可以调用方法,通过Field.get()Field.set()方法可以访问字段。




总结来说,getClass()方法提供了从对象到其运行时类型的连接,而反射则利用这个连接来获取和操作类的信息。通过反射,我们可以在运行时动态地使用类的成员,实现灵活的代码编写和执行。


作者:维维
来源:mdnice.com/writing/c1e0400e54e94e4881aacdfc5bb10508
收起阅读 »

前端异步请求轮询方案

业务背景 在前后端数据交互场景下,使用最多的一种方式是客户端发起 HTTP 请求,等待服务端处理完成后响应给客户端结果。 但在一些场景下,服务端对数据的处理需要较长的时间,比如提交一批数据,对这批数据进行数据分析,将最终分析结果返回给前端。 如果采用一次 HT...
继续阅读 »

业务背景


在前后端数据交互场景下,使用最多的一种方式是客户端发起 HTTP 请求,等待服务端处理完成后响应给客户端结果。


但在一些场景下,服务端对数据的处理需要较长的时间,比如提交一批数据,对这批数据进行数据分析,将最终分析结果返回给前端。


如果采用一次 HTTP 请求,用户会一直处于等待状态,再加上界面不会有进度交互,导致用户不知何时会处理完成;此外,一旦刷新页面或者其他意外情况,用户就无从感知处理结果。


面对这类场景,可以借助 「HTTP 轮询方式」 对交互体验进行优化,具体过程如下:


首先发起一次 HTTP 请求用于提交数据,之后启动轮询在一定间隔时间内查询分析结果,在这期间后台可将分析进度同步到前端来告知用户处理进度;此外即使刷新再次进入页面还可以通过「轮询」实时查询进度结果。


下面,我们来看看代码层面看如何实现这类场景。


JS 实现轮询的方式


在实现代码之前,我们需要先明确 JS 实现轮询的方式有哪些,哪种方式最适合使用。


1. setInterval


作为前端开发人员,提起轮询第一时间能想到的是计时器 setInterval,它会按照指定的时间间隔不间断的轮询执行处理函数。


let index = 1;

setInterval(() => {
console.log('轮询执行: ', index ++);
}, 1000);

回过头来看我们的场景:要轮询的是 异步请求(HTTP),请求响应结果会受限制网络或者服务器处理速度,显然 setInterval 这种固定间隔轮询并不适合这个场景。


2. Promise + setTimeout sleep


setInterval 的不足之处在于 轮询间隔时间 在异步请求场景下无法保证两个请求之间的间隔固定。要解决这个问题,可以使用 sleep 睡眠函数来控制间隔时间。


JS 中没有提供 sleep 相关方法,但可以结合 Promise + setTimeout 来实现。


const sleep = () => {
return new Promise(resolve => {
setTimeout(resolve, 1000);
});
}

sleep 仅控制了轮询间隔,而轮询的执行机制需要我们手动根据异步请求结果来实现,比如下面通过控制 while 循环的条件:


const start = async () => {
let i = 0;
while (i < 5) {
await sleep();
console.log(`第 ${++ i} 次执行`);
}
}

start();


使用轮询的时候可以借助 async/await 同步的方式编写,提高代码阅读质量。



实现异步请求轮询


下面我们通过一个完整示例理解 轮询异步请求 的实现及使用注意事项。


首先我们定义两个变量:index 用于控制何时停止轮询,timer 则用于实现中断轮询。


let index = 1;
let timer = 0;

这里,我们定义 syncPromise 来模拟异步请求,可以看作是一次 HTTP 请求,当进行 5 次异步请求后,会返回 false 表示拿到数据分析结果,停止数据查询轮询:


const syncPromise = () => {
return new Promise(resolve => {
setTimeout(() => {
console.log(`第 ${index} 次请求`);
resolve(index < 5 ? true : false);
index ++;
}, 50);
})
}

现在,我们实现 pollingPromise 作为 sleep 睡眠函数使用,去控制轮询的间隔时间,并在指定时间执行异步请求:


const pollingPromise = () => {
return new Promise(resolve => {
timer = setTimeout(async () => {
const result = await syncPromise();
resolve(result);
}, 1000);
});
}

最后,startPolling 作为开始轮询的入口,包含以下逻辑:



  • 1)在轮询前会清除正在进行的轮询任务,避免出现多次轮询;

  • 2)如果需要,在开始轮询时会立刻调用异步请求查询一次数据结果;

  • 3)最后,通过 while 循环根据异步请求的结果,决定是否继续轮询;


const startPolling = async () => {
// 清除进行中的轮询,重新开启计时轮询
clearTimeout(timer); // !!! 注意:清除计时器后,会导致整个 async/await 链路中断,若计时器的位置下方还存在代码,将不会执行。
index = 1;
// 立刻执行一次异步请求
let needPolling = await syncPromise();
// 根据异步请求结果,判断是否需要开启计时轮询
while (needPolling) {
needPolling = await pollingPromise();
}
console.log('轮询请求处理完成!'); // 若异步请求被 clearTimeout(timer),这里不会被执行打印输出。
}

const start = async () => {
await startPolling();
console.log('若异步请求被 clearTimeout(timer),这里将不会被执行');
}
start();

不过,需要注意的是:一旦清除计时器后,会导致整个 async/await 链路中断,若计时器的位置下方还存在代码,将不会执行。


假设当前执行了两次轮询被 clearTimeout(timer) 后,从 startPollingstart 整个 async/await 链路都会中断,且后面未执行的代码也不会被执行。


基于以上规则,异步轮询的处理逻辑尽量放在 syncPromise 异步请求核心函数中完成,避免在开启轮询

作者:明里人
来源:juejin.cn/post/7262261749105639481
的辅助函数中去实现。

收起阅读 »

hive宽表窄表互转

hive宽表窄表互转 背景 在工作中经常会遇到高表转宽表,宽表转窄表的场景,在此做一些梳理。 宽表转窄表 传统思路 使用sql代码作分析的时候,几次遇到需要将长格式数据转换成宽格式数据,一般使用left join或者case when实现,代码...
继续阅读 »

hive宽表窄表互转


背景


在工作中经常会遇到高表转宽表,宽表转窄表的场景,在此做一些梳理。


宽表转窄表


传统思路



使用sql代码作分析的时候,几次遇到需要将长格式数据转换成宽格式数据,一般使用left join或者case when实现,代码看起来冗长,探索一下,可以使用更简单的方式实现长格式数据转换成宽格式数据。



select year,
max(case when month=1 then money else 0 endas M1,
max(case when month=2 then money else 0 endas M2,
max(case when month=3 then money else 0 endas M3,
max(case when month=4 then money else 0 endas M4 
from sale group by year;

需求描述


某电商数据库中存在一张客户信息表user_info,记录着客户属性数据和消费数据,需要将左边长格式数据转化成右边宽格式数据。 需求实现



涉及函数: str_to_map, concat_ws, collect_set, sort_array




实现思路: 步骤一:将客户信息转化成map格式的数据。 collect_set形成的集合是无序的,若想得到有序集合,可以使用sort_array对集合元素进行排序。 步骤二:将map格式数据中的key与value提取出来,key就是每一列变量名,value就是变量值



select 
    user_no,
    message1['name'name,
    message1['sex'] sex,
    message1['age'] age,
    message1['education'] education,
    message1['regtime'] regtime,
    message1['first_buytime'] first_buytime
from 
  (select
      user_no,
      str_to_map(concat_ws(',',sort_array(collect_set(concat_ws(':', message, detail))))) message1
      from user_info
      group by user_no
      order by user_no
   ) a


窄表转宽表


长宽格式数据之间相互转换使用到的函数,可以叫做表格生成函数


需求描述


某电商数据库中存在表user_info1,以宽格式数据记录着客户属性数据和消费数据,需要将左边user_info1宽格式数据转化成右边长格式数据。 需求实现



步骤一:将宽格式客户信息转化成map格式的数据。 步骤二:使用explode函数将 map格式数据中的元素拆分成多行显示



select user_no, explode(message1)
    from 
    (select user_no, 
        map('name',name'sex',sex, 'age',age, 'education',education, 'regtime',regtime, 'first_buytime',first_buytime) message1
        from user_info1
    ) a

总结



不管是将长格式数据转换成宽格式数据还是将宽格式数据转换成长格式数据,都是先将数据转换成map格式数据。长格式数据转换成宽格式数据:先将长格式数据转换成map格式数据,然后使用列名['key']得到每一个key的value;宽格式数据转换成长格式数据:先将宽格式数据转换成map格式数据,然后使用explode函数将 map格式数据中的元素拆分成多行显示。顺便说一句,R语言中也是通过类似的方法实现长宽格式之间相互转换的。



作者:大数据启示录
来源:mdnice.com/writing/cfacb28094f643d5970e425fc6130980
收起阅读 »

看完这篇,SpringBoot再也不用写try/catch了

前言 使用 SpringBoot 开发 Web 应用时,异常处理是必不可少的一部分。在应用中,异常可能会出现在任何地方,例如在控制器、服务层、数据访问层等等。如果不对异常进行处理,可能会导致应用崩溃或者出现未知的错误。因此,对于异常的处理是非常重要的。 ...
继续阅读 »

前言


使用 SpringBoot 开发 Web 应用时,异常处理是必不可少的一部分。在应用中,异常可能会出现在任何地方,例如在控制器、服务层、数据访问层等等。如果不对异常进行处理,可能会导致应用崩溃或者出现未知的错误。因此,对于异常的处理是非常重要的。


本篇主要讲述在SpringBoot 中,如何用全局异常处理优雅的处理异常。


为什么要优雅的处理异常


如果我们不统一的处理异常,开发人员经常会在代码中东一块的西一块的写上 try catch代码块,长久以往容易堆积成屎山。


@Slf4j
@Api(value = "User Interfaces", tags = "User Interfaces")
@RestController
@RequestMapping("/user")
public class UserController {
    /**
     * @param userParam user param
     * @return user
     */

    @ApiOperation("Add User")
    @ApiImplicitParam(name = "userParam", type = "body", dataTypeClass = UserParam.classrequired true)
    @PostMapping("add")
    public ResponseEntity add(@Valid @RequestBody UserParam userParam) {
        // 每个接口都需要手动try catch
        try {
            // do something
        } catch(Exception e) {
            return ResponseEntity.fail("error");
        }
        return ResponseEntity.ok("success");
    }
}

那我们应该如何实现统一的异常处理呢?


使用 @ControllerAdvice + @ExceptionHandler注解



@ControllerAdvice 定义该类为全局异常处理类


@ExceptionHandler 定义该方法为异常处理方法。value 的值为需要处理的异常类的 class 文件。



首先自定义异常类 BusinessException :


/**
 * 业务异常类
 * @author rango
 */

@Data
public class BusinessException extends RuntimeException {
    private String code;
    private String msg;
 
    public BusinessException(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

然后编写全局异常类,用 @ControllerAdvice 注解:


/**
 * 全局异常处理器
 * @author rango
 */

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    /**
     * 处理 Exception 异常
     * @param httpServletRequest httpServletRequest
     * @param e 捕获异常
     * @return
     */

    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public ResponseEntity exceptionHandler(HttpServletRequest httpServletRequestException e
{
        logger.error("服务错误:", e);
        return new ResponseEntity("******""服务出错");
    }
 
    /**
     * 处理 BusinessException 异常
     * @param httpServletRequest httpServletRequest
     * @param e 捕获异常
     * @return
     */

    @ResponseBody
    @ExceptionHandler(value = BusinessException.class)
    public ResponseEntity businessExceptionHandler(HttpServletRequest httpServletRequestBusinessException e
{
        logger.info("业务异常报错!code:" + e.getCode() + "msg:" + e.getMsg());
        return new ResponseEntity(e.getCode(), e.getMsg());
    }
}


定义了全局异常处理器,项目就可以对不同的异常进行统一处理了。通常,为了使 controller 中不再使用任何 try/catch,会在 GlobalExceptionHandler 中对 Exception 做统一的拦截处理。这样其他没有用 @ExceptionHandler 配置的异常就都会统一被处理。


遇到异常时主动抛出异常


在业务中,遇到业务异常的地方,我们直接 throw 抛出对应的业务异常即可。如下所示


throw new BusinessException(ERROR_CODE, "用户账号/密码有误");

在 Controller 中的写法


Controller 中,不需要再写 try/catch,除非特殊场景。


@RequestMapping(value = "/test")
public ResponseEntity test() {
    ResponseEntity re = new ResponseEntity();
    // 业务处理
    return re;
}

结果展示


异常抛出后,返回如下结果。


{
    "code""E0014",
    "msg""用户账号/密码有误",
    "data"null
}

注意!!!



  • 抛出的异常如果被代码内的 try/catch 捕获了,就不会被 GlobalExceptionHandler 处理



  • 异步方法中的异常不会被全局异常处理(多线程)



  • 不是 controller 层抛出的异常才能被 GlobalExceptionHandler 处理,只要异常最后是从 contoller 层抛出去的都可以被捕获并处理

总结


本文介绍了使用 SpringBoot 时,如何通过配置全局异常处理器统一处理项目中的一些通用的异常,避免程序员不断的写try/catch导致的代码冗余,有利于代码的维护。


作者:程序员典籍
来源:mdnice.com/writing/103055f00ba04cf4b06f0195f839a449
收起阅读 »

千万级高可用分布式对账系统设计实践

背景         目前线上业务量与日俱增,每日的订单量超过千万,资金流动大,资金安全成为了重点关注的问题。为了确保每一笔交易的正确性,提高资金的正确性和保障业务的利益,除了RD代码逻辑严格以外,还需要对每日甚至每小时订单的流水进行核对,对异常情况能及时处理...
继续阅读 »

背景


        目前线上业务量与日俱增,每日的订单量超过千万,资金流动大,资金安全成为了重点关注的问题。为了确保每一笔交易的正确性,提高资金的正确性和保障业务的利益,除了RD代码逻辑严格以外,还需要对每日甚至每小时订单的流水进行核对,对异常情况能及时处理。面对千万级的订单量,人工对账肯定是不可行的,所以,实现一套对账系统成为了必然的事,不仅为资金安全提供依据,也节省公司运维人力,数据更加可视化。目前这套系统已覆盖聚合渠道网关与外部渠道100%的对账业务,完成春晚期间支付宝亿级订单量对账,完成日常AC项目千万级订单量对账,对账准确率实现6个9,为公司节省2~3个人力。


介绍


        对账模块是支付系统的核心功能之一,不同业务设计的对账模型不同,但是都会遇到以下几个问题:



  • 海量的数据,就目前聚合支付的订单量来看,设计的对账系统需要应对千万级的数据量;

  • 面对日切、多账、少账等异常差异订单应该如何处理;

  • 账单格式、下载账单时间、下载方式等不一致问题。


        针对以上问题,并结合财经聚合支付系统的特点,本文将设计一套可以应对千万级数据量、分布式和高可用的对账系统,利用消息队列Kafka的解耦性解决对账系统各模块之间的强依赖性。文章从三个方面介绍对账系统,第一方面,总体介绍对账系统的设计,依次介绍各个模块的实现及其过程中使用到的设计模式;第二方面,介绍对账系统版本迭代的过程,为什么需要进行版本迭代,以及版本迭代过程中踩过的“坑”;第三方面,总结现有版本的特点并提出下一步的优化思路。


系统设计


系统结构图


        图1为对账系统总结构图,分为六个模块,分别是文件下载模块、文件解析并推送模块、平台数据获取并推送模块、执行对账模块、对账结果统计模块和中间态模块,每个模块负责自己的职能。
对账系统总结构图


图1 对账系统总结构图


        图2为对账系统利用Kafka实现的状态转换图。每个模块独立存在,彼此之间通过消息中间件Kafka实现系统状态转换,通过中间态UpdateReconStatus类实现状态更新和message发送。这种设计不仅实现流水线对账,也利用消息中间件的特点,实现重试和模块之间的解耦。

对账系统状态转换图.png


图2 对账系统状态转换图


        为了更好的了解每个模块的实现过程,下面将依次对各个模块进行说明。

文件下载模块


设计

        文件下载模块主要完成各个外部渠道账单的下载功能。众所周知,聚合支付是聚众家三方机构能力为一体的支付方式,其中三方机构包括支付宝、微信等支付界的领头羊,多样性的支付渠道导致账单下载存在多样性,如何实现多模式、可拔插的文件下载能力成为该模块设计的重点。分析Java设计模式的特点,本模块采用接口模式,符合面向对象的设计理念,可实现快速接入。具体实现类图如图3所示(只展示部分类图)。


图3 文件下载实现类图


        下面就以支付宝对账文件下载方式为例,具体阐述一下实现过程。


实现

        分析支付宝接口文档,目前采用的下载方式为HTTPS,文件格式为.csv的压缩包。根据这些条件,本系统的实现方式如下(只摘取了部分代码)。由于消息中间件Kafka和中间态模块的机制,已经从系统层面考虑了重试的能力,因此不需要考虑重试机制,后续模块也如此。


public interface BillFetcher {
// ReconTaskMessage 为kafka消息,
// FetcherConsumer为自定义账单下载后的处理方式
String[] fetch(ReconTaskMessage message,FetcherConsumer consumer) throws IOException;
}

@Component
public class AlipayFetcher implements BillFetcher {

public AlipayFetcher(@Autowired BillDownloadService billDownloadService) {
Security.addProvider(new BouncyCastleProvider());
billDownloadService.register(BillFetchWay.ALIPAY, this);
}
...
@Override
public String[] fetch(ReconTaskMessage message, FetcherConsumer consumer) throws IOException {
String appId = map.getString("appId");
String privateKey = getConvertedPrivateKey(map.getString("privateKey"));
String alipayPublicKey = getPublicKey(map.getString("publicKey"), appId);
String signType = map.getString("signType");
String url = "https://openapi.alipay.com/gateway.do";
String format = "json";
String charset = "utf-8";
String billDate = DateFormatUtils.format(message.getBillDate(), DateTimeConstants.BILL_DATE_PATTERN);
String notExists = "isp.bill_not_exist";
String fileContentType = "application/oct-stream";
String contentTypeAttr = "Content-Type";
//实例化客户端
AlipayClient alipayClient = new DefaultAlipayClient(url, appId, privateKey, format, charset, alipayPublicKey, signType);
//实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称
AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
// trade指商户基于支付宝交易收单的业务账单
// signcustomer是指基于商户支付宝余额收入及支出等资金变动的帐务账单
request.setBizContent("{" +
""bill_type":"trade"," +
""bill_date":"" + billDate + """ +
" }");
AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
//do 根据下载地址获取对账文件,通过流式方式将文件放到指定的目录下
...
System.out.println("调用成功");
} else {
System.out.println("调用失败");
}
}
}

具体步骤:



  1. 重写构造方法,将实现类注入到一个map中,根据对应的下载方式获取实现类;

  2. 实现fetch接口,包括构造请求参数、请求支付宝、解析响应结果、采用流式将文件放入对应的目录下,以及这个过程中的异常处理。


文件解析并推送模块


设计

        前面提到,聚合支付是面对不同的外部渠道,对账文件的多样性不言而喻。比如微信是采用txt格式,支付宝采用csv格式等等,而且各个渠道的账单内容也是不一致的。如何解决渠道之间账单的差异性成为该模板需要重点考虑的问题。通过调研和现有对账系统的分析,本系统采用接口模式+RDF(结构化文本文件)的实现方式,其中接口模式解决账单多模式的问题,同时也实现可拔插的机制,RDF工具组件实现账单的快速标准化,操作简单易会。具体实现类图如图4所示(只展示部分类图)。


图4 文件标准化实现类图


        下面就以支付宝对账文件解析为例,具体阐述一下实现过程。
实现

        根据支付宝的账单格式,提前定义RDF标准模板,后续账单解析将根据模板将每一行对账文件解析为对应的一个实体类,其中需要注意标准模板的字段必须要和账单数据一一对应,实体类的字段可以多于账单字段,但必须包括所有的账单字段。接口定义如下:


public interface BillConverter<T> {
//账单是否可以使用匹配器
boolean match(String channelType, String name);
//转换原始对账文件到Hive
void convertBill(InputStream sourceFile, ConverterConsumer<T> consumer) throws IOException;
//转换原始对账文件到Hive
void convertBill(String localPath, ConverterConsumer<T> consumer) throws IOException;
}

具体实现步骤如图5所示:


流程图.png


图5 文件解析流程图



  1. 定义RDF标准模板,如下为支付宝业务流水明细模板,其中body结构内字段名必须和实体类名保持一致。


{
"head": [
"title|支付宝业务明细查询|Required",
"merchantId|账号|Required",
"billDate|起始日期|Required",
"line|业务明细列表|Required",
"header|header|Required"
],
"body": [
"channelNo|支付宝交易号",
"merchantNo|商户订单号",
"businessType|业务类型",
"production|商品名称",
"createTime|创建时间|Date:yyyy-MM-dd HH:mm:ss",
"finishTime|完成时间|Date:yyyy-MM-dd HH:mm:ss",
"storeNo|门店编号",
"storeName|门店名称",
"operator|操作员",
"terminalNo|终端号",
"account|对方账户",
"orderAmount|订单金额|BigDecimal",
"actualReceipt|商家实收|BigDecimal",
"alipayRedPacket|支付宝红包|BigDecimal",
"jiFenBao|集分宝|BigDecimal",
"alipayPreferential|支付宝优惠|BigDecimal",
"merchantPreferential|商家优惠|BigDecimal",
"cancelAfterVerificationAmount|券核销金额|BigDecimal",
"ticketName|券名称",
"merchantRedPacket|商家红包消费金额|BigDecimal",
"cardAmount|卡消费金额|BigDecimal",
"refundOrRequestNo|退款批次号/请求号",
"fee|服务费|BigDecimal",
"feeSplitting|分润|BigDecimal",
"remark|备注",
"merchantIdNo|商户识别号"
],
"tail": [
"line|业务明细列表结束|Required",
"tradeSummary|交易合计|Required",
"refundSummary|退款合计|Required",
"exportTime|导出时间|Required"
],
"protocol": "alib",
"columnSplit":","
}


  1. 实现接口的getChannelType、match方法,这两个方法用于匹配具体使用哪一个Convert类。如匹配支付宝账单,实现方式为:


@Override
public String getChannelType() {
return ChannelType.ALI.name();
}
@Override
public boolean match(String channelType, String name) {
return name.endsWith(".csv.zip");
}


  1. 实现接口的convertBill方法,完成账单标准化;


@Override
public void convertBill(String path, ConverterConsumer<ChannelBillPojo> consumer) throws IOException
{
FileConfig config = new FileConfig(path, "rdf/alipay-business.json", new StorageConfig("nas"));
config.setFileEncoding("UTF-8");
FileReader fileReader = FileFactory.createReader(config);
AlipayBusinessConverter.AlipayBusinessPojo row;
try {
while (null != (row = fileReader.readRow(AlipayBusinessConverter.AlipayBusinessPojo.class))) {
convert(row, consumer);
}
...
}


  1. 将标准化账单推送至Hive


平台数据获取并推送模块


        平台数据获取一般都是从数据库中获取,数据量小的时候,查询时数据库的压力不会很大,但是数据量很大时,如电商交易,每天成交量在100万以上,通过数据库查询是不可取的,不仅效率低,而且容易导致数据库崩溃,影响线上交易,这点会在后续的版本迭代中体现。因此,平台数据的抽取是从Hive上获取,只需要提前将交易数据同步到Hive表中即可,这样做不仅效率高,而且更加安全。考虑到抽取的Hive表不同、数据的表结构,数据收集器Collector类也采用了接口模式。Collector接口定义如下:


public interface DataCollector {
void collect(OutputStream os) throws IOException;
}

        根据目前平台数据收集器实现情况,可以得到类图如图6所示。


图6 平台数据收集器实现类图


执行对账模块


        该模块主要完成Hive命令的执行,在平台账单和渠道账单已全部推送至Hive的前提下,利用Hive处理大数据效率高的特点,执行全连接sql,并将结果存入指定的Hive表中,用于对账结果统计。执行对账sql可以根据业务需求而定,如需要了解本系统的全连接sql,欢迎与我交流。


对账结果统计模块


        对账任务执行成功之后,需要统计全连接后的数据,重点统计金额不一致、状态不一致、日切、少账(平台无账,渠道有账)和多账(平台有账,渠道无账)等差异。针对不同的情况,本系统分别采用如下的解决方案:



  1. 金额不一致:前端页面展示差异原因,人工进行核对;

  2. 状态不一致:针对退款订单,查询平台退款表,存在且金额一致认为已对平,不展示差异,其他情况,需要在前端页面展示差异原因,人工进行核对;

  3. 日切:当平台订单为成功,渠道无单时,根据平台订单创建时间判断是否可能存在日切,如果判断是日切订单,会将这笔订单存入buffer文件中,待统计结束后,将buffer文件上传至Hive日切表中,等第二天重新加载这部分数据实现跨日对账。对于平台无订单,渠道有单的情况,通过查询平台数据库判断是否存在差异,如果存在差异,需要在前端页面展示差异,人工进行核对。

  4. 少账:目前主要通过查询平台数据库判断是否存在差异,确认确实存在差异时,需要在前端页面展示差异,人工进行核对。

  5. 多账:目前这种有可能是日切,会先考虑日切,如果不在日切范围内,需要在前端页面展示差异,人工进行核对。


中间态模块


        中间态模块是用于各模块之间状态转换的模块,利用Kafka和状态是否更新的机制,实现消息的重发和对账状态的更新。从一个状态到下一个状态,必须满足当前状态为成功,对账流程才会往下一步执行。中间态的设计不仅解决了重试问题,而且将数据库的操作进行了收敛,更符合模块化的设计,各个模块各司其职。重试次数也不是无限的,目前设置的重试次数为3次,如果3次重试后依然没有成功,会发lark通知,人工介入解决。


        总之,对账工作,既复杂也不复杂,需要我们细心,对业务要有深入的了解,并选择合适的处理方式,针对不同的业务,不断迭代优化系统。


版本迭代


        系统的设计很大程度受业务规模的影响,对于财经聚合支付而言,订单量发生了几个数量级的变化,这个过程中不断暴露出对账系统存在的问题,优化改进对账系统是必然的事。从系统设计到目前大致可以分为三个阶段:初始阶段、过渡阶段和当前阶段。


初始版(v1.0)

        初始版上线后实现了聚合渠道对账的自动化,尤其在2018年的春节活动中,资金安全提供了重要的保障,实现了聚合和老合众、支付宝、微信等渠道的对账。随着财经业务的发展,抖音电商的快速崛起,对账系统逐渐暴露出不足,比如对账任务失败增多,尤其是数据量大的对账、非正常差异结果展示、对账效率低等问题。通过不断分析,发现存在以下几个问题:



  1. 系统的文件都是放在临时目录tmp下的,TCE平台会对这个目录下的文件定时清理,导致推送文件到Hive时会报找不到文件的情况,尤其是大数据量的对账任务;

  2. Kafka消息积累多,导致对账流程中断,主要是新增渠道,对账任务增加,同时Hive执行队列是共享队列,大部分的对账流程因为没有资源而卡住;

  3. 非正常差异结果展示,主要是查单没有增加重试机制,当查询过程中出现超时等异常,会出现非正常差异结果,还有部分原因是日切跨度小而导致的非正常差异结果。


过渡版(v2.0)

        考虑到初始版对账系统存在的不足和对账功能的急迫性,对初始版进行过渡性的优化,初步实现大数据量的对账功能,同时也提高了差异结果的准确率。相比初始版,该版本主要进行了以下几点优化:



  1. 文件存放目录由临时目前改为服务下的某一个目录,防止大文件被回收,文件上传到Hive后删除文件;

  2. 重新申请独占的执行队列,解决资源不足导致对账流程卡住的问题;

  3. 查单新增重试机制,日切跨度增大,解决非正常差异结果展示,提供差异结果的准确率。


        过渡版集中解决初始版明显存在的问题,对于一些潜在的问题并没有彻底解决,如代码容错率低、对账任务异常后人工响应慢、对账效率低、数据库安全性低等问题。


当前版(v3.0)

        当前版优化的宗旨是实现对账系统的"三高",分别为高效率、高准确率(6个9)和高稳定性。


        对于高效率,主要体现在平台数据获取慢,而且存在数据库安全问题,针对这块逻辑进行了优化,改变数据获取途径,由原来的数据库获取改为从高效率的Hive中获取,只需要提前将数据同步到Hive表中即可。


        对于高准确率,主要优化对账差异处理逻辑,进一步细化差异处理方式,新增差异结果报警,细化前端页面差异原因。


        对于高稳定性,主要优化RDF处理对账文件发生异常时新增兜底逻辑,提高系统的容错性;对账任务失败或超过指定重试阈值时增加报警,加快人工响应速率;对查单等操作数据库逻辑增加限流,防止数据库崩溃。


        版本迭代过程可以总结如下,希望读者别重复入坑,尤其是大文件处理方面。


业务情况优点存在的问题目标
初始版(v1.0)财经部门初期,订单量少,业务结构简单实现少量交易量对账;支持分布式效率低;对账任务容易卡住;非异常case普遍;大数据基本不能完成对账保障资金安全问题,实现聚合渠道网关与外部渠道的对账功能
过渡版(v2.0)电商业务崛起,订单量增加,业务种类增多实现海量数据对账;查单新增重试机制;降低非异常case数量影响数据库安全性;代码容错率低;对账效率低;对账任务异常时人工响应慢支持千万级订单量对账
当前版(v3.0)优化过渡版遗漏问题,改变数据获取路径效率大大提升;实现千万级数据量对账;实现高稳定性,高准确率,高效率全连接效率低;不支持订单状态推进实现对账系统的高效率,准确率实现6个9;功能全面

总结


        对账系统模型与业务息息相关,业务不同,对账系统模型也会不同,但是大部分对账系统的整体架构变化不大,主要区别是各个模块的实现方式不同。希望本文介绍的对账系统能为各位读者提供设计思路,避免重复入坑。对对账系统感兴趣的同学可以找财经支付团队同学详聊,一起深入探讨,提出优化建议,比如优化全连接策略,也欢迎各种简历推荐。


参考文章


信息流对账与平台化实现-曾佳


混合编程在财经对账中的应用-王亚宁


内推链接


image.png

收起阅读 »

第三方认证中心跳转

一、业务需求 由第三方认证中心将 token 放在 header 中跳转系统,前端获取到第三方系统携带 header 中的 token。 二、 业务流程 模拟第三方应用 CUSTOM-USERTOKEN 是第三方的 tok...
继续阅读 »

一、业务需求


由第三方认证中心将 token 放在 header 中跳转系统,前端获取到第三方系统携带 header 中的 token。


二、 业务流程





模拟第三方应用





  • CUSTOM-USERTOKEN 是第三方的 token



  • proxy_pass 是我们的前端地址


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1;
}
}

前端静态代理





  • backend 是后端服务地址



  • 80 是前端代理端口


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 405 =200 $uri;
}

三、处理方式


由于放在 header 中的内容,前端只有从 XHR 请求中才能拿到,所以直接打开页面时,肯定是无法拿到 header 中的 token 的,又因为这个 token 只有从第三方系统中跳转才能携带,所以也无法通过请求当前页面去获取 header 中的内容。


一、通过后端重定向


在 nginx 代理中,第三方请求从原本跳转访问前端的地址==改为==后端地址, 因为后端是可以从请求总直接拿到 header,所以这时由后端去处理 token ,在重定向到前端。





  • 后端可以设置 cookie,前端从 cookie 中获取



  • 后端可以拼接 URL, 前端从 url 中获取



  • 后端可以通过缓存 cookie, 重定向到前端后发请求获取 token


模拟第三方应用





  • 第三方应用由跳转前端改为跳转后端接口


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://backend/token;
}
}

前端静态代理





  • 前端代理不需要做任何处理


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 405 =200 $uri;
}

二、通过 nginx 重定向 URL


在 nginx 代理中,新增一个 /token 的代理地址,用于转发地址,第三方请求从原本跳转访问前端的地址,改为 /token 代理地址 因为 nginx 中是可以获取 header 中的内容的,所以这时由 /token 处理拼接好 url ,在重定向到前端。





模拟第三方应用



  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1/token;
}
}

前端静态代理





  • 新增 /token 代理,进行拼接 URL 后跳转


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
location /token {
# 将 $http_custom_usertoken 拼接在 URL 中,同时重定向到前端
# 前端通过 location.search 处理 token
rewrite (.+) http://127.0.0.1?token=$http_custom_usertoken;
}
error_page 405 =200 $uri;
}

三、通过 nginx 设置 Cookie


由于通过响应头中设置 Set-Cookie 可以直接存储到浏览器中,所以我们也可以通过直接设置 cookie 的方式处理。





模拟第三方应用





  • 此时第三方应用直接访问前端即可


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1;
}
}

前端静态代理





  • token 设置在 cookie


  server {
listen 80;
server_name localhost;

location / {
add_header Set-Cookie "token=$http_custom_usertoken;HttpOnly;Secure";
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 405 =200 $uri;
}

四、nginx 代理转发设置 Cookie


方法 三、通过 nginx 设置 Cookie 中,存在一个问题,由于此时在前端静态代理上添加 cookie,这就会导致所有静态资源都会携带 cookie, 这就会造成 cookie 中因为 path 不同而重复添加, 所以我们还可以通过造一层代理的方式处理这个问题





模拟第三方应用





  • 代理地址再次修改为 token


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1/token;
}
}

前端静态代理





  • token 设置在 /token 代理地址的 cookie



  • /token 重定向到前端地址


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}

location /token {
add_header Set-Cookie "token=$http_custom_usertoken;HttpOnly;Secure";
rewrite (.+) http://127.0.0.1;
}
error_page 405 =200 $uri;
}

作者:子洋
来源:mdnice.com/writing/d92f346cc96a43b49fc36c9894add729
收起阅读 »

微服务的各种边界在架构演进中的作用

演进式架构 在微服务设计和实施的过程中,很多人认为:“将单体拆分成多少个微服务,是微服务的设计重点。”可事实真的是这样吗?其实并非如此! Martin Fowler 在提出微服务时,他提到了微服务的一个重要特征——演进式架构。那什么是演进式架构呢?演进式...
继续阅读 »

演进式架构


在微服务设计和实施的过程中,很多人认为:“将单体拆分成多少个微服务,是微服务的设计重点。”可事实真的是这样吗?其实并非如此!


Martin Fowler 在提出微服务时,他提到了微服务的一个重要特征——演进式架构。那什么是演进式架构呢?演进式架构就是以支持增量的、非破坏的变更作为第一原则,同时支持在应用程序结构层面的多维度变化。


那如何判断微服务设计是否合理呢?其实很简单,只需要看它是否满足这样的情形就可以了:随着业务的发展或需求的变更,在不断重新拆分或者组合成新的微服务的过程中,不会大幅增加软件开发和维护的成本,并且这个架构演进的过程是非常轻松、简单的。


这也是微服务设计的重点,就是看微服务设计是否能够支持架构长期、轻松的演进。


那用DDD方法设计的微服务,不仅可以通过限界上下文和聚合实现微服务内外的解耦,同时也可以很容易地实现业务功能积木式模块的重组和更新,从而实现架构演进。


微服务还是小单体?


有些项目团队在将集中式单体应用拆分为微服务时,首先进行的往往不是建立领域模型,而只是按照业务功能将原来单体应用的一个软件包拆分成多个所谓的“微服务”软件包,而这些“微服务”内的代码仍然是集中式三层架构的模式,“微服务”内的代码高度耦合,逻辑边界不清晰,这里我们暂且称它为“小单体微服务”。


下面这张图也很好地展示了这个过程。





而随着新需求的提出和业务的发展,这些小单体微服务会慢慢膨胀起来。当有一天你发现这些膨胀了的微服务,有一部分业务功能需要拆分出去,或者部分功能需要与其它微服务进行重组时,你会发现原来这些看似清晰的微服务,不知不觉已经摇身一变,变成了臃肿油腻的大单体了,而这个大单体内的代码依然是高度耦合且边界不清的。


“辛辛苦苦好多年,一夜回到解放前啊!”这个时候你就需要一遍又一遍地重复着从大单体向单体微服务重构的过程。想想,这个代价是不是有点高了呢?


其实这个问题已经很明显了,那就是边界。


这种单体式微服务只定义了一个维度的边界,也就是微服务之间的物理边界,本质上还是单体架构模式。微服务设计时要考虑的不仅仅只有这一个边界,别忘了还要定义好微服务内的逻辑边界和代码边界,这样才能得到你想要的结果。


那现在你知道了,我们一定要避免将微服务设计为小单体微服务,那具体该如何避免呢?清晰的边界人人想要,可该如何保证呢?DDD已然给出了答案。


微服务边界的作用


你应该还记得DDD设计方法里的限界上下文和聚合吧?它们就是用来定义领域模型和微服务边界的。


我们再来回顾一下DDD的设计过程。


在事件风暴中,我们会梳理出业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出实体等领域对象。根据实体对象之间的业务关联性,将业务紧密相关的多个实体进行组合形成聚合,聚合之间是第一层边界。根据业务及语义边界等因素将一个或者多个聚合划定在一个限界上下文内,形成领域模型,限界上下文之间的边界是第二层边界。


为了方便理解,我们将这些边界分为: 逻辑边界、物理边界和代码边界


逻辑边界 主要定义同一业务领域或应用内紧密关联的对象所组成的不同聚类的组合之间的边界。事件风暴对不同实体对象进行关联和聚类分析后,会产生多个聚合和限界上下文,它们一起组成这个领域的领域模型。微服务内聚合之间的边界就是逻辑边界。一般来说微服务会有一个以上的聚合,在开发过程中不同聚合的代码隔离在不同的聚合代码目录中。


逻辑边界在微服务设计和架构演进中具有非常重要的意义!


微服务的架构演进并不是随心所欲的,需要遵循一定的规则,这个规则就是逻辑边界。微服务架构演进时,在业务端以聚合为单位进行业务能力的重组,在微服务端以聚合的代码目录为单位进行微服务代码的重组。由于按照DDD方法设计的微服务逻辑边界清晰,业务高内聚,聚合之间代码松耦合,因此在领域模型和微服务代码重构时,我们就不需要花费太多的时间和精力了。


现在我们来看一个微服务实例,在下面这张图中,我们可以看到微服务里包含了两个聚合的业务逻辑,两个聚合分别内聚了各自不同的业务能力,聚合内的代码分别归到了不同的聚合目录下。


那随着业务的快速发展,如果某一个微服务遇到了高性能挑战,需要将部分业务能力独立出去,我们就可以以聚合为单位,将聚合代码拆分独立为一个新的微服务,这样就可以很容易地实现微服务的拆分。





另外,我们也可以对多个微服务内有相似功能的聚合进行功能和代码重组,组合为新的聚合和微服务,独立为通用微服务。现在你是不是有点做中台的感觉呢?


物理边界 主要从部署和运行的视角来定义微服务之间的边界。不同微服务部署位置和运行环境是相互物理隔离的,分别运行在不同的进程中。这种边界就是微服务之间的物理边界。


代码边界 主要用于微服务内的不同职能代码之间的隔离。微服务开发过程中会根据代码模型建立相应的代码目录,实现不同功能代码的隔离。由于领域模型与代码模型的映射关系,代码边界直接体现出业务边界。代码边界可以控制代码重组的影响范围,避免业务和服务之间的相互影响。微服务如果需要进行功能重组,只需要以聚合代码为单位进行重组就可以了。


正确理解微服务的边界


从上述内容中,我们知道了,按照DDD设计出来的逻辑边界和代码边界,让微服务架构演进变得不那么费劲了。


微服务的拆分可以参考领域模型,也可以参考聚合,因为聚合是可以拆分为微服务的最小单位的。但实施过程是否一定要做到逻辑边界与物理边界一致性呢?也就是说聚合是否也一定要设计成微服务呢?答案是不一定的,这里就涉及到微服务过度拆分的问题了。


微服务的过度拆分会使软件维护成本上升,比如:集成成本、发布成本、运维成本以及监控和定位问题的成本等。在项目建设初期,如果你不具备较强的微服务管理能力,那就不宜将微服务拆分过细。当我们具备一定的能力以后,且微服务内部的逻辑和代码边界也很清晰,你就可以随时根据需要,拆分出新的微服务,实现微服务的架构演进了。


当然,还要记住一点,微服务内聚合之间的服务调用和数据依赖需要符合高内聚松耦合的设计原则和开发规范,否则你也不能很快完成微服务的架构演进。


总结


我们主要讨论了微服务架构设计中的各种边界在架构演进中的作用。


逻辑边界: 微服务内聚合之间的边界是逻辑边界。它是一个虚拟的边界,强调业务的内聚,可根据需要变成物理边界,也就是说聚合也可以独立为微服务。


物理边界: 微服务之间的边界是物理边界。它强调微服务部署和运行的隔离,关注微服务的服务调用、容错和运行等。


代码边界: 不同层或者聚合之间代码目录的边界是代码边界。它强调的是代码之间的隔离,方便架构演进时代码的重组。


通过以上边界,我们可以让业务能力高内聚、代码松耦合,且清晰的边界,可以快速实现微服务代码的拆分和组合,轻松实现微服务架构演进。但有一点一定要格外注意,边界清晰的微服务,不是大单体向小单体的演进。


作者:架构狂人
来源:mdnice.com/writing/2e64f8fdf9cb4213894a57d4e7a8a904
收起阅读 »

分布式架构关键设计10问

一、选择什么样的分布式数据库? 分布式架构下的数据应用场景远比集中式架构复杂,会产生很多数据相关的问题。谈到数据,首先就是要选择合适的分布式数据库。 分布式数据库大多采用数据多副本的方式,实现数据访问的高性能、多活和容灾。目前主要有三种不同的分布式数据库...
继续阅读 »

一、选择什么样的分布式数据库?


分布式架构下的数据应用场景远比集中式架构复杂,会产生很多数据相关的问题。谈到数据,首先就是要选择合适的分布式数据库。


分布式数据库大多采用数据多副本的方式,实现数据访问的高性能、多活和容灾。目前主要有三种不同的分布式数据库解决方案。它们的主要差异是数据多副本的处理方式和数据库中间件。


1. 一体化分布式数据库方案


它支持数据多副本、高可用。多采用Paxos协议,一次写入多数据副本,多数副本写入成功即算成功。代表产品是OceanBase和高斯数据库。


2. 集中式数据库+数据库中间件方案


它是集中式数据库与数据库中间件结合的方案,通过数据库中间件实现数据路由和全局数据管理。数据库中间件和数据库独立部署,采用数据库自身的同步机制实现主副本数据的一致性。集中式数据库主要有MySQL和PostgreSQL数据库,基于这两种数据库衍生出了很多的解决方案,比如开源数据库中间件MyCat+MySQL方案,TBase(基于PostgreSQL,但做了比较大的封装和改动)等方案。


3. 集中式数据库+分库类库方案


它是一种轻量级的数据库中间件方案,分库类库实际上是一个基础JAR包,与应用软件部署在一起,实现数据路由和数据归集。它适合比较简单的读写交易场景,在强一致性和聚合分析查询方面相对较弱。典型分库基础组件有ShardingSphere。


小结: 这三种方案实施成本不一样,业务支持能力差异也比较大。一体化分布式数据库主要由互联网大厂开发,具有超强的数据处理能力,大多需要云计算底座,实施成本和技术能力要求比较高。集中式数据库+数据库中间件方案,实施成本和技术能力要求适中,可满足中大型企业业务要求。第三种分库类库的方案可处理简单的业务场景,成本和技能要求相对较低。在选择数据库的时候,我们要考虑自身能力、成本以及业务需要,从而选择合适的方案。


二、如何设计数据库分库主键?


选择了分布式数据库,第二步就要考虑数据分库,这时分库主键的设计就很关键了。


与客户接触的关键业务,我建议你以客户ID作为分库主键。这样可以确保同一个客户的数据分布在同一个数据单元内,避免出现跨数据单元的频繁数据访问。跨数据中心的频繁服务调用或跨数据单元的查询,会对系统性能造成致命的影响。


将客户的所有数据放在同一个数据单元,对客户来说也更容易提供客户一致性服务。而对企业来说,“以客户为中心”的业务能力,首先就要做到数据上的“以客户为中心”。


当然,你也可以根据业务需要用其它的业务属性作为分库主键,比如机构、用户等。


三、数据库的数据同步和复制


在微服务架构中,数据被进一步分割。为了实现数据的整合,数据库之间批量数据同步与复制是必不可少的。数据同步与复制主要用于数据库之间的数据同步,实现业务数据迁移、数据备份、不同渠道核心业务数据向数据平台或数据中台的数据复制、以及不同主题数据的整合等。


传统的数据传输方式有ETL工具和定时提数程序,但数据在时效性方面存在短板。分布式架构一般采用基于数据库逻辑日志增量数据捕获(CDC)技术,它可以实现准实时的数据复制和传输,实现数据处理与应用逻辑解耦,使用起来更加简单便捷。


现在主流的PostgreSQL和MySQL数据库外围,有很多数据库日志捕获技术组件。CDC也可以用在领域事件驱动设计中,作为领域事件增量数据的获取技术。


四、跨库关联查询如何处理?


跨库关联查询是分布式数据库的一个短板,会影响查询性能。在领域建模时,很多实体会分散到不同的微服务中,但很多时候会因为业务需求,它们之间需要关联查询。


关联查询的业务场景包括两类:第一类是基于某一维度或某一主题域的数据查询,比如基于客户全业务视图的数据查询,这种查询会跨多个业务线的微服务;第二类是表与表之间的关联查询,比如机构表与业务表的联表查询,但机构表和业务表分散在不同的微服务。


如何解决这两类关联查询呢?


对于第一类场景,由于数据分散在不同微服务里,我们无法跨多个微服务来统计这些数据。你可以建立面向主题的分布式数据库,它的数据来源于不同业务的微服务。采用数据库日志捕获技术,从各业务端微服务将数据准实时汇集到主题数据库。在数据汇集时,提前做好数据关联(如将多表数据合并为一个宽表)或者建立数据模型。面向主题数据库建设查询微服务。这样一次查询你就可以获取客户所有维度的业务数据了。你还可以根据主题或场景设计合适的分库主键,提高查询效率。


对于第二类场景,对于不在同一个数据库的表与表之间的关联查询场景,你可以采用小表广播,在业务库中增加一张冗余的代码副表。当主表数据发生变化时,你可以通过消息发布和订阅的领域事件驱动模式,异步刷新所有副表数据。这样既可以解决表与表的关联查询,还可以提高数据的查询效率。


五、如何处理高频热点数据?


对于高频热点数据,比如商品、机构等代码类数据,它们同时面向多个应用,要有很高的并发响应能力。它们会给数据库带来巨大的访问压力,影响系统的性能。


常见的做法是将这些高频热点数据,从数据库加载到如Redis等缓存中,通过缓存提供数据访问服务。这样既可以降低数据库的压力,还可以提高数据的访问性能。


另外,对需要模糊查询的高频数据,你也可以选用ElasticSearch等搜索引擎。


缓存就像调味料一样,投入小、见效快,用户体验提升快。


六、前后序业务数据的处理


在微服务设计时你会经常发现,某些数据需要关联前序微服务的数据。比如:在保险业务中,投保微服务生成投保单后,保单会关联前序投保单数据等。在电商业务中,货物运输单会关联前序订单数据。由于关联的数据分散在业务的前序微服务中,你无法通过不同微服务的数据库来给它们建立数据关联。


如何解决这种前后序的实体关联呢?


一般来说,前后序的数据都跟领域事件有关。你可以通过领域事件处理机制,按需将前序数据通过领域事件实体,传输并冗余到当前的微服务数据库中。


你可以将前序数据设计为实体或者值对象,并被当前实体引用。在设计时你需要关注以下内容:如果前序数据在当前微服务只可整体修改,并且不会对它做查询和统计分析,你可以将它设计为值对象;当前序数据是多条,并且需要做查询和统计分析,你可以将它设计为实体。


这样,你可以在货物运输微服务,一次获取前序订单的清单数据和货物运输单数据,将所有数据一次反馈给前端应用,降低跨微服务的调用。如果前序数据被设计为实体,你还可以将前序数据作为查询条件,在本地微服务完成多维度的综合数据查询。只有必要时才从前序微服务,获取前序实体的明细数据。这样,既可以保证数据的完整性,还可以降低微服务的依赖,减少跨微服务调用,提升系统性能。


七、数据中台与企业级数据集成


分布式微服务架构虽然提升了应用弹性和高可用能力,但原来集中的数据会随着微服务拆分而形成很多数据孤岛,增加数据集成和企业级数据使用的难度。你可以通过数据中台来实现数据融合,解决分布式架构下的数据应用和集成问题。


你可以分三步来建设数据中台。


第一,按照统一数据标准,完成不同微服务和渠道业务数据的汇集和存储,解决数据孤岛和初级数据共享的问题。


第二,建立主题数据模型,按照不同主题和场景对数据进行加工处理,建立面向不同主题的数据视图,比如客户统一视图、代理人视图和渠道视图等。


第三,建立业务需求驱动的数据体系,支持业务和商业模式创新。


数据中台不仅限于分析场景,也适用于交易型场景。你可以建立在数据仓库和数据平台上,将数据平台化之后提供给前台业务使用,为交易场景提供支持。


八、BFF与企业级业务编排和协同


企业级业务流程往往是多个微服务一起协作完成的,每个单一职责的微服务就像积木块,它们只完成自己特定的功能。那如何组织这些微服务,完成企业级业务编排和协同呢?


你可以在微服务和前端应用之间,增加一层BFF微服务(Backend for Frontends)。 BFF主要职责是处理微服务之间的服务组合和编排,微服务内的应用服务也是处理服务的组合和编排,那这二者有什么差异呢?


BFF位于中台微服务之上,主要职责是微服务之间的服务协调; 应用服务主要处理微服务内的服务组合和编排。 在设计时我们应尽可能地将可复用的服务能力往下层沉淀,在实现能力复用的同时,还可以避免跨中心的服务调用。


BFF像齿轮一样,来适配前端应用与微服务之间的步调。它通过Façade服务适配不同的前端,通过服务组合和编排,组织和协调微服务。BFF微服务可根据需求和流程变化,与前端应用版本协同发布,避免中台微服务为适配前端需求的变化,而频繁地修改和发布版本,从而保证微服务核心领域逻辑的稳定。


如果你的BFF做得足够强大,它就是一个集成了不同中台微服务能力、面向多渠道应用的业务能力平台。


九、分布式事务还是事件驱动机制?


分布式架构下,原来单体的内部调用,会变成分布式调用。如果一个操作涉及多个微服务的数据修改,就会产生数据一致性的问题。数据一致性有强一致性和最终一致性两种,它们实现方案不一样,实施代价也不一样。


对于实时性要求高的强一致性业务场景,你可以采用分布式事务,但分布式事务有性能代价,在设计时我们需平衡考虑业务拆分、数据一致性、性能和实现的复杂度,尽量避免分布式事务的产生。


领域事件驱动的异步方式是分布式架构常用的设计方法,它可以解决非实时场景的数据最终一致性问题。基于消息中间件的领域事件发布和订阅,可以很好地解耦微服务。通过削峰填谷,可以减轻数据库实时访问压力,提高业务吞吐量和处理能力。你还可以通过事件驱动实现读写分离,提高数据库访问性能。对最终一致性的场景,我建议你采用领域事件驱动的设计方法。


十、多中心多活的设计


分布式架构的高可用主要通过多活设计来实现,多中心多活是一个非常复杂的工程,下面我主要列出以下几个关键的设计。


1.选择合适的分布式数据库。数据库应该支持多数据中心部署,满足数据多副本以及数据底层复制和同步技术要求,以及数据恢复的时效性要求。


2.单元化架构设计。将若干个应用组成的业务单元作为部署的基本单位,实现同城和异地多活部署,以及跨中心弹性扩容。各单元业务功能自包含,所有业务流程都可在本单元完成;任意单元的数据在多个数据中心有副本,不会因故障而造成数据丢失;任何单元故障不影响其它同类单元的正常运行。单元化设计时我们要尽量避免跨数据中心和单元的调用。


3.访问路由。访问路由包括接入层、应用层和数据层的路由,确保前端访问能够按照路由准确到达数据中心和业务单元,准确写入或获取业务数据所在的数据库。


4.全局配置数据管理。实现各数据中心全局配置数据的统一管理,每个数据中心全局配置数据实时同步,保证数据的一致性。


总结


企业级分布式架构的实施是一个非常复杂的系统工程,涉及到非常多的技术体系和方法。今天我列的10个关键的设计领域,每个领域其实都非常复杂,需要很多的投入和研究。在实施的时候,你和你的公司要结合自身情况来选择合适的技术组件和实施方案。


作者:架构狂人
来源:mdnice.com/writing/efcac6bf632b4172903c8a14c2e1f0f4
收起阅读 »

二维码基本原理

二维码技术始于20世纪80年代末,全球现有250多种二维码,其中常见技术标准有PDF417,QRCode,Code49Code16K,CodeOne等20余种。我们日常扫码以QR码居多。 从1997年到2012年,我国陆续发布了5个二维码国家标准:PDF41...
继续阅读 »


二维码技术始于20世纪80年代末,全球现有250多种二维码,其中常见技术标准有PDF417,QRCode,Code49Code16K,CodeOne等20余种。我们日常扫码以QR码居多。



从1997年到2012年,我国陆续发布了5个二维码国家标准:PDF417,QRCode(快速响应码),汉信码,GM码(网格矩阵码)和CM码(紧密矩阵码)。其中QRCode因为具有识读速度快、信息容量大、占用空间小、保密性强、可靠性高的优势,是目前使用最为广泛的一种二维码。QRCode 呈正方形,只有两种颜色,在4个角落的其中3个,印有像“回”字的的小正方图案。QR码是属于开放式的标准。




二维码的工作原理


二维码内的图案代表二进制代码,经过解释后可显示代码存储的数据。


二维码阅读器根据二维码外侧的三个较大方块来识别标准二维码。当识别出这三个形状后,就知道整个方块内包含的内容是一个二维码。


二维码阅读器随后将整个二维码分解到网格进行分析。它查看每个网格方块,并根据方块是黑色还是白色来为其分配一个值。然后将网格方块组合在一起,创建更大的图案。



二维码由哪些部分组成


①.静态区域 (Quiet zone)


这是二维码外侧的空白边框。如果没有这个边框,二维码阅读器会因为外界因素的干扰而无法确定二维码包含和不包含的内容。


②.寻像图案 (Finder pattern)


二维码在左下角、左上角和右上角包含三个黑色方块。这些方块告诉二维码阅读器它看到的是一个二维码,及二维码的外部边框在哪里。


③.校准图案 (Alignment pattern)


这是二维码右下角附近的某个位置包含的另一个较小方块,用于确保二维码在倾斜或有角度的情况下仍然可以阅读。


④.定位图案 (Timing pattern)


这是一条 L 形线,在寻像图案的三个方块之间。定位图案帮助阅读器识别整个二维码中的各个方块,使损坏的二维码仍有可能被阅读。


⑤.版本信息 (Version information)


这是二维码右上角寻像图案附近的一小块信息区域。它标识了正在阅读的二维码的版本(请参阅“二维码有哪四个版本?”)。


⑥.数据单元 (Data cell)


二维码的其余部分传达实际信息,即所包含的 URL、电话号码或消息。



二维码的特点


①.高密度编码,信息容量大:可容纳1850个大写字母或2710个数字或1108个字节,或500多个汉字,比普通条码信息容量约高几十倍。


②.编码范围广:可以把图片、声音、文字、签字、指纹等可以数字化的信息进行编码,用二维码表示出来;可以表示多种语言文字;可表示图像数据。


③.容错能力强,具有纠错功能:这使得二维条码因穿孔、污损等引起局部损坏时,照样可以正确识读,损毁面积达30%仍可恢复信息。


④.译码可靠性高:它比普通条码译码错误率百万分之二要低得多,误码率不超过千万分之一。


⑤.可引入加密措施:保密性、防伪性好。


⑥.成本低,易制作,持久耐用。



为什么要统一标准


①.如果二维码的数据格式不统一、印制精度、符号大小不符合要求,就容易导致信息乱码、无法识读。
②.如果特定二维码只能在特定客户端上扫描,导致用户扫描反复受挫,用户体验不好。


 
统一的二维码国家标准是解决这方面问题的最佳手段,以实现最佳的兼容性和用户体验。而不兼容国家标准的客户端由于用户体验差,自然被用户抛弃。



常用二维码对比




QR二维码读取


QR码从360°任一方向均可快速读取。其奥秘就在于QR码中的3处定位图案,可以帮助QR码不受背景样式的影响,实现快速稳定的读取。





QR码的基本结构



格式信息:表示改二维码的纠错级别,分为L、M、Q、H;


校正图形:规格确定,校正图形的数量和位置也就确定了;


数据和纠错码字:实际保存的二维码信息,和纠错码字(用于修正二维码损坏带来的错误)


位置探测图形、位置探测图形分隔符、定位图形:用于对二维码的定位,每个QR码位置都是固定存在的,只是大小有所差异;


版本信息:即二维码的规格,QR码符号共有40种规格的矩阵(一般为黑白色),从21x21(版本1),到177x177(版本40),每一版本符号比前一版本 每边增加4个模块。





QR码存储容量


格式容量
数字最多7089字符
字母最多4296字符
二进制数(8 bit)最多2953字节
日文汉字/片假名最多1817字符(采用Shift JIS)
中文汉字最多984字符(采用UTF-8)


QR码纠错能力


即使编码变脏或破损,也可自动恢复数据。这一“纠错能力”具备4个级别,用户可根据使用环境选择相应的级别。调高级别,纠错能力也相应提高,但由于数据量会随之增加,编码尺寸也也会变大。


纠错等级纠错水平
L7%字码修正
M15%字码修正
Q25%字码修正
H

30%字码修正







下图所示:相同内容的二维码,纠错等级不一样,矩阵的密度也不一样,容错率越高,密度越大




不是所有位置都可以缺损,像三个角上的回字方框,直接影响初始定位,不能缺失。中间零散的部  分是内容编码,可以容忍缺损。



计算机的世界都是0和1,二维码再一次说明了这个问题







普通二维码存在的问题


普通二维码只是对文字、网址、电话等信息进行编码,不支持图片、音频、视频等内容,且生成二维码后内容无法改变,在信息内容较多时生成的二维码图案复杂,不容易识别和打印,正是由于存在这些特性故称之为静态二维码。静态二维码的好处就是无需联网也能识别,但是有些时候在线下场景经常需要打印二维码出来让用户去扫码,或者在一些运营场景下需要对用户的扫码情况进行数据统计和分析,再使用普通的二维码就无法提供这些功能了,这时候就要使用动态二维码了





动态二维码(活码)及其原理


动态二维码也称之为活码,内容可变但是二维码不变。支持随时修改二维码的内容且二维码图案不变,可跟踪扫描统计数据,支持存储大量文字、图片、文件、音视、视频等内容,同时生成的图案简单易扫。


实际上二维码是按照指定的规则编码后的一串字符串,通常情况下是一个网址,在二维码出现之前,打开浏览器输入网址即可访问相应的网站,而有了二维码之后,我们扫描二维码,首先会做一次从二维码到文本的解析、转换,然后根据解析出来的文本结果判断是否是链接,是则跳转到这个链接,尽管我们操作方式改变了,但其原理是相同的。 


二维码对外暴露的是同一个网址,服务端只需要对这个网址做个二次跳转就行,这个对外暴露固定不变的网址也称为“活址”。




静态二维码和动态二维码(活码)的区别


比较项普通二维码动态二维码(活码)
内容修改不支持可以随时修改
内容类型支持文字、网址、电话等支持文字、图片、文件、音视、视频等内容
二维码图案内容越多越复杂活码图案简单
数据统计不支持支持
样式排版不支持支持


汉信码 -- 中国自主开发的二维码标准


汉信码是一种全新的二维矩阵码,由中国物品编码中心牵头组织相关单位合作开发,完全具有自主知识产权,支持任意语言编码、汉字信息编码能力超强、极强抗污损、抗畸变识读能力、识读速度快、信息密度高、信息容量大、纠错能力强等突出特点,达到国际领先水平。和国际上其他二维条码相比,更适合汉字信息的表示,而且可以容纳更多的信息。



物品编码中心于2003年申请了国家"十五"重大科技专项课题,并与我国多家自动识别技术企业合作,开展汉信码技术研究工作。2005年12月26日该课题顺利通过国家标准委组织的项目验收。2007年8月23日《汉信码》国家标准正式颁布,并于2008年2月1日正式实施。 




 汉信码生成:tuzim.net/barcode/han…
 汉信码识别:https://tuzim.net/hxdecode



它的主要技术特色是:
①.具有高度的汉字表示能力和汉字压缩效率
汉信码支持GB18030中规定的160万个汉字信息字符,并且采用12比特的压缩比率,每个符号可表示12~2174个汉字字符。


②.信息容量大
在打印精度支持的情况下,每平方英寸最多可表示7829个数字字符, 2174个汉字字符, 4350个英文字母。


③.编码范围广
汉信码可以将照片、指纹、掌纹、签字、声音、文字等凡可数字化的信息进行编码。


④.支持加密技术
汉信码是第一种在码制中预留加密接口的条码,它可以与各种加密算法和密码协议进行集成,因此具有极强的保密防伪性能。


⑤.抗污损和畸变能力强
汉信码具有很强的抗污损和畸变能力,可以被附着在常用的平面或桶装物品上,并且可以在缺失两个定位标的情况下进行识读。


⑥.修正错误能力强
汉信码采用世界先进的数学纠错理论,采用太空信息传输中常采用的Reed-Solomon纠错算法,使得汉信码的纠错能力可以达到30%。


⑦.可供用户选择的纠错能力
汉信码提供四种纠错等级,用户可以根据自己的需要在8%、15%、23%和30%各种纠错等级上进行选择,从而具有高度的适应能力。


⑧.容易制作且成本低
利用现有的点阵、激光、喷墨、热敏/热转印、制卡机等打印技术,即可在纸张、卡片、PVC、甚至金属表面上印出汉信码。由此所增加的费用仅是油墨的成本,可以真正称得上是一种“零成本”技术。


⑨.条码符号的形状可变
汉信码支持84个版本,可以由用户自主进行选择,最小码仅有指甲大小。


⑩.外形美观
汉信码在设计之初就考虑到人的视觉接受能力,所以较之现有国际上的二维条码技术,汉信码在视觉感官上具有突出的特点。



汉信码实现了我国二维码底层技术的后来居上,在我国多个领域行业实现规模化应用,为我国应用二维码技术提供了可靠核心技术支撑。





********** 延伸 **********


一维码 (条形码)


一维码也叫条形码,它是由不同宽度的黑条和白条按照一定的顺序排列组成的平行线图案,它的宽度记录着数据信息,长度没有记录信息,条形码常用于标出物品的生产国、制造厂家、商品名称、生产日期、图书分类号、邮件起止地点、类别、日期等信息,大部分食品包装袋背后都会印有条形码。


 全球的条形码标准都是由一个叫GS1的非营利性组织管理和维护的,通常情况下条形码由 95 条红或黑色的平行竖线组成,前三条是由黑-白-黑 组成,中间的五条由白-黑-白-黑-白组成,最后的三条和前三条一样也是由黑-白-黑组成,这样就把一个条形码分为左、右两个部分。剩下的 84 (95-3-5-3=84) 条按每 7 条一组分为 12 组,每组对应着一个数字,不同的数字的具体表示因编码方式而有所不同,不过都遵循着一个规律:右侧部分每一组的白色竖线条数都是奇数个。这样不管你是正着扫描还是反着扫描都是可以识别的。


 中国使用的条形码大部分都是 EAN-13 格式的,条形码数字编码的含义从左至右分别是前三位标识来源 国家编码 ,比如中国为:690–699,后面的 4 ~ 8 位数字代表的是厂商公司代码,但是位数不是固定的,紧接着后面 的 9~12 位是商品编码,第 13 位是校验码,这就意味着公司编码越短,剩余可用于商品编码的位数也越多,可表示的商品也就越多,当然公司代码出售价格也相应更昂贵,另外用在商品上的 EAN-13 条码是要到 国家物品编码中心去申请的。





作者:似水流年QC
来源:juejin.cn/post/7258201505337131065

收起阅读 »

生存or毁灭?QQ空间150万行代码的涅槃重生

腾小云导读 今年是 QQ 空间诞生的第十八年,空间客户端团队也在它十八岁生日前夕完成了架构升级。因为以前不规范的多团队协同开发,导致代码逐渐劣化,有着巨大的风险。于是 QQ 空间面对庞大的历史债务,选择了重构升级,不破不立。这里和大家分享一下在重构过程中遇到的...
继续阅读 »

腾小云导读


今年是 QQ 空间诞生的第十八年,空间客户端团队也在它十八岁生日前夕完成了架构升级。因为以前不规范的多团队协同开发,导致代码逐渐劣化,有着巨大的风险。于是 QQ 空间面对庞大的历史债务,选择了重构升级,不破不立。这里和大家分享一下在重构过程中遇到的问题和解题思路,欢迎阅读。


目录


1 空间重构项目的背景


2 为什么要重构


3 空间的架构是如何崩坏的


4 架构的生命力


5 渐进式重构如何实现


6 如何保证架构的扩展性与复用性7 如何降低复杂度并长期可控


8 如何防止劣化


9 性能优化


10 项目重构成果总结


11 展望


18年前,QQ 空间上线,迅速风靡全网,成为了很多人的青春回忆。18年后的今天,QQ 空间的生命力依然强劲,是很多年轻用户的首选社交平台。


而作为最老牌的互联网产品之一,QQ 空间的代码也比较陈旧,代码运行环境复杂,维护成本高,整体架构亟需一场升级。


01、空间重构项目的背景


作为一个平台型的入口,空间承担了为很多兄弟业务引流的责任,许多团队在空间的代码里协作开发。加上自身多年累积的功能迭代,空间的业务变得非常复杂。业务的复杂带来了架构的复杂,架构的复杂意味着维护成本的升高。多年来空间的业务交接频繁,多个团队接手。交到我们团队手上时,空间的代码已经一言难尽。


这里先简单介绍一下空间的业务形态:空间目前主要的入口是在手 Q 里,我们叫做结合版。同时独立版的空间 App 还在维护(没错,空间独立 App 仍然还有一批忠实观众)。可以看到,空间有一套独立于手 Q 之外的架构,结合版与独立版会共用大量技术组件和业务组件。



02、为什么要重构?


空间是一个祖上很阔的业务,代码量非常庞大,单统计结合版的代码,就超过了150w 行。同时空间的代码运行环境也极为复杂,涉及5个进程和2个插件。随着频繁的交接和多团队的协同开发,空间的代码逐渐劣化,各项代码质量的指标几乎都在手 Q 里垫底。


空间的代码成了著名的原始森林 - 进得去出不来。代码的劣化导致历史 bug 难以收敛,即使一行代码不改,每个版本也会新增历史 bug30+。


面对如此庞大的历史债务,空间已经到了寸步难行,不破不立的地步,重构势在必行。所以,借着空间 UI 升级的契机,空间团队开始空间历史上最大规模的一次重构。


03、空间的架构是如何逐步劣化的?


跳出棋局,站在今天的角度回头看,可以发现空间的代码是个典型案例,很好地展示了一个干净的架构是如何逐步劣化的。



3.1 扩展性低,异化代码无处安放


结合版与独立版涉及大量的代码复用,包括组件、页面和跨 App 的复用等。但由于前期架构扩展性不高,导致异化的业务代码无处安放,开始侵入底层技术组件。底层组件代码开始受到污染。


3.2 代码未隔离且缺乏编程范式


空间是个平台型的业务,广告、会员、游戏、直播、小世界等团队都会在空间的代码里开发。由于没有做好代码隔离,各团队的代码耦合在一起,各写各的。同时由于缺乏编程范式,同一个类中的代码风格迥异。破窗效应发生,污染开始扩散。


3.3 维护成本暴增,恶性循环


空间的业务逻辑本身就很复杂,代码的劣化使其复杂度暴增,后续接手团队已有心无力,只能缝缝补补又三年,恶性循环。


最后陷入怪圈: 代码很乱但是稳定,开发道理都懂但确实不敢动。



3.4 Feeds 流的崩坏


以空间的 Feeds 流为例,最开始的架构思路是很清楚的,核心功能在基类实现,上层业务可以低成本地开发一个新的 Feeds 流页面。同时做了很多动态化和容器化的设计,来满足迭代效率。



但后续的需求迅速膨胀,异化出18种 Feeds 流场景,单 Feeds 流可能出现60多种卡片。这导致基类代码与 Feed View 中的代码迅速膨胀。同时 N 个团队在同一批代码中开发,代码行数和圈复杂度逐渐劣化。



04、架构的生命力


痛定思痛,在进行空间重构前的首件事情就是总结经验,避免重蹈覆辙。如何保证这次重构平稳落地并且避免后续每三年一重构?


我们总结了四点:


渐进式重构:高速公路换轮胎,如何平稳落地? 提高扩展性和复用性:是否能低成本迁移到其他业务,甚至是其他 App? 复杂度长期可控:n 个团队跑来做两年需求,复杂度会不会变高? 做好防劣化:劣化代码被引入,能否快速发现?

空间的重构都围绕着这四个问题来进行。


05、渐进式重构如何实现?


作为一个亿级日活的业务,空间出现线上问题很容易引起大量投诉。高速公路换轮胎,小步快跑是最合适的方式。因此,平稳落地的关键是渐进式重构,避免步子迈得太大导致工作量扩散。


要做到渐进式重构,核心是保证两点:


一个复杂的大问题能被分解为许多个小问题,可针对小问题重构和回滚; 系统随时都是可用状态。每解决一个小问题,都可以针对性的测试和上线。

为了实现以上两点,我们基于以下几点来进行改造:



5.1 先拆解,后治理


我们并没有立即开始对旧代码进行重写,而是先基于团队的 RFW-Part 框架对老代码进行拆解。Part 自带生命周期,可以保证老代码平移前后的运行逻辑一致。


尽管代码逻辑没有翻新,但大问题被拆解为一个个小问题,我们再根据优先级对单个 Part 进行重构。保证无论重构了多少,空间都是可用状态,能立即上线验证。


RFW-Part 框架后文会有介绍,此处不做展开。


5.2 架构融合


我们彻底抛弃了空间老的技术组件,与团队内部沉淀的 RFWComponent 进行架构融合,同时也积极接入手 Q 统一的 UI 体系。保证开发能专注于业务中间层开发。


5.3 提效前置,简化运行环境


在进行业务重构前,我们先还了一部分技术债。包括去插件化、进程统一、工程结构优化和编译优化等。这些工作都在业务重构前完成并上线验证,简化了空间代码的运行环境,提升开发效率,保证了重构工作的敏捷性,达到了针对单点问题快速重构快速验证的目的。



06、如何保证架构的扩展性与复用性?


扩展性和复用性是软件工程永恒的话题。空间历史架构并没有很好处理这两点,其他业务接入时难以处理异化逻辑,使异化逻辑侵入底层代码。同时为了强行实现结合版和独立版的代码复用,使不同的场景耦合在一起,互相干扰。


为了提高架构的扩展性和复用性,我们重新设计了空间的架构层级。


6.1 业务层打薄,专注中间层


为了避免代码跨层级污染,我们对架构的分层比以往更细,隔离做得更严格。


底层技术组件基于 RFW 框架。RFW 中的组件更干净,没有任何业务侵入,能在其他 App 开箱即用。


中间层负责对 RFW 组件和手 Q 运行环境做桥接,并对底层组件进行扩展,实现一些空间相关但与具体场景无关的功能。中间层的代码能在一周之内迁移到其他 App。



6.2 业务层打薄,专注中间层


RFWComponent 是一线开发在实际业务中沉淀出的一套组件库,目前由空间和小世界团队共同维护。所有组件都经过了线上业务的验证,保证了易用性和扩展性。组件也很完整,开箱即用。


最重要的是,RFW 的核心组件都可由上层注入代理实现,这使其并不依赖于手 Q 的运行环境,也避免了业务侧逻辑入侵底层代码。


目前整套架构已在空间、小世界、频道、基础等团队深度使用。空间也是第一个使用这套架构重构老代码的业务,整个过程非常省心。



07、如何降低复杂度并长期可控?


7.1 组合代替继承,Part + Section,拆!


什么是 RFW-Part?RFW-Part 是团队内部沉淀的一套页面级的 UI 容器架构,Part 可感知页面的生命的周期,功能在内部闭环。不同 Part 无法感知对方存在,代码是严格隔离的。



但是 Part 是页面级框架,无法解决 Feeds 流列表复杂的问题,Section 架构作为 Part 的补充,主要解决列表以及 ItemView 的拆解问题。其设计思路与 Part 框架一致。



基于 Part 和 Section 架构,我们将空间的代码拆分为了一个个标准的集装箱。代码复杂度和上手难度大大降低。新人内包入职一周便可独立开发,三天就完成了新功能此刻的消息页。



7.2 使用 Part 架构重塑超级页面


空间80%的流量和功能都集中在好友动态页和个人主页两个 Feeds 流页面,尽管内部已基于 mvvm 分层,但单层内的复杂度仍然过高:



以空间的好友动态页为例,我们将页面不同功能的代码都拆分到一个个 Part 里,Fragment 仅作为一个容器,负责组装自己需要的 Part。



最终页面被拆分为27个 Part,页面代码由6000多行减少到320行。很多 Part 可以直接拿去被其他 Feeds 流页面复用。



7.3 使用 Section 框架重塑 Feeds 流


经过 Part 的改造,页面级的功能都被拆分为子模块。但 Feeds 流整体作为一个 Part,复杂度仍然过高,我们需要设计一套新的框架,对 Feeds 流中的卡片进一步拆解。


7.3.1 空间老的 Feeds 流框架


这里先介绍一下空间老的 Feeds 流框架 - Ditto。


Ditto 框架魔改了 Android 原生的布局体系。其将一个卡片按位置分为不同 Area,每个 Area 作为一个容器。不同类型的卡片根据服务端下发的数据在 Area 内部做异化。


而每个 Area 的布局由 json 文件下发,Ditto 框架解析后使用 canvas 自绘,完成显示。



这套架构的优势是动态化能力强,服务端可定义任意样式,但缺点同样明显:


代码复杂度持续膨胀; 各业务代码耦合; 功能代码分散,AB 测试不友好; 难以扩展。

7.3.2 优化方向


为了降低复杂度,我们决定按以下方向优化:


中心化 -> 去中心化; 代码物理隔离; 内部闭环,动态开关; 组装者模式,方便扩展。

7.3.3 Section 框架架构设计


和 Part 一样,我们将一个卡片按照功能逻辑拆分为一个个 Section,形成一个 Section 池。不同卡片根据需要组装自己需要的 Section 即可。


Section 的 UI、数据、业务都是内部闭环的。不同 Section 互不感知,保证了代码物理隔离。


每个 Section 会与 ViewStub 绑定,布局可以按需加载。ViewStub 与 Section 是一对多的关系,Section 在查找 ViewStub 前会先去缓存池找,这样实现了多个 Section 修改同一个 View,保证 Section 拆得尽可能细。



上图中各模块的具体职责如下:


Section:某一切片的完整 UI+逻辑; ViewStub:与 Section 一对多,按需加载; Assembler:负责组装 Section,可根据页面异化; SectionManager:绑定数据、分发生命周期; DataCenter:Feeds 相关数据在各页面间的同步; IOC 框架:控制反转,用于 Section 与页面交互。

Section 整体的结构图如下:



7.3.4 落地效果


基于这套 Feeds 流框架,我们完成了历史卡片的梳理和重构:


接入36种 Feed,拆分52个 Section,下线28种 Feed; 重构4个核心页面,单类代码不超过500行; 单条 Feed 开发时间缩短一半; 广告/增值团队一个版本即完成历史功能迁移。

7.4 完善通信设计,保证代码隔离不被打破


Part 和 Section 之间会有许多通信的需求,比如数据同步,不同模块交互等。为了保证代码隔离不被打破,我们设计了比较完善的通信机制:


页面与 Part:ViewModel + LiveData; Part 与 Part:页面级事件,事件只在 PartHost 内部生效,无需注册与反注册; 页面与页面:DataCenter 数据同步。


7.5 异化逻辑抽离,复杂度持续可控


除此之外,另一种容易打穿架构的元素是异化逻辑。比如同一张卡片在不同的页面需要显示不同效果,比如数据埋点的参数需要从页面最外层传递到 Section。针对这种跨层级通信的场景,我们设计了一套 IOC 框架来完成依赖注入,将异化逻辑拆分到了一个个 IOC 实现类中。



IOC 机制的核心是:View 树回溯 + ViewTag 存储 + 接口中心管理。我们注册时将 IOC 实现类与 View 绑定,查找时基于 View 树来回溯,保证了 O(N) 的复杂度,且可以跨越任意层级。



过去,即使传递一个 pageId 参数,也要一层层传递:



现在,层级再深我们也可以很方便拿到需要的 IOC 实现。


08、升级方案


8.1 容灾设计


站在用户的角度,其实对重构与否并没有太大感知,用户只关心稳定性是否有下降。如此大规模的重构,一行代码引起的崩溃便能使几个月的努力功亏一篑。我们上线前的首要目标便是保证用户使用不受影响,不求有功,但求无过。


因此,我们在上线前做了很多容灾设计,保证空间的核心功能可用性。



8.1.1 动态开关


我们在空间的中间层埋了配置,能通过配置下架任意的 Part 或 Section。业务层编写代码时不用再单独为每个小模块添加开关,只要基于框架做好细粒度的拆分即可。


8.1.2 崩溃保护


同时,我们做了崩溃保护的设计,保证非核心功能崩溃不会影响核心功能的使用:


崩溃时进行关键词匹配,达到指定频率时禁用/降级相关功能; 自动对 Part/Section/页面/Feed 做关键词匹配,无需注册; 非必要功能可手动注册关键词,添加保护。

8.2 性能监控


同时,为了防止性能劣化,我们做了很多性能监控。


针对线上:


利用手 Q RMonitor 框架的监控和我们自己上报的滑动流畅度指标,来监控页面整体的流畅度; 通过在框架层打点,来监控每一个 Part、Section 或 Feed 的耗时。有劣化的模块引入时能快速发现; 实现 RFWTracer 框架,自动在页面启动流程中打点,统计页面启动各阶段的耗时。

针对线下:


我们基于 ARTMethodHook 框架,实现对具体 View 耗时的监控,能快速定位到出问题的控件,节约开发定位性能问题的时间。


整体监控体系如图:



实际效果如图:



09、性能优化


第一次灰度后,我们尴尬地发现启动速度并没有大幅提升,流畅率甚至发生了降低。因此我们做了首屏启动和流畅度的专项优化:


9.1 首屏启动优化


我们重新梳理了启动流程中的数据处理,在启动前和启动后做了一定优化:




  • 布局异步渲染


我们将首屏启动前,会根据缓存提前计算需要的布局,实现布局异步预加载。同时,为了保证 Context 的正确性,我们 Hook 了 Activity 的启动流程,提前准备好空的 Activity 对象用于异步 inflate,并在启动后绑定真实的 Context。




  • 精准预加载


在首屏启动前读取缓存,提前计算首屏 Feed 对应的 Section 布局并异步加载。



  • 生命周期扩展


扩展 Part 生命周期,各个 Part 的次要功能在首屏展示后初始化。



  • 优化后的效果


空间好友动态页的冷启动速度提升56%,热启动速度提升53%。


9.2 列表性能优化


经过分析,我们发现列表卡顿的原因集中在两点:


Item 复用率低,导致频繁创建新 View; 布局嵌套多,测量较慢。

解决思路:


边滑边异步 inflate:为了解决频繁创建新 View 的问题,我们在滑动时,会提前计算后面卡片所需的 ViewStub,并提前异步加载好。 自定义组件,降低层级,提前计算高度:列表中部分组件测量性能较差,比如部分嵌套 RecyclerView 的组件,会频繁触发子 RecyclerView 的测量,拉高整体测量耗时。对于这些组件,我们使用自定义组件的方式进行了替换。降低布局层级,并且提前计算高度,设置布局的高度为固定值,防止频繁测量。

优化后的效果:完成优化后,空间首页 FPS 完成了反超,相比老版本提升了 4.9%。


10、项目重构成果总结


从我们 AB 测试的实验数据来看,重构的整体结果是比较正向的,代码质量提升与性能提升带来了业务指标的提升,业务指标的提升也带来广告指标的提升。



11、展望


空间的代码历史悠久,错综复杂,使得空间业务在很长一段时间都处于维护状态,难以快速开发新的需求。最大的三个模块是压在空间业务上的三座大山:Feeds 流、相册和发表。通过这次架构升级,我们完成空间底层架构的焕新,完全重写了最复杂的 Feeds 流场景,同时相册模块也已经重构了一半。等剩余模块重构完成,空间的祖传代码就被全部重写了。面向未来,我们也能够更迅速地支撑新需求的落地,让十八岁的 QQ 空间焕然新生,重新上路。欢迎转发分享~


-End-


原创作者|尹述迪

收起阅读 »

单线程 Redis 如此快的 4 个原因

本文翻译自国外论坛 medium,原文地址:levelup.gitconnected.com/4-reasons-w… 作为内存数据存储,Redis 以其速度和性能而闻名,通常被用作大多数后端服务的缓存解决方案。 然而,在 Redis 内部采用的也只是单线程的...
继续阅读 »

本文翻译自国外论坛 medium,原文地址:levelup.gitconnected.com/4-reasons-w…


作为内存数据存储,Redis 以其速度和性能而闻名,通常被用作大多数后端服务的缓存解决方案。


然而,在 Redis 内部采用的也只是单线程的设计。


为什么 Redis 单线程设计会带来如此高的性能?如果利用多个线程并发处理请求不是更好吗?


在本文中,我们将探讨使 Redis 成为快速高效的数据存储的设计选择。


长话短说


Redis 的性能可归因于 4 个主要因素



  • 基于内存存储

  • 优化的数据结构

  • 单线程架构

  • 非阻塞IO


让我们一一剖析一下。



推荐博主开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。


github 地址:github.com/wayn111/way…



基于内存存储




Redis 是在内存中进行键值存储。


Redis 中的每次读写操作都相当于从内存的变量中进行读写。


访问内存比直接访问磁盘快几个数量级,因此Redis 比其他数据存储快得多。


优化的数据结构




作为内存数据存储,Redis 利用各种底层数据结构来高效存储数据,无需担心如何将它们持久化到持久存储中。


例如,Redis list 是使用链表实现的,它允许在列表的头部和尾部附近进行恒定时间 O(1) 插入和删除。


另一方面,Redis sorted set 是通过跳跃列表实现的,可以实现更快的查询和插入。


简而言之,无需担心数据持久化,Redis 中的数据可以更高效地存储,以便通过不同的数据结构进行快速检索。


单线程




Redis 中的写入和读取速度非常快,并且 CPU 使用率从来不是 Redis 关心的问题。


根据 Redis 官方文档,在普通 Linux 系统上运行时,Redis 每秒最多可以处理 100 万个请求。


通常瓶颈来自于网络 I/O, Redis 中的处理时间大部分浪费在等待网络 I/O 上。


虽然多线程架构允许应用程序通过上下文切换并发处理任务,但这对 Redis 的性能增益很小,因为大多数线程最终会在 I/O 中被阻塞。


所以 Redis 采用单线程架构,有如下好处



  • 最大限度地减少由于线程创建或销毁而产生的 CPU 消耗

  • 最大限度地减少上下文切换造成的 CPU 消耗

  • 减少锁开销,因为多线程应用程序需要锁来进行线程同步,而这容易出现错误

  • 能够使用各种“线程不安全”命令,例如 Lpush


非阻塞I/O




为了处理传入的请求,服务器需要在套接字上执行系统调用,以将数据从网络缓冲区读取到用户空间。


这通常是阻塞操作,线程被阻塞并且在完全接收到来自客户端的数据之前不能执行任何操作。


为什么我们不能在只有确定套接字中的数据已准备好读取时,才执行系统调用嘞?


这就是 I/O 多路复用发挥作用的地方。


I/O 多路复用模块同时监视多个套接字,并且仅返回可读的套接字。


准备读取的套接字被推送到单线程事件循环,并由相应的处理程序使用响应式模型进行处理。


总之,



  • 网络 I/O 速度很慢,因为其阻塞特性,

  • Redis 收到命令后可以快速执行,因为这在内存中执行,操作速度很快,


所以 Redis 做出了以下决定,



  • 使用 I/O 多路复用来缓解网络 I/O 缓慢问题

  • 使用单线程架构减少锁开销


结论




综上所述,单线程架构是 Redis 团队经过深思熟虑的选择,并且经受住了时间的考验。


尽管是单线程,Redis 仍然是性能最高、最常用的内存数据存储之一。

作者:waynaqua
来源:juejin.cn/post/7257783692563611685

收起阅读 »

SpringBoot可以同时处理多少请求?

SpringBoot是一款非常流行的Java后端框架,它可以帮助开发人员快速构建高效的Web应用程序。但是,许多人对于SpringBoot能够同时处理多少请求的疑问仍然存在。在本篇文章中,我们将深入探讨这个问题,并为您提供一些有用的信息。 首先,我们需要了解一...
继续阅读 »

SpringBoot是一款非常流行的Java后端框架,它可以帮助开发人员快速构建高效的Web应用程序。但是,许多人对于SpringBoot能够同时处理多少请求的疑问仍然存在。在本篇文章中,我们将深入探讨这个问题,并为您提供一些有用的信息。


首先,我们需要了解一些基本概念。在Web应用程序中,请求是指客户端向服务器发送的消息,而响应则是服务器向客户端返回的消息。在高流量情况下,服务器需要能够同时处理大量的请求,并且尽可能快地响应这些请求。这就是所谓的“并发处理”。


SpringBoot使用的是Tomcat作为默认的Web服务器。Tomcat是一种轻量级的Web服务器,它可以同时处理大量的请求。具体来说,Tomcat使用线程池来管理请求,每个线程都可以处理一个请求。当有新的请求到达时,Tomcat会从线程池中选择一个空闲的线程来处理该请求。如果没有可用的线程,则该请求将被放入队列中,直到有线程可用为止。


默认情况下,SpringBoot会为每个CPU内核创建一个线程池。例如,如果您的服务器有4个CPU内核,则SpringBoot将创建4个线程池,并在每个线程池中创建一定数量的线程。这样可以确保服务器能够同时处理多个请求,并且不会因为线程过多而导致性能下降。


当然,如果您需要处理大量的请求,您可以通过配置来增加线程池的大小。例如,您可以通过修改application.properties文件中的以下属性来增加Tomcat线程池的大小:


server.tomcat.max-threads=200

上述配置将使Tomcat线程池的最大大小增加到200个线程。请注意,增加线程池大小可能会导致服务器资源消耗过多,因此应该谨慎使用。


除了Tomcat之外,SpringBoot还支持其他一些Web服务器,例如Jetty和Undertow。这些服务器也都具有良好的并发处理能力,并且可以通过配置来调整线程池大小。


最后,需要注意的是,并发处理能力不仅取决于Web服务器本身,还取决于应用程序的设计和实现。如果您的应用程序设计得不够好,那么即使使用最好的Web服务器也无法达到理想的并发处理效果。因此,在开发应用程序时应该注重设计和优化。


总之,SpringBoot可以同时处理大量的请求,并且可以通过配置来增加并发处理能力。但是,在实际应用中需要根据具体情况进行调整,并注重应用程序的设计和优化。希望本篇文章能够帮助您更好地理解SpringBo

作者:韩淼燃
来源:juejin.cn/post/7257732392541618237
ot的并发处理能力。

收起阅读 »

适合小公司的自动化部署脚本

背景(偷懒) 在小小的公司里面,挖呀挖呀挖。快挖不动了,一件事重复个5次,还在人肉手工,身体和心理就开始不舒服了,并且违背了个人的座右铭:“偷懒”是人类进步的第一推动力。 每次想要去测试环境验证个新功能,又或者被测试无情的催促着部署新版本后;都需要本地打那个2...
继续阅读 »

背景(偷懒)


在小小的公司里面,挖呀挖呀挖。快挖不动了,一件事重复个5次,还在人肉手工,身体和心理就开始不舒服了,并且违背了个人的座右铭:“偷懒”是人类进步的第一推动力


每次想要去测试环境验证个新功能,又或者被测试无情的催促着部署新版本后;都需要本地打那个200多M的jar包;以龟速般的每秒几十KB网络,通过ftp上传到服务器;用烂熟透的jps命令查找到进程,kill后,重启服务。


是的,我想偷懒,想从已陷入到手工部署的沼泽地里走出来。如何救赎?


自我救赎之路


我的诉求很简单,想要一款“一键CI/CD的工具”,然后可以继续偷懒。为了省事,我做了以下工作


找了一款停止服务的脚本,并做了小小的优化


首推 陈皮大哥的停服脚本(我在里面加了个sleep 5);脚本见下文。只需要修改 APP_MAINCLASS的变量“XXX-1.0.0.jar”替换为自己jar的名字即可,其它不用动


该脚本主要是通过jps + jar的名字获得进程号,进行kill。( 脚本很简单,注释也很详细,就不展开了,感兴趣可以阅读下,不到5分钟,写过代码的你能看懂的)


把以下脚本保存为stop.sh


#!/bin/bash
# 主类
APP_MAINCLASS="XXX-1.0.0.jar"
# 进程ID
psid=0
# 记录尝试次数
num=0
# 获取进程ID,如果进程不存在则返回0,
# 当然你也可以在启动进程的时候将进程ID写到一个文件中,
# 然后使用的使用读取这个文件即可获取到进程ID
getpid() {
javaps=`jps -l | grep $APP_MAINCLASS`
if [ -n "$javaps" ]; then
psid=`echo $javaps | awk '{print $1}'`
else
psid=0
fi
}
stop() {
getpid
num=`expr $num + 1`
if [ $psid -ne 0 ]; then
# 重试次数小于3次则继续尝试停止服务
if [ "$num" -le 3 ];then
echo "attempt to kill... num:$num"
kill $psid
sleep 5
else
# 重试次数大于3次,则强制停止
echo "force kill..."
kill -9 $psid
fi
# 检查上述命令执行是否成功
if [ $? -eq 0 ]; then
echo "Shutdown success..."
else
echo "Shutdown failed..."
fi
# 重新获取进程ID,如果还存在则重试停止
getpid
if [ $psid -ne 0 ]; then
echo "getpid... num:$psid"
stop
fi
else
echo "App is not running"
fi
}
stop

编写2行的shell 启动脚本


修改脚本中的XXX-1.0.0.jar为你自己的jar名称即可。保存脚本内容为start.sh。jvm参数可自行修改


basepath=$(cd `dirname $0`; pwd)
nohup java -server -Xmx2g -Xms2g -Xmn1024m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseParNewGC -XX:-UseAdaptiveSizePolicy -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -Xloggc:logs/gc.log -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:HeapDumpPath=logs/dump.hprof -XX:ParallelGCThreads=4 -jar $basepath/XXX-1.0.0.jar &>nohup.log &

复用之前jenkins,自己写部署脚本


脚本一定要放到 Post Steps里


1689757456174.png


9行脚本,主要干了几件事:



  • 备份正在运行的jar包;(万一有啥情况,还可以快速回滚)

  • 把jenkins上打好的包,复制到目标服务上

  • 执行停服脚本

  • 执行启动服务脚本


脚本见下文:


ssh -Tq $IP << EOF 
source /etc/profile
#进入应用部署目录
cd /data/app/test
##备份时间戳
DATE=`date +%Y-%m-%d_%H-%M-%S`
#删除备份jar包
rm -rf /data/app/test/xxx-1.0.0.jar.bak*
#备份历史jar包
mv /data/app/test/xxx-1.0.0.jar /data/app/test/xxx-1.0.0.jar.bak$DATE
#从jenkins上拉取最新jar包
scp root@$jenkisIP:/data/jenkins/workspace/test/target/XXX-1.0.0.jar /data/app/test
# 执行停止应用脚本
sh /data/app/test/stop.sh
#执行重启脚本
sh /data/app/test/start.sh
exit
EOF

注:



  • $IP 是部署服务器ip,$jenkisIP 是jenkins所在的服务器ip。 在部署前请设置jenkins服务器和部署服务器之间ssh免密登录

  • /data/app/test 是部署jar包存放路径

  • stop.sh 是上文的停止脚本

  • start.sh 是上文的启动脚本


总结


如果不想把时间浪费在本地打包,忍受不了上传jar包的龟速网络,人肉停服和启动服务。请尝试下这款自动部署化脚本。小小的投入,带来大大的回报。


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


参考资料:

xie.infoq.cn/article/52c…
Linux ----如何使用 kill 命令优雅停止 Java 服务

blog.csdn.net/m0_46897923…

作者:程序员猪佩琪
来源:juejin.cn/post/7257440759569055802
--ssh免密登录

收起阅读 »

前端入门Docker最佳实践

本地安装及相关库 下载 Docker Desktop 双击安装即可。 作用: 打包:就是把你软件运行所需的依赖、第三方库、软件打包到一起,变成一个安装包 分发:你可以把你打包好的“安装包”上传到一个镜像仓库,其他人可以非常方便的获取和安装 部署:拿着“安...
继续阅读 »

本地安装及相关库



  1. 下载 Docker Desktop 双击安装即可。

  2. 作用:



  • 打包:就是把你软件运行所需的依赖、第三方库、软件打包到一起,变成一个安装包

  • 分发:你可以把你打包好的“安装包”上传到一个镜像仓库,其他人可以非常方便的获取和安装

  • 部署:拿着“安装包”就可以一个命令运行起来你的应用,自动模拟出一摸一样的运行环境,不管是在 Windows/Mac/Linux。


启动报错解决



  • 报错截图


image.png



  • 解决方法:


控制面板->程序->启用或关闭 windows 功能,开启 Windows 虚拟化和 Linux 子系统(WSL2)


image.png


命令行安装 Linux 内核


wsl --install -d Ubuntu


设置开机启动 Hypervisor


bcdedit /set hypervisorlaunchtype auto


设置默认使用版本2


wsl --set-default-version 2


查看 WSL 是否安装正确


wsl --list --verbose


应该如下图,可以看到一个 Linux 系统,名字你的不一定跟我的一样,看你安装的是什么版本。


并且 VERSION 是 2


image.png


切换镜像加速源


镜像加速器镜像加速器地址
Docker 中国官方镜像registry.docker-cn.com
DaoCloud 镜像站f1361db2.m.daocloud.io
Azure 中国镜像dockerhub.azk8s.cn
科大镜像站docker.mirrors.ustc.edu.cn
阿里云ud6340vz.mirror.aliyuncs.com
七牛云reg-mirror.qiniu.com
网易云hub-mirror.c.163.com
腾讯云mirror.ccs.tencentyun.com

"registry-mirrors": ["https://registry.docker-cn.com"]


image.png


目录挂载:



  • 使用 Docker 运行后,我们改了项目代码不会立刻生效,需要重新buildrun,很是麻烦。

  • 容器里面产生的数据,例如 log 文件,数据库备份文件,容器删除后就丢失了。


挂载方式



  • bind mount 直接把宿主机目录映射到容器内,适合挂代码目录和配置文件。可挂到多个容器上

  • volume 由容器创建和管理,创建在宿主机,所以删除容器不会丢失,官方推荐,更高效,Linux 文件系统,适合存储数据库数据。可挂到多个容器上

  • tmpfs mount 适合存储临时文件,存宿主机内存中。不可多容器共享。


文档参考:docs.docker.com/storage/


多容器通信


项目往往都不是独立运行的,需要数据库、缓存这些东西配合运作。


文档参考:docs.docker.com/engine/refe…


创建一个名为test-net的网络:


docker network create test-net


运行 Redis 在 test-net 网络中,别名redis


docker run -d --name redis --network test-net --network-alias redis redis:latest


Docker-Compose



  • 如果你是安装的桌面版 Docker,不需要额外安装,已经包含了。

  • 如果是没图形界面的服务器版 Docker,你需要单独安装 安装文档

  • 运行docker-compose检查是否安装成功


要把项目依赖的多个服务集合到一起,我们需要编写一个docker-compose.yml文件,描述依赖哪些服务


参考文档:docs.docker.com/compose/


docker-compose.yml 文件所在目录,执行:docker-compose up就可以跑起来了。


命令参考:docs.docker.com/compose/ref…


常用命令


docker ps 查看当前运行中的容器


docker images 查看镜像列表


docker rm container-id 删除指定 id 的容器


docker stop/start container-id 停止/启动指定 id 的容器


docker rmi image-id 删除指定 id 的镜像


docker volume ls 查看 volume 列表


docker network ls 查看网络列表


编译 docker build -t test:v1 . -t 设置镜像名字和版本号


在后台运行只需要加一个 -d 参数docker-compose up -d


查看运行状态:docker-compose ps


停止运行:docker-compose stop


重启:docker-compose restart


重启单个服务:docker-compose restart service-name


进入容器命令行:docker-compose exec service-name sh


查看容器运行log:docker-compose logs [service-na

作者:StriveToY
来源:juejin.cn/post/7256607606465380411
me]

收起阅读 »

拒绝复杂 if-else,前端策略模式实践

设计模式的重要性 为什么要学习和使用设计模式,我觉得原因主要有两点 解除耦合:设计模式的目的就是把 “不变的” 和 “可变的” 分离开,将 “不变的” 封装为统一对象,“可变的” 在具体实例中实现 定义统一标准:定义一套优秀代码的标准,相当于一份实现优秀代码...
继续阅读 »

设计模式的重要性


为什么要学习和使用设计模式,我觉得原因主要有两点



  1. 解除耦合:设计模式的目的就是把 “不变的” 和 “可变的” 分离开,将 “不变的” 封装为统一对象,“可变的” 在具体实例中实现

  2. 定义统一标准:定义一套优秀代码的标准,相当于一份实现优秀代码的说明书


在前端开发过程中面对复杂场景能能够更清晰的处理代码逻辑,其中策略模式在我的前端工作中的应用非常多,下面就展开讲讲策略模式在前端开发的具体应用


策略模式基础


策略模式的含义是:定义了一系列的算法,并将每个算法封装起来,使它们可以互相替换


我个人对于策略模式的理解,就是将原来写在一个函数中一整套功能,拆分为一个个独立的部分,从而达到解耦的目的。所以策略模式最好的应用场景,就是拆解 if-else,把每个 if 模块封装为独立算法


在面向对象的语言中,策略模式通常有三个部分



  • 策略(Strategy):实现不同算法的接口

  • 具体策略(Concrete Strategy):实现了策略定义的接口,提供具体的算法实现

  • 上下文(Context):持有一个策略对象的引用,用一个ConcreteStrategy 对象来配置,维护一个对 Strategy 对象的引用


这么看定义可能不太直观,这里我用 TS 面向对象的方式实现的一个计算器的策略模式例子说明一下


// 第一步: 定义策略(Strategy)
interface CalculatorStrategy {
calculate(a: number, b: number): number;
}

// 第二步:定义具体策略(Concrete Strategy)
class AddStrategy implements CalculatorStrategy {
calculate(a: number, b: number): number {
return a + b;
}
}

class SubtractStrategy implements CalculatorStrategy {
calculate(a: number, b: number): number {
return a - b;
}
}

class MultiplyStrategy implements CalculatorStrategy {
calculate(a: number, b: number): number {
return a * b;
}
}

// 第三步: 创建上下文(Context),用于调用不同的策略
class CalculatorContext {
private strategy: CalculatorStrategy;

constructor(strategy: CalculatorStrategy) {
this.strategy = strategy;
}

setStrategy(strategy: CalculatorStrategy) {
this.strategy = strategy;
}

calculate(a: number, b: number): number {
return this.strategy.calculate(a, b);
}
}

// 使用策略模式进行计算
const addStrategy = new AddStrategy();
const subtractStrategy = new SubtractStrategy();
const multiplyStrategy = new MultiplyStrategy();

const calculator = new CalculatorContext(addStrategy);
console.log(calculator.calculate(5, 3)); // 输出 8

calculator.setStrategy(subtractStrategy);
console.log(calculator.calculate(5, 3)); // 输出 2

calculator.setStrategy(multiplyStrategy);
console.log(calculator.calculate(5, 3)); // 输出 15

前端策略模式应用


实际上在前端开发中,通常不会使用到面向对象的模式,在前端中应用策略模式,完全可以简化为两个部分



  1. 对象:存储策略算法,并通过 key 匹配对应算法

  2. 策略方法:实现 key 对应的具体策略算法


这里举一个在最近开发过程应用策略模式重构的例子,实现的功能是对于不同的操作,处理相关字段的联动,在原始代码中,对于操作类型 opType 使用大量 if-else 判断,代码大概是这样的,虽然看起来比较少,但是每个 if 里面都有很多处理逻辑的话,整体的可读性的就会非常差了


export function transferAction() {
actions.forEach((action) => {
const { opType } = action

// 展示 / 隐藏字段
if (opType === OP_TYPE_KV.SHOW) { }
else if (opType === OP_TYPE_KV.HIDE) {}
// 启用 / 禁用字段
else if (opType === OP_TYPE_KV.ENABLE) { }
else if (opType === OP_TYPE_KV.DISABLE) {}
// 必填 / 非必填字段
else if (opType === OP_TYPE_KV.REQUIRED) { }
else if ((opType === OP_TYPE_KV.UN_REQUIRED) { }
// 清空字段值
else if (opType === OP_TYPE_KV.CLEAR && isSatify) { }
})
}

在使用策略模式重构之后,将每个 action 封装进单独的方法,再把所用的算法放入一个对象,通过触发条件匹配。这样经过重构后的代码,相比于原来的 if-else 结构更清晰,每次只要找到对应的策略方法实现即可。并且如果后续有扩展,只要继续新的增加策略方法就好,不会影响到老的代码


export function transferAction( /* 参数 */ ) {
/**
* @description 处理字段显示和隐藏
*/

const handleShowAndHide = ({ opType, relativeGroupCode, relativeCode }) => {}

/**
* @description // 启用、禁用字段(支持表格行字段的联动)
*/

const handleEnableAndDisable = ({ opType, relativeGroupCode, relativeCode }) => {}

/**
* @description 必填 / 非必填字段(支持表格行字段的联动)
*/

const handleRequiredAndUnrequired = ({ opType, relativeGroupCode, relativeCode }) => {}

/**
* @description 清空字段值
*/

const handleClear = ({ opType, relativeGroupCode, relativeCode }) => {}

// 联动策略
const strategyMap = {
// 显示、隐藏
[OP_TYPE_KV.SHOW]: handleShowAndHide,
[OP_TYPE_KV.HIDE]: handleShowAndHide,
// 禁用、启用
[OP_TYPE_KV.ENABLE]: handleEnableAndDisable,
[OP_TYPE_KV.DISABLE]: handleEnableAndDisable,
// 必填、非必填
[OP_TYPE_KV.REQUIRED]: handleRequiredAndUnrequired,
[OP_TYPE_KV.UN_REQUIRED]: handleRequiredAndUnrequired,
// 清空字段值
[OP_TYPE_KV.CLEAR]: handleClear,
}

// 遍历执行联动策略
actions.forEach((action) => {
const { opType, relativeGroupCode, relativeCode, value } = action

if (strategyMap[opType]) {
strategyMap[opType]({ /* 入参 */ })
}
})
}

总结


策略模式的优点在于:代码逻辑更清晰,每个策略对对应一个实现方法;同时遵循开闭原则,新的策略方法无需改变已有代码,所以非常适合处理或重构复杂逻辑的 if-else


在前端开发过程中,不需要遵循面向对象的应用方式,只需要通过对象存储策略算法,通过 key 匹配具体策略实现,就可以实

作者:WujieLi
来源:juejin.cn/post/7256721204300202042
现一个基础的策略模式

收起阅读 »

多端登录如何实现踢人下线

1:项目背景 或者你登录了PC端,这时候你登陆了APP或者小程序,这时候PC端的账号也会被强制下线 2:项目只有PC端 假设我们现在的项目只有PC端,没有小程序或者APP,那么这时候就是很简单了,用户的sessin(也就是所谓的Token)一般都是存储在re...
继续阅读 »

1:项目背景



或者你登录了PC端,这时候你登陆了APP或者小程序,这时候PC端的账号也会被强制下线


2:项目只有PC端


假设我们现在的项目只有PC端,没有小程序或者APP,那么这时候就是很简单了,用户的sessin(也就是所谓的Token)一般都是存储在redis中,session中包括用户ID等一些信息,当然还有一个最重要的就是登录的ip地址。


image.png


1:用户在登录的时候,从redis中获取用户session,如果没有就可以直接登录了


2:用户在另外一台电脑登录,从redis中获取到用户session,这时候用户session是有的,说明用户之前已经登录过了


3:这时候从用户session中获取IP,判断二者的ip是不是相同,如果不同,这时候就要发送一个通知给客户端,让另外一台设备登录的账号强制下线即可


3:项目有PC端和APP端和小程序端


当你的应用有PC端和APP端的时候,我们用户的session如果还是只存一个ip地址,那明显就是不够的,因为很多情况下,我们PC端和APP端是可以同时登录的,比如淘宝,京东等都是,也就是所谓的双端登录


这时候就会有多种情况


单端登录:PC端,APP端,小程序只能有一端登录
双端登录:允许其中二个端登录
三端登录:三个端都可以同时登录

对于三端可以同时登录就很简单,但是现在有个限制,就是app端只能登录一次,不能同时登录,也就是我一个手机登录了APP,另外一个手机登录的话,之前登录的APP端就要强制下线


所以我们的用户session存储的格式如下


{
userId:用户的id
clientType:PC端,小程序端,APP端
imei:就是设备的唯一编号(对于PC端这个值就是ip地址,其余的就是手机设备的一个唯一编号)
}


单端登录


首先我们要知道,用户登录不同的设备那么用户session是不一样的。对于单端登录,那么我们可以拿到用户的所有的session,然后根据clientType和imei号来强制将其它端的用户session删除掉,然后通知客户端强制下线


双端登录


同样拿到所有用户的session,然后根据自己的业务需求来判定哪一端需要强制下线,比如我们现在已经登录了PC端和APP端,这时候登录小程序,现在要让APP端的强制下线。


这时候登录之后获取用户所有的session,这时候会有二个用户session,首先拿到clientType = APP的session,然后来通知客户端这个端需要强制下线。


如果这时候我登录了PC端和一个APP端,这时候我用另外一台手机登录APP端,那么之前那台手机上登录的APP端就要被强制下线,这个时候仅通过clientType是不行的,因为我二个手机登录的clientType都是APP端。所以这时候就要根据imei号来判断了。因为不同的手机imei号是不一样的。


这时候我拿到用户所有的session



PC端的session
sessionA{
userId: 1,
clientType: PC,
imei: "123"
}

APP端的session
sessionA{
userId: 1,
clientType: APP,
imei: "12345"
}

这时候我从另外一台手机登录的时候,生成的session应该是这样的


 APP端的session
sessionA{
userId: 1,
clientType: APP,
imei: "1234567"
}

我发现同一个clientType的session已经有了,这时候我要判断imei号是否一样,imei一样说明是同一台设备,不同说明不是同一台设备,我们只需要把对应设备的账号强制下线即可了


总结


不管是单端登录,双端登录还是多端登录,我们都是根据用户session来判断。只要根据clientType和imei号来就可以满足我们大部

作者:我是小趴菜
来源:juejin.cn/post/7213598216884486204
分的踢人下线需求了。

收起阅读 »

网站“重定向次数过多”问题排查

ERR_TOO_MANY_REDIRECTS 不久前部署了一个网站,访问时却直接打不开: 当前无法使用此页面 xxx.com 重定向次数过多 若要解决此问题,请尝试清除 Cookie. ERR_TOO_MANY_REDIRECTS 我的网络架构如下: gr...
继续阅读 »

ERR_TOO_MANY_REDIRECTS


不久前部署了一个网站,访问时却直接打不开:



当前无法使用此页面 xxx.com 重定向次数过多


若要解决此问题,请尝试清除 Cookie.


ERR_TOO_MANY_REDIRECTS



我的网络架构如下:


graph LR
subgraph A["VLAN"]
subgraph B["Local Server"]
C("nginx server")
end
subgraph D["VPS"]
E("nginx server")
end
end
F["Internet"] --http/https--> E
E --http--> C


其中网站部署在 Local Server 的 Nginx 服务器上,在 VPS 上再用 Nginx 做反向代理,并不复杂。


Nginx


VPS 上的 Nginx 主要配置如下:


	upstream mycloud {
# Local server
server archlinux:5173;
}

server {
listen 443 ssl;
server_name xxx.com;
...
#证书
ssl_certificate /data/nginx/cert/xxx.com_bundle.pem;
ssl_certificate_key /data/nginx/cert/xxx.com.key;
...
location / {
proxy_pass http://mycloud;
...
}
}

server {
listen 80;
server_name xxx.com;
...
#核心转发代码
rewrite ^(.*)$ https://${server_name}$1 permanent;
}

上述配置主要做了两个事情,一是将访问的 https 流量代理到 archlinux:5173,二是将访问的 http 请求转发到 https。


怎么看都不像能导致无限重定向的样子。


再三确认配置无误后,我看了一眼浏览器 Network,在一众 301 的列表中,请求全被重定向到了一个网址:104.21.27.176,再一查,好家伙,原来是 CloudFlare Load Balancer!


这时我才想起来曾经在 Cloudflare 上开启了 DNS 服务。


Cloudflare


那么问题来了,会是 DNS 服务导致的无限重定向吗?


登录 Cloudflare 看了一下,SSL/TLS 加密模式 设置成了 flexible , 这下真相大白了!


已知 SSL/TLS加密模式 有如下选项:





  • 关闭(不安全)


    未应用加密




  • 灵活


    加密浏览器与 Cloudflare 之间的流量




  • 完全


    端到端加密,使用服务器上的自签名证书




  • 完全(严格)


    端到端加密,但服务器上需要有受信任的 CA 证书或 Cloudflare Origin CA 证书





由于应用的是 灵活 , 所以只加密了浏览器与 Cloudflare 之间的流量,并没有加密 Cloudflare 到 VPS 服务器的流量。


什么原理呢:


当域名的 DNS 记录指向 Cloudflare 后,所有的流量都将经过 Cloudflare 的代理服务器。


而在 Nginx 配置中,我使用了 rewrite 规则将 HTTP 请求重定向到 HTTPS。然而,由于 Cloudflare 代理了流量,它将请求转发给 VPS 服务器时,仍然是通过 HTTP 连接进行。这导致了一个循环:请求通过 HTTPS 到达 Cloudflare,然后被转发为 HTTP 请求到 VPS,然后 VPS 再次重定向到 HTTPS, 无限循环了。


问题清楚了,解决方案有如下选择:




  1. 在 Nginx 中取消 http 到 https 的 rewrite。




  2. 加密 Cloudflare 到 VPS 的流量。




  3. 在 Nginx 配置中使用代理服务器的原始协议(X-Forwarded-Proto)来判断是否启用 HTTPS,例如


    if ($http_x_forwarded_proto != "https") {
    rewrite ^(.*)$ https://${server_name}$1 permanent;
    }

    这将确保当请求通过 HTTPS 到达 Cloudflare 时,Cloudflare 会在转发请求时设置 X-Forwarded-Proto 头部字段为 https,然后服务器将检查该字段,并决定是否进行重定向。




毫无疑问最简单高效的方法是直接将 SSL/TLS 加密模式 设置成 完全(严格) 就行了。


点击一下选项,测试网页,问题解决。


Conclusion


从这个事情需要认识到,在使用 Cloudflare 或者其它服务时,要确保 Nginx 配置和 Cloudflare 设置之间的一致性,以避免任何不必要

作者:looko
来源:juejin.cn/post/7254572372136738853
的重定向或连接问题。

收起阅读 »

当你的服务挂了,该怎么排查服务挂了的原因

1. 背景 某天凌晨一点多,服务挂了,日志戛然而止,grafanar监控内存,cpu、磁盘都是正常的,该怎么去排查 2. 排查手段 观测日志、是否有程序触发关闭jvm进程,system.exit(),观测内存,cpu,磁盘,是否有因为机器资源不够分配的问题导...
继续阅读 »

1. 背景


某天凌晨一点多,服务挂了,日志戛然而止,grafanar监控内存,cpu、磁盘都是正常的,该怎么去排查


2. 排查手段



观测日志、是否有程序触发关闭jvm进程,system.exit(),观测内存,cpu,磁盘,是否有因为机器资源不够分配的问题导致进程被机器kill




观测机器的操作日志/var/log/messages,直接搜索kill 的日志



messages 日志是核心系统日志文件。它包含了系统启动时的引导消息,以及系统运行时的其他状态消息。IO 错误、网络错误和其他系统错误都会记录到这个文件中。其他信息,比如某个人的身份切换为 root,也在这里列出。如果服务正在运行,比如 DHCP 服务器,您可以在 messages 文件中观察它的活动。通常,/var/log/messages 是您在做故障诊断时首先要查看的文件。


/var/log/messages文件中存放的就是系统的日志信息,当内核程序调试时,printk语句所产生的信息显示不出来的时候,就使用cat /var/log/messages文件的方法,查看所打印出的信息.



都没问题了,直接联系运维看一下,开发也没有绝对百分百的手段判断进程挂的原因,最终判断为阿里云系统错误导致机器重启


作者:斯瓦辛武
来源:juejin.cn/post/7254542743098818621

收起阅读 »

了解短信的实现原理以及验证码短信API

前言 短信作为一种便捷、快速的通信方式,已经在我们的日常生活中得到广泛应用。无论是个人通信、企业沟通还是身份验证等场景,短信都发挥着重要的作用。而实现短信功能的核心是短信实现原理和验证码短信API。 本文将介绍短信实现的基本原理以及 验证码短信API,帮助读者...
继续阅读 »

前言


短信作为一种便捷、快速的通信方式,已经在我们的日常生活中得到广泛应用。无论是个人通信、企业沟通还是身份验证等场景,短信都发挥着重要的作用。而实现短信功能的核心是短信实现原理和验证码短信API。


本文将介绍短信实现的基本原理以及 验证码短信API,帮助读者更好地了解短信技术和应用。


实现原理(步骤)





  1. 触发事件:通知短信的实现通常是作为某种事件的响应而触发的。例如,用户完成了注册、下单、密码重置等操作,这些事件可以触发发送通知短信。




  2. 业务逻辑处理:在触发事件后,相关的业务逻辑将被执行。这可能包括生成通知内容、确定接收者等。




  3. 调用短信服务提供商的API:为了发送短信,系统将调用短信服务提供商的API。这些提供商通常是专门的短信网关或通信运营商,提供发送短信的基础设施和服务。




  4. 构建短信内容:在调用短信服务提供商的API之前,系统需要构建短信的内容。这包括编写文本消息、添加动态变量或链接等。通常,短信内容可以包含特定的占位符,用于在发送时插入动态数据,如用户名、订单号等。




  5. 调用短信服务API发送短信:使用短信服务提供商的API,系统将发送短信请求。这通常涉及向API端点发送HTTP请求,包括目标手机号码、短信内容和身份验证信息等。




  6. 短信服务商处理:短信服务提供商接收到发送短信的请求后,会进行一系列的处理步骤。这可能包括验证发送者的身份、检查短信内容的合法性、处理短信队列等。




  7. 短信传递:一旦短信服务提供商完成处理,它会将短信传递到相应的目标手机号码。这通常是通过与移动网络运营商之间的通信渠道实现的。




  8. 接收短信:目标手机号码的手机将接收到短信,并在短信应用程序中显示。用户可以查看和阅读通知短信的内容。




验证码短信API



在短信实现原理中,必不可少的一个东西就是 —— 短信API,只有调用了 短信API 我们才能把短信发送出去。


在 短信API 中最常见的就是 验证码短信API通知短信API。在之前说过了通知短信,今天就说一说 验证码短信API。短信API 我们可以去网上各个平台查看,我这里使用的是 APISpace验证码短信API~


以 JavaScript 为例的调用示例代码:


var data = "{"msg":"Eolinker】尊敬的用户{$var},欢迎联调通知短信。","params":"15800000000,张先生;13200000000,王小姐","sendtime":"","extend":"","uid":""}"

$.ajax({
"url":"https://eolink.o.apispace.com/sms-notify/notify",
"method": "POST",
"headers": {
"X-APISpace-Token":"",
"Authorization-Type":"apikey",
"Content-Type":"application/json"
},
"data": data,
"crossDomain": true
})
.done(function(response){})
.fail(function(jqXHR){})

验证码短信应用场景




  1. 注册和登录验证:许多网站、应用和服务在用户注册和登录过程中使用验证码短信来验证用户的身份。用户在提供手机号码后,会收到包含验证码的短信,然后需要输入验证码才能完成注册或登录过程。这样可以确保用户提供的手机号码是有效的,并增加账户的安全性。




  2. 密码重置和账户安全:当用户忘记密码或账户出现异常时,验证码短信可以用于重置密码或确保账户安全。通过发送验证码短信,用户可以通过验证自己的身份来重新设置密码,或者确认是否进行了某些账户操作,如更改手机号码或绑定新设备。




  3. 手机号码验证:许多平台需要验证用户提供的手机号码的真实性,以保护用户账户的安全性。验证码短信可以用于验证用户拥有指定手机号码,并通过让用户输入验证码来确认其所有权。




  4. 交易和支付安全:在电子商务和移动支付中,验证码短信被广泛用于交易和支付的安全验证。用户在进行支付或敏感操作时,会收到包含验证码的短信,需要输入正确的验证码才能完成交易或操作,以防止未经授权的访问和欺诈行为。




  5. 帐户活动通知:验证码短信也可以用于向用户发送帐户活动通知,例如当用户进行重要操作、更改账户信息、进行高风险活动等时,发送验证码短信以提醒用户并增加账户的安全性。




结束语


通过本文的介绍,我们对短信实现原理以及 验证码短信API 有了一定的了解。短信作为一种简单而高效的通信方式,在各个领域都发挥着重要的作用。验证码短信API为开发者提供了便捷的工具,使他们能够轻松地集成和使用验证码短信功能。无论是个人用户还是企业开发者,都可以利用短信技术和API来实现更安全、高效的通信和身份验证。随着移动通信技术的不断发展,我们相信短信技术将继续在各个领域发挥重要作用,并为我们的生活带

作者:爱分享的程序员
来源:juejin.cn/post/7254384497658429499
来更多便利和安全性。

收起阅读 »

我的师父把 「JWT 令牌」玩到了极致

你好,我是悟空。 我的师父是唐玄奘~ 西游记的故事想必大家在暑假看过很多遍了,为了取得真经,唐玄奘历经苦难,终于达成。 在途经各国的时候,唐玄奘都会拿出一个通关文牒交给当地的国王进行盖章,方能通过。 本篇目录如下: 通关文牒 通关文牒就是唐朝官方发的一个凭证...
继续阅读 »

你好,我是悟空。


我的师父是唐玄奘~


西游记的故事想必大家在暑假看过很多遍了,为了取得真经,唐玄奘历经苦难,终于达成。


在途经各国的时候,唐玄奘都会拿出一个通关文牒交给当地的国王进行盖章,方能通过。


本篇目录如下:


图片


通关文牒


通关文牒就是唐朝官方发的一个凭证,证明持有人来自东土大唐,一般是使臣持有。


有了这个凭证后,到其他国家,比如女儿国国王看到这个凭证后,就会放行。


下面来一张西游记中通关文牒的生命周期图。


图片


长安是一个颁发凭证(通关文牒)的微服务节点,乌鸡国、女儿国和大雷音寺等都是集群中的一个微服务节点,唐玄奘拿着凭证访问各国。


那为什么别的国家认可这个凭证呢?


那是因为当时的唐朝非常强大,有很多国家都要向唐朝朝贡,与唐朝交好有很多好处的~


朝贡也有篇故事哦~唐太宗把微服务的“心跳机制”玩到了极致!


唐太宗在通关文牒上写道:“倘到西邦诸国,不灭善缘,照牒放行,须至牒者。


图片


意思就是说唐玄奘法师是我们唐朝的使臣,如果途经诸侯国,希望大家放行。


贞观之治时期的唐朝是在经济文化上都无比繁盛,国力强盛,周边国家都希望和唐朝建立友好关系,看到是唐朝使臣来了,好生招待下,然后盖章放行,给唐朝留个好印象。


在安全架构中,凭证 出现得太频繁了,比如我们在网关这一层加的校验令牌,其实就是校验凭证。


凭证是什么


凭证(Credentials)的出现就是系统保证它与用户之间的承诺是双方当时真实意图的体现,是准确、完整且不可抵赖的。


那唐太宗给唐玄奘的通关文牒就是一个凭证,上面盖着唐朝的官印、唐太宗的亲笔,这充分体现了持有者是拥有一个可信的令牌的,而且这个通关文牒上的官印是不可篡改的,如果改了,其他国家就不认了


上面这种模式其实对应的是一种普通的认证授权模式,而大名鼎鼎的 OAuth 2.0 认证授权模式虽然有五种模式,但他们殊途同归,最后的目的都是生成一个凭证给到客户端,让客户端持有这个凭证来访问资源。关于 OAuth2.0 本篇不做展开。


关于凭证的存储方案,业界的安全架构中有两种方案:



  • Cookie-Session 模式

  • JWT 方案


Cookie-Session 模式


流程图如下:


图片


用户登录认证通过后,后端会存放该客户端的身份信息,也就是存放到 session 中,session 可以用来区分不同,然后返回一个 sessionId 给到客户端。


客户端将 sessionId 缓存在客户端。当客户端下次发送 HTTP 请求时,在 header 的 cookie 字段附带着 sessionId 发送给后端服务器。


后端服务器拿到 header 中的 sessionId,然后根据 sessionId 找到 session,如果 session 存在,则从 session 中解析出用户的身份信息,然后执行业务逻辑。


我们都知道 HTTP 协议是一种无状态的传输协议,无状态表示对一个事务的处理没有上下文的记忆能力,每一个 HTTP 请求都是完全独立的。但是 Cookie-Seesion 模式却和 HTTP 无状态特性相悖,因为客户端访问资源时,是携带第一次拿到的 sessionId 的,让服务端能够顺利区分出发送请求的用户是谁。


服务端对 session 的管理,就是一种状态管理机制,该机制存储了每个在线用户的上下文状态,再加上一些超时自动清理的管理措施。Cookie-Session 也是最传统但今天依旧应用到大量系统中,由服务端与客户端联动来完成的状态管理机制。


放到西游记中,如果用这种 Cookie-Session 模式是怎么样的呢?



我们把唐朝和周边国家想想成一个分布式集群,所有国家都需要将唐玄奘这个使者信息都保存一份(分布式存储),当唐玄奘路过某个国家时,需要查询本地存储中是否有唐玄奘,如果有,则认为唐玄奘是合法的使者,可以放行。



但是这种方式就会需要每个国家都同步保存,同步的成本是非常高昂的,而且会有同步延迟的存在


Cookie-Session 模式的优势


状态信息都存储于服务器,只要依靠客户端的同源策略和 HTTPS 的传输层安全,保证 Cookie 中的键值不被窃取而出现被冒认身份的情况,就能完全规避掉上下文信息在传输过程中被泄漏和篡改的风险。Cookie-Session 方案的另一大优点是服务端有主动的状态管理能力,可根据自己的意愿随时修改、清除任意上下文信息,譬如很轻易就能实现强制某用户下线的这样功能。(来自凤凰架构)


Cookie-Session 模式的劣势


在单节点的单体服务中再适合不过,但是如果需要水平扩展要部署集群就很麻烦。


如果让 session 分配到不同的的节点上,不重复地保存着一部分用户的状态,用户的请求固定分配到对应的节点上,如果某个节点崩溃了,则里面的用户状态就会完全丢失。如果让 session 复制到所有节点上,那么同步的成本又会很高。


而为了解决分布式下的认证授权问题,并顺带解决少量状态的问题,就有了 JWT 令牌方案,但是 JWT 令牌和 Cookie-Session 并不是完全对等的解决方案,JWT 只能处理认证授权问题,且不能说 JWT 比 Cookie-Session 更加先进,也不可能全面取代 Cookie-Seesion 机制。


JWT 方案


我们上面说到 Cookie-Session 机制在分布式环境下会遇到一致性和同步成本的问题,而且如果在多方系统中,则更不能将 Session 共享存放在多方系统的服务端中,即使服务端之间能共享数据,Cookie 也没有办法跨域。


转换思路,服务端不保存任何状态信息,由客户端来存储,每次发送请求时携带这个状态信息发给后端服务。原理图如下所示:


图片


但是这种方式无法携带大量信息,而且有泄漏和篡改的安全风险。信息量大小受限没有比较好的解决方案,但是确保信息不被中间人篡改则可以借助 JWT 方案。


JWT(JSON WEB TOKEN)是一种令牌格式,经常与 OAuth2.0 配合应用于分布式、多方系统的应用系统中。


我们先来看下 JWT 的格式长什么样:


图片


以上截图来自 JWT 官网(jwt.io),数据则是悟空随意编的。


左边的字符串就是 JWT 令牌,JWT 令牌是服务端生成的,客户端会拿着这个 JWT 令牌在每次发送请求时放到 HTTP header 中。


而右边是 JWT 经过 Base64 解码后展示的明文内容,而这段明文内容的最下方,又有一个签名内容,可以防止内容篡改,但是不能解决泄漏的问题。


JWT 格式


JWT 令牌是以 JSON 结构存储,用点号分割为三个部分。


图片


第一部分是令牌头(Header),内容如下所示:


{
  "alg": "HS256",
  "typ": "JWT"
}

它描述了令牌的类型(统一为 typ:JWT)以及令牌签名的算法,示例中 HS256 为 HMAC SHA256 算法的缩写,其他各种系统支持的签名算法可以参考jwt.io/网站所列。


令牌的第二部分是负载(Payload),这是令牌真正需要向服务端传递的信息。但是服务端不会直接用这个负载,而是通过加密传过来的 Header 和 Payload 后再比对签名是否一致来判断负载是否被篡改,如果没有被篡改,才能用 Payload 中的内容。因为负载只是做了 base64 编码,并不是加密,所以是不安全的,千万别把敏感信息比如密码放到负载里面。


{
  "sub": "passjava",
  "name": "悟空聊架构",
  "iat": 1516239022
}

令牌的第三部分是签名(Signature),使用在对象头中公开的特定签名算法,通过特定的密钥(Secret,由服务器进行保密,不能公开)对前面两部分内容进行加密计算,以例子里使用的 JWT 默认的 HMAC SHA256 算法为例,将通过以下公式产生签名值:


HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload) , secret)

签名的意义:确保负载中的信息是可信的、没有被篡改的,也没有在传输过程中丢失任何信息。因为被签名的内容哪怕发生了一个字节的变动,也会导致整个签名发生显著变化。此外,由于签名这件事情只能由认证授权服务器完成(只有它知道 Secret),任何人都无法在篡改后重新计算出合法的签名值,所以服务端才能够完全信任客户端传上来的 JWT 中的负载信息。


JWT 的优势



  • 无状态:不需要服务端保存 JWT 令牌,也就是说不需要服务节点保留任何一点状态信息,就能在后续的请求中完成认证功能。

  • 天然的扩容便利:服务做水平扩容不用考虑 JWT 令牌,而 Cookie-Session 是需要考虑扩容后服务节点如何存储 Session 的。

  • 不依赖 Cookie:JWT 可以存放在浏览器的 LocalStorage,不一定非要存储在 Cookie 中。


JWT 的劣势



  • 令牌难以主动失效:JWT 令牌签发后,理论上和认证的服务器就没有什么关系了,到期之前始终有效。除非服务器加些特殊的逻辑处理来缓存 JWT,并来管理 JWT 的生命周期,但是这种方式又会退化成有状态服务。而这种要求有状态的需求又很常见:譬如用户退出后,需要重新输入用户名和密码才能登录;或者用户只允许在一台设备登录,登录到另外一台设备,要求强行退出。但是这种有状态的模式,降低了 JWT 本身的价值。

  • 更容易遭受重放攻击:Cookie-Session 也有重放攻击的问题,也就是客户端可以拿着这个 cookie 不断发送大量请求,对系统性能造成影响。但是因为 Session 在服务端也有一份,服务端可以控制 session 的生命周期,应对重放攻击更加主动一些。但是 JWT 的重放攻击对于服务端来说就很被动,比如通过客户端的验证码、服务端限流或者缩短令牌有效期,应用起来都会麻烦些。

  • 存在泄漏的风险:客户端存储,很有可能泄漏出去,被其他人重复利用。

  • 信息大小有限:HTTP 协议并没有强制约束 Header 的最大长度,但是服务器、浏览器会做限制。而且如果令牌很大还会消耗传输带宽。


真假美猴王


西游记中还有一个章节,假的美猴王带着通关文牒和其他行李跑到了花果山,还想自行取经,这不就是盗用  JWT 令牌了吗?


如何使用 JWT


Java 有现成的工具类可以使用,而且校验 JWT 的工作可以统一交给网关来做,这个就是下一篇要重点讲解的实战内容了。


总结


唐玄奘就好比客户端,通关文牒就好比 JWT 令牌,经过的每个国家就好比集群中的微服务。


唐玄奘借助 JWT 令牌的认证授权模式,一路通关,最终取得真经,是不是很酷呀~


下一篇:手摸手实战 Spring Cloud Gateway + JWT 认证功能


参考资料:


《凤凰架构》


《OAuth2.0 实战》

作者:悟空聊架构
来源:juejin.cn/post/7250029300820869178

收起阅读 »