注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

接口优化🚀68474ms->1329ms

小菜的一次接口优化:从68474ms到1329ms 前言 突然,有人大喊一声:小菜,你过来一下 小菜被吓得抖了一抖,连忙切出开发界面,看了一眼,原来是项目经理在喊 小菜屁颠屁颠的过去后 项目经理:小菜,有空你看看后台管理里的商品信息导出Excel功能,导出数据...
继续阅读 »

小菜的一次接口优化:从68474ms到1329ms


前言


突然,有人大喊一声:小菜,你过来一下


小菜被吓得抖了一抖,连忙切出开发界面,看了一眼,原来是项目经理在喊


小菜屁颠屁颠的过去后


项目经理:小菜,有空你看看后台管理里的商品信息导出Excel功能,导出数据只有几千条但是要等特别久


小菜:没问题,等我忙完手上的活就来看看怎么回事


分析与优化


小菜回到工位后,立马看了看后台管理系统的商品信息导出功能


该功能是通过导入规定格式的Excel(比如商品名称),然后导出这些商品的所有信息


小菜用对应的模板(大概数据量5千)使用此功能,大概等了1分钟多才导出结果


小菜:我可以先用arthas的trace监听这个接口,看看接口里哪些方法耗时,再具体进行分析


5ebf0da94e3d187b59b79e81cb66baa.png


使用arthas的trace命令监听端口后,发现总耗时70284ms,其中XXMessage耗时68s,导出Excel花费1.8s


小菜:那具体的业务处理应该在XXMessage里了,我先来看看


     public List<ExportVO> xxMessage(MultipartFile file, HttpServletRequest request, HttpServletResponse response) {
//导出结果
List<ExportVO> exportVOS = new ArrayList<>();
try {
//EasyExcel 读取模板数据
//使用AnalysisEventListener 在读取数据时加入导出结果,在读完后进行封装操作
EasyExcel.read(file.getInputStream(), Product.class, new AnalysisEventListener<Product>() {
private final List<Product> list = new ArrayList<>();

//解析完一行后如何处理
@Override
public void invoke(Product p, AnalysisContext analysisContext) {
doLine(p);
}

//解析完所有数据后如何处理
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
doAfter();
}
}).sheet().doRead();
} catch (IOException e) {
e.printStackTrace();
}
return exportVOS;
}

小菜:使用的EasyExcel读取,那真正的处理应该在实现的AnalysisEventListener中


小菜:让我先来看看每解析一行如何处理的


 //存储要处理的数据
List<Product> products = new ArrayList<>();

private void doLine(Product data) {
try {
//拿到所有使用ExcelProperty的字段
List<Field> fields = Arrays.stream(data.getClass().getDeclaredFields())
.filter(f -> f.isAnnotationPresent(ExcelProperty.class))
.collect(Collectors.toList());

//判断字段是否为空,为空则集合添加false不为空添加true
List<Boolean> lines = new ArrayList<>(fields.size());
for (Field field : fields) {
field.setAccessible(true);
Object value = field.get(data);
if (value == null) {
lines.add(Boolean.TRUE);
} else {
lines.add(Boolean.FALSE);
}
}

//ExcelProperty的所有字段不为空 就加入集合
if(lines.stream().allMatch(Boolean.TRUE::equals)){
products.add(data);
}
} catch (Exception e) {
log.error("parse data fail: " + e.getMessage());
}
}

(ExcelProperty注解用于标记表格中的列)


小菜拿出做算法题分析时间复杂度的思路


小菜:这里总共有三个循环分别是:获取使用ExcelProperty的字段、判断每个字段是否为空、allMatch匹配数组中所有元素为true


小菜:那么用时间复杂度表示就是O(3N),N为数据量,而这些集合的数据量则是使用ExcelProperty的字段,好像是固定的,并不会随着Excel表格中数据量的提升而提升,那么可以把它们看成常量,那最终时间复杂度就是常量级别O(1)


小菜:但是还用了反射会有些性能开销


小菜:咦?为啥要判断实体每个字段不为空才加入要处理的集合呢?


小菜:好像直接判断该商品名不为空就可以了吧?


小菜:用反射来实现通用性,难道这段代码是前辈复制的?


于是,小菜洋洋得意的将代码改成:


 //存储要处理的数据
List<Product> products = new ArrayList<>();

private void doLine(Product data) {
if (StringUtils.isNotEmpty(data.getProductName())) {
products.add(data);
}
}

为了担心自己改错,小菜还保留原始代码,方便回滚


再来看下解析完数据后的处理方法


小菜看着这一望无际一百多行没有注释、多层if嵌套的代码,整个人都呆了


大致观看了一遍后,小菜将shit mountain代码梳理成以下代码:


 //存储要处理的数据
List<Product> products = new ArrayList<>();

private void doLine(Product data) {
if (StringUtils.isNotEmpty(data.getProductName())) {
products.add(data);
}
}

private void doAfter() {
//要处理的数据为空直接返回
if (Empty.isEmpty(products)) {
return;
}

//循环处理数据
products.forEach(product->{
//根据商品名查询出商品列表 IO
List<Sku> skus = skuService.list(product.getProductName());
//查到商品数据为空跳过
if (Empty.isEmpty(skus)) {
continue;
}

//查询商品具体数据 IO

//查询分类、规格... IO

//封装实体 添加到导出列表
});
}

看到这里小菜一下就明白为什么接口这么慢了


小菜:好好好,你这样写代码是吧


小菜:不考虑查数据库的网络IO是吧,肯定是不想写联表SQL,偷懒直接用MP


小菜直接用一次联表查询替代这么多的查询,为了避免数据量太大,小菜设置每次处理的最大数据量,分多次处理


 private void doAfter() {
//要处理的数据为空直接返回
if (Empty.isEmpty(products)) {
return;
}

int batchSize = 520;
//将大集合拆分为多个小集合 分批次处理
List<List<Product>> lists = CollectionUtils.split(products,batchSize);

//循环处理数据
lists.forEach(products->{
//转换为商品名列表
List<String> productNames = products.stream().map(Product::getProductName).collect(Collectors.toList());
//联表查询 IO
List<SkuDetails> skus = skuService.list(productNames);
skus.forEach(skus->{
//封装实体 添加到导出列表
});
});
}

小菜优化完代码后,再用arthas监听一遍,发现这次只需要3s,速度提升近23倍


0dadfdaf5e5b37810bf2266d03a2250.png


最后


接口优化的方式有很多种,在优化前我们需要进行分析哪里需要优化


在平时的开发中,也要多考虑时间、空间复杂度,并不是什么场景下都要去避免关联多张表查询


循环查数据库会造成多次网络IO,等待时间会很久,需要降低网络IO的次数,这种场景就可以联表查询


如果担心查的数据太多,联表查询性能慢,可以考虑分析执行计划增加索引,又或者分批次进行处理


其他接口优化的方式还有很多种,比如数据库优化、缓存、异步....


缓存,可以使用本地缓存、分布式缓存、多级缓存,但引入缓存又会带来一致性问题,要分析业务场景使用适合使用缓存


异步,可以使用MQ去做异步,也可以使用多线程去做异步,各有各的特点


在一些业务场景中,不要为了使用某项技术而去使用


技术是用来服务业务的,使用技术前要考虑到当前项目采用该技术是否合适,就像找伴侣一样,强扭的瓜不甜


有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~


关注菜菜,分享更多干货,公众号:菜菜的后端私房菜


彩蛋


小菜装作忧愁的来到项目经理旁边


小菜:经理,这个接口对应的实现有些复杂,我估计下周忙完手上的事情就可以优化,你先帮我提个需求吧


项目经理:ok没问题,下周忙完就尽快优化吧


作者:菜菜的后端私房菜
来源:juejin.cn/post/7280007832702795795
收起阅读 »

几条SQL把服务器干崩了

大家好,我是冰河~~ 今天跟大家分享一个发生在今天凌晨的真实案例,这篇文章也是我事后临时写出来的,处理事情的过程有点无语,又有点气愤! 事件背景 事情的背景是这样的:一个朋友今年年初新开了一家公司,自己是公司的老板,不懂啥技术,主要负责公司的战略规划和经营管理...
继续阅读 »

大家好,我是冰河~~


今天跟大家分享一个发生在今天凌晨的真实案例,这篇文章也是我事后临时写出来的,处理事情的过程有点无语,又有点气愤!


事件背景


事情的背景是这样的:一个朋友今年年初新开了一家公司,自己是公司的老板,不懂啥技术,主要负责公司的战略规划和经营管理,但是他们公司的很多事情他都会过问。手下员工30多人,涵盖技术、产品、运营和推广,从成立之初,一直在做一款社交类的APP。平时,我们一直保持联系,我有时也会帮他们公司处理下技术问题。


事件经过


今天凌晨,我被电话铃声吵醒了,一看是这个朋友打来的,说是他们公司数据库服务器CPU被打满了,并且一直持续这个状态,他说拉个群,把他们后端Java同事拉进来一起沟通下,让我帮忙看看是什么问题,尽快处理下。说话语气很急,听的出来事态很严重,因为目前正在加大力度推广,周末使用人数也比较多,出现这种问题着实让人着急。


后面我加了那个朋友拉的微信群,开始了解服务器出现问题的具体情况,下面就是一些处理的经过了。


注:聊天内容已经获得授权公布。


2023-10-29-006.png


他们后端Java把运维发的监控截图发出来了,咱继续跟他沟通。


2023-10-29-007.png


为啥我说CPU占用高呢?大家看下他们运维发的图就知道了。


2023-10-29-001.png


CPU已经飙升到了400%了,数据库服务器基本已经卡死。拿到他给我发的SQL后,我跟他们老板要了一份他们的数据库表结构,在我电脑上执行了下查询计划。


2023-10-29-008.png


这不看不知道,一看吓一跳,一个C端频繁访问的接口SQL性能极差,Using temporary、Using filesort、Using join buffer、Block Nested Loop全出来了。


我把这个图发出去了,也结合他们团队的实际情况,给出了优化的目标建议:SQL中不要出现Using filesort、Block Nested Loop,尽量不要出现Using join buffer和Using temporary,把Using where尽量优化到Using Index级别。


2023-10-29-009.png


说是尽量不要出现Using join buffer和Using temporary,把Using where尽量优化到Using Index级别,就是怕他们做不到这点,优先把Using filesort、Block Nested Loop优化掉。 但是这货后面说的话实属把我震惊到了。


2023-10-29-封面.png


我看完他的回复,直接有点无语:卧槽,不超过500万rows效率很高?你这SQL 500万数据效果很高?更让我无语的是这货说MySQL一般一亿以上数据量开始优化,这特么不是完全扯淡吗?他说这话时,我大概就知道这货的水平了。。。


后面我就问他说的这些数据的依据是哪里来的。


2023-10-29-011.png


这货说是什么大数据高并发MySQL数据库压测出来的,稍微有过压测经验的应该都知道,压测一个很重要的前提就是要明确压测的环境,最起码要明确压测环境服务器的CPU核数和内存,直接来句MySQL一亿数据是大数据高并发MySQL数据库压测出来的结果,这还是MySQL官方的数据。。。。


不知道是不是因为群里有他们老板的缘故,这货后面还在狡辩。


2023-10-29-012.png


沟通到这里,我特么有种想打人的冲动,生产环境所有业务快被数据库拖死了,数据库服务器CPU被干爆了,监控到慢SQL,并且查看这些慢SQL的执行计划,性能非常低下,SQL里不管是select部分还是where部分大量使用了MySQL自带的函数,这不慢才怪啊。看这货处理问题的态度,要是我下面的人,早就让他卷铺盖走人了。


处理结果


后续我跟他们老板要了一个代码只读权限的账号,将代码拉取下来后,好家伙,到处都是这种SQL查询,要是一两处还好,把SQL修改并优化下,关联的业务逻辑调整下,再把功能测试下,接口压测下,没啥问题就可以发版上线了。


但是,如果到处都是这种SQL的话,那优化SQL就要花费一定的时间了,甚至是新发布的重大功能逻辑都要重写。最终,我跟他们老板说的是回滚版本吧,最新的功能还是先下线,把新功能的SQL、缓存、业务逻辑、接口都优化好,压测没问题后再重新上线。


2023-10-29-013.png


事后总结


无论什么时候,生产环境一旦出现致命问题,第一时间要优先恢复生产环境正常访问,随后再认真排查、定位和解决问题,毕竟生产环境一旦出现问题,每一秒流失的都是真金白银。


搭建技术团队一定要找靠谱的人,最起码团队的核心人员要靠谱,像我朋友团队的这个技术,在他的认知里,MySQL执行计划出现Using temporary、Using filesort、Using join buffer、Block Nested Loop,500W rows效率都很高,殊不知他们生产环境实际主表数据才10几条,要是真达到500W量级就别查询了,数据库直接就趴下了。还有这个MySQL一般一亿以上开始优化,这个依据我也不知道这货是从哪里看到的,并且还说了大数据高并发MySQL数据库压测出来的,这不纯属扯淡吗?


更离谱的是我事后悄悄问了他们老板,他的工作年限是多久,据说工作10多年了,是位80后。


顿时让我想到了一句话:人的认知有几个层次:不知道自己不知道,知道自己不知道,知道自己知道,不知道自己知道。


好了,今天就到这儿吧,我是冰河,我们下期见~~


作者:冰_河
来源:juejin.cn/post/7298635800631459892
收起阅读 »

Nginx与洗脚城技师那些有意思的小故事

FBI WARNING: 内容仅作为举例用途,本人不曾去过洗脚城,也不知道技师是什么 我们今天来讲讲Nginx的一些小常识吧。 什么是Nginx Nginx 是一款是由伟大且牛逼的俄罗斯的程序设计师 Igor Sysoev(伊戈尔 赛索耶夫) 在摸鱼玩耍时...
继续阅读 »

FBI WARNING:



内容仅作为举例用途,本人不曾去过洗脚城,也不知道技师是什么



我们今天来讲讲Nginx的一些小常识吧。


什么是Nginx


Nginx 是一款是由伟大且牛逼的俄罗斯的程序设计师 Igor Sysoev(伊戈尔 赛索耶夫) 在摸鱼玩耍时所开发高性能的 Web反向代理 服务器,也是一个 IMAP/POP3/SMTP 代理服务器。


什么是反向代理


说到反向代理之前,我们来看看什么是 正向代理(跑腿代购)。


正向代理运行在客户端的出口,获取客户端发送的请求,用自己的身份发送到服务器并获取返回数据,最终返回给客户端。


常见的正向代理,如网络爬虫的IP代理、VPN、游戏加速器......


image.png


那么,什么是反向代理呢?


反向代理服务器运行在服务端的入口,根据客户端请求的部分参数进行请求的分发与调度等。(那不就是洗脚城的前台吗?)


常见的反向代理场景:



  • 负载均衡

  • 动静分离    

  • 业务分离

  • ......


image.png


Nginx 有哪些作用?


1. Nginx的虚拟站点服务


Nginx可基于 端口号、域名(IP) 实现多站点服务,已实现前端访问80或443端口,通过上述不同条件访问不同的后端服务



一个洗脚城前台,后面有不知道数量的技师。



server
{
listen 80;
server_name http://www.hamm.cn;
index index.html;
root /home/www/portal_site;
}
server
{
listen 80;
listen 443 ssl http2;
# 证书相关配置
server_name api.hamm.cn;
index index.html;
root /home/www/api_service;
}
server
{
listen 80;
server_name static.hamm.cn;
index index.html;
root /home/www/static_service;
autoindex on;
}

image.png


2. Nginx反向代理实现负载均衡


通过反向代理指定上游多个负载点,配置不同的负载优先级以及调度策略来实现负载均衡。



按业务能力和服务质量,洗脚城前台往往会针对不同的技师分配不同的任务。



upstream api_slb {
server 192.168.123.78:8100 weight=5 max_fails=3 fail_timeout=5;
server 192.168.123.78:8200 weight=3 max_fails=3 fail_timeout=5;
server 192.168.123.79:8100 weight=2 max_fails=3 fail_timeout=5;
# 优先使用局域网测试服务器的服务 按权重进行负载


# 如果测试服务器不可用,可通过下面两台备用服务跑

server 192.168.123.77:8080 weight=2 backup;
# 同事张三 代码一般是最新 优先使用
server 192.168.123.151:8080 weight=1 backup;
# 同事李四 工位最近 做备机 方便沟通
}
server {
listen 80;
server_name api.hamm.cn;
location / {
proxy_pass http://api_slb;
}
}

image.png


3. Nginx业务分离或动静分离


使用Nginx解决一些特定的需求



  • 相同域名,不同路径访问不同资源

  • 不同域名,解决访问跨域等问题
    .......



技师根据不同的客户需求,分配不同业务能力的技师。



upstream api_service {
server 192.168.123.78:8100;
}
upstream web_frontend {
server 192.168.123.66:8010;
}
server {
listen 80;

# 使用统一域名 http://hamm.cn访问所有资源

server_name hamm.cn;

# 匹配 http://hamm.cn/api/****
# 到系统API服务器上
location /api/ {
proxy_pass http://api_service;
}

# 如果资源在本机 可使用Nginx自带的静态资源服务
location /static {
index index.html;
alias /home/www/static
}

# 匹配其他请求到前端服务器上
location / {
proxy_pass http://web_frontend
}
}

image.png


4. Nginx完成其他场景下的小需求




  • 跨域问题




server {
# API服务
listen 80;
server_name api.hamm.cn;

# 设置允许跨域的域名 支持通配符 如* 代表所有
add_header Access-Control-Allow-Origin hamm.cn;

# 跨域时允许提交的header数据
add_header Access-Control-Allow-Headers 'authorization','content-type';

# 允许跨域的请求方法
add_header Access-Control-Allow-Methods 'option,get,put,post';

# 还有很多配置项 自己百度吧:)
}
server {
# 前端
listen 80;
server_name hamm.cn;
}




  • 代理过滤





使用 sub_filter 模块将代理中一些数据进行过滤或替换:)



server {
...

location /api {
...
sub_filter_types application/json;
sub_filter_once off;

sub_filter '搜藏成功' '收藏成功';
}
}


技师服务的时候有点不耐烦,大喊了一声 “傻X客户”,洗脚城前台为了洗脚城的形象,给替换成了 “好帅的客户”





  • 自定义错误页




server
{
listen 80;
server_name api.hamm.cn;
error_page 502 503 404 403 500 /error.json;
}


技师让客户不高兴的时候,洗脚城每次都出来给大家唱首歌缓和下气氛。





  • 流量控制 请求黑名单IP 防盗链 请求拦截





技师提前预判是不是意向客户,或者专门找事的客户,提前处理好这些事情,不让技师烦恼。



server
{
listen 80;
server_name static.hamm.cn;
root /home/www/static_files/images;
location ~ .*\.(jpg|gif|png|bmp)$ {
# 如果是百度爬虫 让它一边去
if ($http_user_agent ~ 'Baiduspider') {
# rewrite * "/getout.jpg";
return 403;
}
# 图片 允许直接访问 如有跨域 只允许指定的域名访问
valid_referers none *.hamm.cn;
if ($invalid_referer) {
# 其他的请求 通通甩掉
return 403;
}
}
location /admin/ {
# 如果是上传到admin目录下的文件
allow 123.45.67.89;
# 只允许指定的IP可以访问
deny all;
# 其他人通通甩掉
}
}

image.png


总结一下


我真的没去过洗脚城。


作者:Hamm
来源:juejin.cn/post/7295995236886396939
收起阅读 »

面试官问我唯一ID如何生成

由于简历上,写了分库分表,还写了精通微服务和分布式系统,然后被面试官一顿蹂躏了这里面的技术细节;尤其给我印象最深的问题是,在分布式环境下,你们数据的唯一ID是如何去生成的? 当时条件反射,无脑的回答到,通过数据库自增方式生成的唯一ID;面试官听了后,叫我回家等...
继续阅读 »

由于简历上,写了分库分表,还写了精通微服务和分布式系统,然后被面试官一顿蹂躏了这里面的技术细节;尤其给我印象最深的问题是,在分布式环境下,你们数据的唯一ID是如何去生成的?


当时条件反射,无脑的回答到,通过数据库自增方式生成的唯一ID;面试官听了后,叫我回家等通知,然后就没有然后了。。。。。。。


目前业界有哪些唯一ID的生成方式了?


大概有5种。如下图


1698825367530.png


UUID模式


uuid全称是通用唯一识别码(Universally Unique Identifier)。底层是通过mac地址+时间+随机数进行生成的128位的二进制,转换为16进制字符串后,长度为32位。

uuid的生成非常简单, 如果用java代码生成,那只需要一行代码


public static void main(String[] args) {      
String uuid = UUID.randomUUID().toString().replaceAll("-","");
System.out.println(uuid); }

优点: uuid生成的ID 能满足唯一性;并且不依赖任何中间件;生成速度超快。


但是uuid的缺点也很明显: 生成的ID太长了,字符长度有32位

在业务系统里,一般ID是要进行存储到库里的,并且很有可能会作为常用的查询条件,比如存储到mysql,比int32的4字节或者int64的8字节来讲,太占空间;


第二个缺点:就是生成的ID是无序的,不是递增的;这样在把ID做为索引字段,在底层插入索引时,还会引起索引节点的分裂,影响插入速度。 所以用UUID生成的ID,对数据库来说很不友好


那么UUID是不是就没有业务使用场景了?

并不是的,像很多大厂的日志链路追踪的ID,就是基于UUID进行生成的。


redis 自增模式


大家都知道 在redis里有一个INCR命令,能从0到Long.maxValue-1进行单一连续的自增,最大19位;9位数字就是亿基本的数了,还剩10位,假设每天1千W的订单数据,即1千W个ID,能支持25亿年;距离地球毁灭时间还差点


redis id的自增代码也很简单,不详述。


优点:

redis 自增生成的ID 能满足唯一性也能满足单调自增性。做底层数据存储或者索引的时候,对数据库也很友好。生成速度还飞快,因为都是在内存中生成。


缺点:

redis 自增ID的缺点也很明显可能会丢数据
我们知道redis是一个kv内存数据库,虽提供了异步复制数据+哨兵模式 来保证服务的高可用+数据高可靠;但redis不保证不丢数据,即使从库变为主库,但也不保证之前的从库数据是最新的,毕竟是异步同步数据。


建议中大厂,或者能稳定运维redis和具有高可用方案+失败重试方案的厂子使用;小厂慎用吧。


数据库自增模式


在早些年,确实看到过依靠数据库自增的方式,进行唯一ID的生成。此种方式主要是依赖数据库的自增来实现;想要一个唯一的ID,那么往带有自增主键的表里插入一条数据吧,这样sql返回的时候,就会把自增ID返回来了


具体sql


INSERT INTO your_table (column1, column2) VALUES ('value1', 'value2'); SELECT LAST_INSERT_ID();

优点:
数据库自增生成的ID 能满足唯一性也基本能满足自增性


缺点:

生成速度较慢

受限于数据库的实现方式,,ID生成速度相较于前两者至少慢一个数量级,毕竟在生成的时候数据库有磁盘IO操作;高并发下,不建议使用此种方式。而且每次要获得一个ID,还需要往数据库里先插入一条记录。本来高并发下,数据库操作就是个瓶颈了,用了此种方式还加剧了数据库的负担。


数据库号段模式


数据库号段模式,可以看做是数据库自增模式的升级版,在数据库自增模式上进行了性能优化,解决了ID生成慢的问题。
思路入下图:


1698825866585.png


数据库自增模式,每次获取一个ID,都需要操作一次库,太慢了。
号段模式每次操作数据库是申请一个号段的范围。
比如操作一次数据库,申请1000到2000是这个应用的业务申请的号段;然后只能这个应用实例的业务能用;应用申请了号段后放到内存中,每次业务需要的时候,从内存里累加1返回,在java里也有现成的工具,比如AtomicLong包装下(省事还能解决并发问题);如果发现快不够了,还能异步提前从数据库中申请,放入内存中,这样就避免了业务需要唯一ID的时候,在去数据库申请,加快业务获取ID的速度。


优点:

能满足唯一性,也能满足自增性,性能也不差,美团开源的leaf使用此种模式,速度能到 5W/秒。


缺点:

对数据库是强依赖;但我想大多数业务系统基本都依赖数据库吧


个人认为此方案最适合小厂;性能不差,而且也不需要依赖太多的中间件;而且大厂里也不缺乏使用此方案身影,比如滴滴开源的Tinyid也是用的此模式


雪花算法模式


雪花算法(Snowflake)出自大名鼎鼎的twitter公司之手;是的就是那个被硅谷钢铁侠 马斯克收购的公司。
该算法开源后,深受国内大厂好评。并且像百度这种大厂,基于雪花算法思想,开发了自己开源的
UidGenerator 分布式ID生成器。


1698825928764.png


Snowflake生成的是Long类型的ID,我们知道一个Long类型占8个字节空间,1个字节又占8比特,也就是说一个Long类型会用88 64个比特。


Snowflake ID组成结构:正数位(占1比特)+ 时间戳(占41比特)+ 机器ID(占5比特)+ 数据中心(占5比特)+ 自增值(占12比特),总共64比特。



  • 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。

  • 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年 (如果你建设的IT系统要使用100年,慎用此法)

  • 工作机器id(10bit):也被叫做workId,这个可以灵活配置,机房或者机器号组合都可以。

  • 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID


优点:

* 雪花算法生成的ID,能保证唯一性;

* 随这时间齿轮的流转,也能满足自增性;有大厂的背书,生成速度也是超快;

* 对第三方中间件也是弱依赖


缺点:



  • 存在时钟回拨问题,所有机器时钟必须保存同步,否则会导致生成重复ID;但是也能解决

  • 生成的数字会随这时间的流逝增大,不会因为数据量的增大而增大,在某些业务场景不太适用

  • 需要维护机器ID的配置;或者依赖第三方中间件根据机器信息,生成机器ID


这么多的分布式唯一id生成方法,我该选择哪一种?


为了方便选择,列了个表。到底我的应用里,该选哪个方案了?还需结合自身业务,团队大小,技术实力,实现复杂度和维护难易度进行选择。


image.png


想偷懒,有现成的唯一ID生成工具吗?


有的,像比如美团开源的leaf 能支持数据库号段和雪花模式,传输门:github.com/Meituan-Dia…


百度的 UidGenerator 使用的是雪花模式,传输门:github.com/baidu/uid-g…


而滴滴的 Tinyid 使用的是号段模式,可以认为是基于美团leaf的开源版,传输门:github.com/didi/tinyid…


请注意:以上三个工具都是用java语言写的,用java语言的同学有福了,基本是开箱即用。


集成美团leaf 号段模式


1、从git上 github.com/Meituan-Dia… 下载 leaf源码


1698826132995.png


2、mvn clean install -DskipTests 编译打包工程


3、用工程里的script脚本目录下的sql 脚本,创建数据表


CREATE DATABASE leaf
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '',
`max_id` bigint(20) NOT NULL DEFAULT '1',
`step` int(11) NOT NULL,
`description` varchar(256) DEFAULT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

insert int0 leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id')


4、在leaf-server工程里配置数据库连接信息


1698826171611.png


5、启动leaf-server工程,访问
浏览器访问 http://localhost:8080/api/segment/get/leaf-segment-test即可。


注意:这里的leaf-segment-test即时bizTag。如果想要换成其它业务标,往leaf_alloc插入一条数据即可。


不想用leaf的web模式;但又想用号段模式+本地api方式直接调用,是否可行?


可行的。只需要引入美团的leaf-core jar包即可。是的,这么好的工具,居然没有maven信息;不死心的我,去maven官方仓库搜了一把,还真发现了leaf-core的坐标。
如下:


<dependency>
<groupId>com.tencent.devops.leafgroupId>
<artifactId>leaf-coreartifactId>
<version>1.0.2-RELEASEversion>
dependency>

但是下载下来后,包名居然是腾讯的包名。反编译后,发现代码基本上没怎么变。如果想偷懒,可直接下载。


为了保险起间,我还是用了美团的leaf-core(只好做本地jar依赖)。
然后在代码里加入对ID生成器的初始化。代码如下:



@Service
public class LeafIdDBSegmentImpl implements InitializingBean {
IDGen idGen;
DruidDataSource dataSource;

@Override
public void afterPropertiesSet() throws Exception {
Properties properties = PropertyFactory.getProperties();

// Config dataSource
dataSource = new DruidDataSource();
dataSource.setUrl(properties.getProperty("leaf.jdbc.url"));
dataSource.setUsername(properties.getProperty("leaf.jdbc.username"));
dataSource.setPassword(properties.getProperty("leaf.jdbc.password"));
dataSource.init();

// Config Dao
IDAllocDao dao = new IDAllocDaoImpl(dataSource);

// Config ID Gen
idGen = new SegmentIDGenImpl();
((SegmentIDGenImpl) idGen).setDao(dao);
idGen.init();
}

public Long getId(String key){
Result result = idGen.get(key);
if (result.getStatus().equals(Status.EXCEPTION)) {
throw BizException.createBizException(BizErrorCode.LEAF_CREATE_ERROR_REQUIRED);
}
return result.getId();
}

}


该代码主要做了几件事:

1、在该单列类,实例化后,会进行 数据库源的初始化工作,并且自行实例化IDAllocDao;当然你可以把工程里已有的数据源赋给IDAllocDaoImpl

2、获取ID时,调用底层idGen.get获取

3、业务代码使用时 用 LeafIdDBSegmentImpl.get方法 传入数据库里事先配置好的bizTag即可。


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

一文告诉你,如何实现 IP 属地功能

细心的朋友们可能已经发现了,先在抖音、知乎、快手、小红书等这些平台已经上线了“网络用户显示 IP 的功能”,境外用户显示的是国家,国内的用户显示的省份,而且此项显示无法关闭,归属地强制显示。 作为网友,我们可能只是看看戏,但是作为一个努力学习的码农,我们肯定...
继续阅读 »

细心的朋友们可能已经发现了,先在抖音、知乎、快手、小红书等这些平台已经上线了“网络用户显示 IP 的功能”,境外用户显示的是国家,国内的用户显示的省份,而且此项显示无法关闭,归属地强制显示。



作为网友,我们可能只是看看戏,但是作为一个努力学习的码农,我们肯定要来看一下这个功能是怎么实现的,今天这篇文章,就用几分钟的时间来讲述一下这个功能是怎么实现的。



获取用户 IP 地址


HttpServletRequest 获取 IP




首先我们来看一下,在 Java 中,是如何获取到 IP 属地的,主要有以下两步:



  1. 通过 HttpServletRequest 对象,获取用户的 「IP」 地址

  2. 通过 IP 地址,获取对应的省份、城市]


首先,我们这里写一个工具类用于获取 IP 地址,因为用户的每次 Request 请求都会携带请求的 IP 地址放到请求头中,所以我们可以通过截取请求中的 IP 来获取 IP 地址;


/**
* 网络工具类
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @from <a href="https://yupi.icu">编程导航知识星球</a>
*/
public class NetUtils {

/**
* 获取客户端 IP 地址
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (ip.equals("127.0.0.1")) {
// 根据网卡取本机配置的 IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (Exception e) {
e.printStackTrace();
}
if (inet != null) {
ip = inet.getHostAddress();
}
}
}
// 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ip != null && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
// 本机访问
if ("localhost".equalsIgnoreCase(ip) || "127.0.0.1".equalsIgnoreCase(ip) || "0:0:0:0:0:0:0:1".equalsIgnoreCase(ip)){
// 根据网卡取本机配置的IP
InetAddress inet;
try {
inet = InetAddress.getLocalHost();
ip = inet.getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
// 如果查找不到 IP,可以返回 127.0.0.1,可以做一定的处理,但是这里不考虑
// if (ip == null) {
// return "127.0.0.1";
// }
return ip;
}

/**
* 获取mac地址
*/
public static String getMacAddress() throws Exception {
// 取mac地址
byte[] macAddressBytes = NetworkInterface.getByInetAddress(InetAddress.getLocalHost()).getHardwareAddress();
// 下面代码是把mac地址拼装成String
StringBuilder sb = new StringBuilder();
for (int i = 0; i < macAddressBytes.length; i++) {
if (i != 0) {
sb.append("-");
}
// mac[i] & 0xFF 是为了把byte转化为正整数
String s = Integer.toHexString(macAddressBytes[i] & 0xFF);
sb.append(s.length() == 1 ? 0 + s : s);
}
return sb.toString().trim().toUpperCase();
}
}

获取用户的 IP 地址属地


淘宝库获取用户 IP 地址属地




通过这个方法,就可以重请求头中获取到用户的 IP 地址了,然后接下来就是 IP 地址归属地省份、城市的获取了,这里可以用很多 IP 地址查询的库进行查询,这里用一个库来测试一下。


淘宝 IP 地址库:ip.taobao.com/



不过淘宝的 IP 地址查询库已经在 2022 年 3 月 31 日下线了,这里我们就不能使用它了,只能另辟蹊径了。



这里我们截取一段之前淘宝货期 IP 地址的源码,然后一起来看一下。



这里可以看到,在日志文件中,出现了大量的 the request over max qps for user 问题。


虽然这个方法已经寄了,但是我们求知的道路可以停吗?肯定不可以啊,这里我们就来整一个新的获取 IP 地址属地的方法,也就是我们今天文章的主角:Ip2region。


Ip2region 介绍




这个是在之前的一篇文章看到的,他是一个 Gthub 的开源项目,即 Ip2region 开源项目


地址如下:github.com/lionsoul201…


这个开源库目前已经更新到了 V2 的版本,现在的它是一个强大的离线IP地址定位库和IP定位数据管理框架,其达到了微秒级别的查询效率,还提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,可以说是非常得好用👍👍👍👍,今天这篇文章我们主要针对其 V2 版本进行讲解,如果想要查询 1.0 版本的内容的话,可以去 Github 上面进行查看。


Ip2region 详解


高达 99.9 % 的查询准确率




数据聚合了一些知名 ip 到地名查询提供商的数据,这些是他们官方的的准确率,经测试着实比经典的纯真 IP 定位准确一些。ip2region 的数据聚合自以下服务商的开放 API 或者数据(升级程序每秒请求次数 2 到 4 次),比例如下:





  1. 80%, 淘宝 IP 地址库, ip.taobao.com/%5C




  2. ≈10%, GeoIP, geoip.com/%5C

  3. ≈2%, 纯真 IP 库, http://www.cz88.net/%5C


Ip2region V2.0 特性




1、IP 数据管理框架


xdb 支持亿级别的 IP 数据段行数,默认的 region 信息都固定了格式:国家|区域|省份|城市|ISP,缺省的地域信息默认是0。 region 信息支持完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。


2、数据去重和压缩


xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。


3、极速查询响应


即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:



  1. vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。

  2. xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。


多语言以及查询客户端的支持


已经客户端有:Java、C#、php、C、Python、Node.js、PHP 拓展(PHP 5 和 PHP 7)等,主要如下:


binding描述开发状态binary查询耗时b-tree查询耗时memory查询耗时
cANSC c binding已完成0.0x毫秒0.0x毫秒0.00x毫秒
c#c# binding已完成0.x毫秒0.x毫秒0.1x毫秒
Golanggolang binding已完成0.x毫秒0.x毫秒0.1x毫秒
Javajava binding已完成0.x毫秒0.x毫秒0.1x毫秒
Lualua实现 binding已完成0.x毫秒0.x毫秒0.x毫秒
Lua_clua的c扩展已完成0.0x毫秒0.0x毫秒0.00x毫秒
nginxnginx的c扩展已完成0.0x毫秒0.0x毫秒0.00x毫秒
nodejsnodejs已完成0.x毫秒0.x毫秒0.1x毫秒
phpphp实现 binding已完成0.x毫秒0.1x毫秒0.1x毫秒
php5_extphp5的c扩展已完成0.0x毫秒0.0x毫秒0.00x毫秒
php7_extphp7的c扩展已完成0.0毫秒0.0x毫秒0.00x毫秒
pythonpython bindng已完成0.x毫秒0.x毫秒0.x毫秒
rustrust binding已完成0.x毫秒0.x毫秒0.x毫秒

Ip2region xdb Java 查询客户端实现




这里简单展示一下 Java 的实现,这里使用开发中常用的 Maven 实现的方式:


1. 引入 Maven 仓库


由于项目使用Spring 的方式构建,这里可以选择使用引入 Spring 的 starter 的方式进行


<dependency>
<groupId>com.github.hiwepy</groupId>
<artifactId>ip2region-spring-boot-starter</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>

<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.7.0</version>
</dependency>

在引入 Maven 依赖之后,我们这里引入几种实现的方式:


2. 实现方式 1:【基于文件查询】


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
public static void main(String[] args) {
// 1、创建 searcher 对象
String dbPath = "ip2region.xdb file path";
Searcher searcher = null;
try {
searcher = Searcher.newWithFileOnly(dbPath);
} catch (IOException e) {
System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
return;
}

// 2、查询
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}

// 3、备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
}
}

3. 实现方式 2:【缓存VectorIndex索引】


我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作,从而加速查询,减少 IO 压力。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
public static void main(String[] args) {
String dbPath = "ip2region.xdb file path";

// 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。
byte[] vIndex;
try {
vIndex = Searcher.loadVectorIndexFromFile(dbPath);
} catch (Exception e) {
System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e);
return;
}

// 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
Searcher searcher;
try {
searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
} catch (Exception e) {
System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e);
return;
}

// 3、查询
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}

// 备注:每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。
}
}

4. 实现方式 3:「缓存整个 xdb 数据」


我们也可以预先加载整个 ip2region.xdb 的数据到内存,然后基于这个数据创建查询对象来实现完全基于文件的查询,类似之前的 memory search。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
public static void main(String[] args) {
String dbPath = "ip2region.xdb file path";

// 1、从 dbPath 加载整个 xdb 到内存。
byte[] cBuff;
try {
cBuff = Searcher.loadContentFromFile(dbPath);
} catch (Exception e) {
System.out.printf("failed to load content from `%s`: %s\n", dbPath, e);
return;
}

// 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
Searcher searcher;
try {
searcher = Searcher.newWithBuffer(cBuff);
} catch (Exception e) {
System.out.printf("failed to create content cached searcher: %s\n", e);
return;
}

// 3、查询
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}

// 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。
}
}

5. 编译测试程序


通过 maven 来编译测试程序。


# cd 到 java binding 的根目录
cd binding/java/
mvn compile package

然后会在当前目录的 target 目录下得到一个 ip2region-{version}.jar 的打包文件。


6. 查询测试



  • 可以通过 java -jar ip2region-{version}.jar search 命令来测试查询:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar search
java -jar ip2region-{version}.jar search [command options]
options:
--db string ip2region binary xdb file path
--cache-policy string cache policy: file/vectorIndex/content


  • 例如:使用默认的 data/ip2region.xdb 文件进行查询测试:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar search --db=../../data/ip2region.xdb
ip2region xdb searcher test program, cachePolicy: vectorIndex
type 'quit' to exit
ip2region>> 1.2.3.4
{region: 美国|0|华盛顿|0|谷歌, ioCount: 7, took: 82 μs}
ip2region>>

输入 ip 即可进行查询测试,也可以分别设置 cache-policy 为 file/vectorIndex/content 来测试三种不同缓存实现的查询效果。


bench 测试


可以通过 java -jar ip2region-{version}.jar bench 命令来进行 bench 测试,一方面确保 xdb 文件没有错误,一方面可以评估查询性能:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar bench
java -jar ip2region-{version}.jar bench [command options]
options:
--db string ip2region binary xdb file path
--src string source ip text file path
--cache-policy string cache policy: file/vectorIndex/content

例如:通过默认的 data/ip2region.xdb 和 data/ip.merge.txt 文件进行 bench 测试:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar bench --db=../../data/ip2region.xdb --src=../../data/ip.merge.txt
Bench finished, {cachePolicy: vectorIndex, total: 3417955, took: 8s, cost: 2 μs/op}

可以通过分别设置 cache-policy 为 file/vectorIndex/content 来测试三种不同缓存实现的效果。 @Note: 注意 bench 使用的 src 文件要是生成对应 xdb 文件相同的源文件。


作者:夭要7夜宵
来源:juejin.cn/post/7295576148364296231
收起阅读 »

为什么堂堂微信数据库表名、字段名起的如此随意?

1.微信数据库解密 微信数据库在在哪个文件夹 EnMicroMsg.db的父文件加密规则是md5("mm" + uin)这样就可以准确的获取到db文件的位置. uin的获取:/data/data/com.tencent.mm/shared_prefs/au...
继续阅读 »

1.微信数据库解密




  • 微信数据库在在哪个文件夹


    EnMicroMsg.db的父文件加密规则是md5("mm" + uin)这样就可以准确的获取到db文件的位置.


    uin的获取:/data/data/com.tencent.mm/shared_prefs/auth_info_key_prefs.xml`里面有个uinz字段,直接获取value值,示例如下图所示:


    image-20210526135110550




  • 解密微信数据库:目前只涉及两个数据库EnMicroMsg.db(微信数据涉数据库)和WxFileIndex.db(文件索引数据库)


    解密的密码:md5(手机imei+uin)的32位字符串取前7位,如果imei拿不到就用1234567890ABCDEF代替




2. 好友相关信息


微信的好友相关数据涉及三张表:rcontact,bizinfo,img_flag




  • rcontact表存放了该账号涉及的所有账号(微信账号,群账号)的基本信息(eg:微信昵称,备注,原微信号,改之后的微信号,全拼等等)。如下图所示:


    111




  • bizinfo表存放的是该账号的好友微信号,群账号,这里好友包括已经通过的和添加没通过的,如下所示:


    22222




  • img_flag表存放该账号所有涉及的微信(好友,同属一个群不是好友,添加的陌生人)的头像地址,数据如下图所示:


    3333


    总结:rcontact表是一张基础表,存放所有的账号基本信息,bizinfo存放是该账号的好友信息或者群组信息,img_flag存放了微信账号对应的头像信息,以下场景有:




    • 获取微信好友信息,查询sql如下:


      select r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin,r.encryptUserName, i.reserved2 from rcontact r INNER JOIN img_flag i  on r.username = i.username where r.type&2=2  and r.username not like '%@chatroom' and i.lastupdatetime > 0



    • 获取添加未通过的好友信息,此时有两种情况:1)添加同属一个群的好友。2)添加陌生人。比如说通过微信号,扫码什么。这两种情况在数据库的表现形式是不一样的,添加同属一个群的,在bizinfo表会插入一条username为添加好友的微信号记录,而如果是添加陌生人,则username是一个以@stranger结尾的key,对应的数据如下图所示:


      4444


      注意:这里如果通过微信号,扫码添加的陌生人,其username是一长串的以@stranger结尾的key,同 时pyInitial,qunPin两个字段存的并不是这个陌生人的微信号


      查询sql如下:


      SELECT r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin, r.encryptUserName, i.reserved2 FROM rcontact r INNER JOIN bizinfo b ON r.username = b.username INNER JOIN img_flag i ON r.username = i.username 
      WHERE r.type <> 33 and r.type & 2 <> 2 AND r.username <> '当前微信号' AND r.username NOT LIKE '%@chatroom' AND b.updateTime > 0



    • 获取同属一个群但不是好友的基本信息:


      查询sql如下:


      SELECT DISTINCT r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin, i.reserved2 FROM rcontact r  INNER JOIN img_flag i ON r.username = i.username 
      WHERE r.username not in(select username from bizinfo) and i.lastupdatetime >0

    3.微信群组


    微信群组信息表为chatroom,存放着一些基本信息,数据如下图所示:


    555


    注意:微信群组一开始建立显示群昵称是所有好友微信昵称加起来的一个字符串,即displayname字段,但是如果修改了群昵称之后,显示的是修改之后的,这时候需要根据根据群账号chatroomname去rcontact表做关联查询,根据rcontact表的username等于群账号查询出一条记录,此时这条记录的字段nickname即修改后的群昵称,查询sql如下:


    select c.chatroomname, c.memberlist, c.displayname, c.roomowner, c.selfDisplayName, r.nickname from chatroom c inner join rcontact r on r.username = c.chatroomname where c.modifytime > 0


    目前的微信群组的头像在img_flag表没有存储,暂时找不到资源所在


    4.微信聊天数据


    微信的聊天记录是保存在message表中的,数据示例如下图:


    666


    msgSvrId:唯一标示一条聊天记录的id,可以作为更新聊天记录数据的条件


    createTime:发送消息的时间


    talker:如果是群账号,说明这条消息是群会话消息,发送人在content内容前面(发送人微信号:发送内容);如果是好友微信号,说明这条消息是好友会话消息


    isSend:发送或者接收标志。0:接收 1:发送


    type:消息类型 1:文本内容(包括小表情) 3:图片 34:语音 43:视频 47:大表情 49:文件


    436207665:微信红包 419430449:微信转账


    图片,视频,语音, 文件 根据msgId去索引库WxFileIndex的表WxFileIndex2查询




    • 图片查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=3 and msgSubType20



    • 语音查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=34



    • 视频查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=43 and msgSubType=1



    • 文件查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=49 and msgSubType = 34



    • 大表情查询sql:根据groupId去找到对应的包名,md5即表情的文件名


      select e.md5, e.groupid, m.msgSvrId from emojiinfo e INNER JOIN message m on e.md5=m.imgpath where m.type=47



    5.总结


    以上分析师基于Android系统端的微信,且微信数据的撤销删除仍需要研究,待补充,未完待续.......




作者:shepherd111
来源:juejin.cn/post/7295160228879122458
收起阅读 »

适合小公司的自动化部署脚本

背景(偷懒) 在小小的公司里面,挖呀挖呀挖。快挖不动了,一件事重复个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… --ssh免密登录


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

如何优雅的判断一个对象是否为空?

我们在刚开始学习Java的时候,遇到过最多的异常肯定是臭名昭著的空指针异常(NullPointerException),可以说它陪伴了我们整个初学阶段。字符串、对象、集合等等一不留神就容易出现空指针异常! 那么如何优雅的判断一个对象是否为空并且减少空指针异常呢...
继续阅读 »

我们在刚开始学习Java的时候,遇到过最多的异常肯定是臭名昭著的空指针异常(NullPointerException),可以说它陪伴了我们整个初学阶段。字符串、对象、集合等等一不留神就容易出现空指针异常!


那么如何优雅的判断一个对象是否为空并且减少空指针异常呢?


今天来介绍一个容器类——Optional


Optional介绍


Optional是一个容器类,它只有两种情况:



  • 要么包含一个非空对象

  • 要么为空


它有助于编写更健壮的代码,以处理可能为空的值,而不必担心空指针异常!


Optional用法


Optional的创建


Optional有以下两种常见的创建方法:



  • Optional.of(T value):创建一个包含非空值的Optional,如果value为null,则抛出NullPointerException

  • Optional.ofNullable(T value):创建一个Optional,允许value为null


判断Optional容器中是否包含对象


isPresent(): 返回一个布尔值,如果Optional容器中包含一个非空对象则返回true,否则返回false


获取Optional容器的对象



  • get(): 如果Optional包含非空值,返回该值;否则抛出NoSuchElementException

  • orElse(T other): 如果Optional为空,返回指定的默认值other

  • orElseGet(Supplier<? extends T> other): 如果Optional为空,使用提供的Supplier函数生成默认值

  • orElseThrow(Supplier<? extends X> exceptionSupplier): 如果Optional为空,抛出由提供的Supplier函数生成的异常


过滤


filter(Predicate<? super T> predicate): 如果Optional包含非空值且满足predicate条件,则返回当前Optional,否则返回一个空Optional。


映射



  • map(Function<? super T, ? extends U> mapper): 如果Optional包含非空值,应用mapper函数并返回新的Optional。

  • flatMap(Function<? super T, Optional> mapper): 类似于map,但允许mapper函数返回Optional。


Optional示例


假如我们有一个User类,可以使用Optional来处理可能为空的User对象。User类结构如下:


public class User {
private String name;

public User(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

示例:创建Optional



Optional userOptional = Optional.ofNullable(new User("张三"));



示例:判断Optional是否包含对象


if (userOptional.isPresent()) {
System.out.println("用户存在:" + userOptional.get().getName());
} else {
System.out.println("用户不存在");
}

示例:获取Optional容器的对象


User user = userOptional.orElse(new User("李四"));
System.out.println("User: " + user.getName());

示例:过滤


Optional<User> filteredUserOptional = userOptional.filter(u -> u.getName().startsWith("张"));
if (filteredUserOptional.isPresent()) {
System.out.println("结果:" + filteredUserOptional.get().getName());
} else {
System.out.println("未找到对应用户");
}

示例:映射


Optional<String> userNameOptional = userOptional.map(User::getName);
userNameOptional.ifPresent(name -> System.out.println("用户名为: " + name));

使用场景总结



  • 当你从某个方法返回一个值,但该值可能为空,而调用者需要明确知道值是否存在。

  • 在处理方法参数时,你可以用Optional来表示某个参数可以为空,以提醒调用者可能会传入null。

  • 避免繁琐的null检查和条件语句,使代码更简洁和可读!



更多文章干货,推荐公众号【程序员老J】



作者:程序员老J
来源:juejin.cn/post/7298142364194979852
收起阅读 »

强无敌!一个项目涵盖SpringBoot集成各种场景

大家好,我是 Java陈序员。我们都知道,作为 Java 后端开发肯定绕不开 Spring,而 SpringBoot 的横空出世更是帮助我们开发者可以快速迭代一个项目! SpringBoot 之所以强大,是因为支持自动化配置,可以快速装配组件,如持久化框架缓存...
继续阅读 »

大家好,我是 Java陈序员。我们都知道,作为 Java 后端开发肯定绕不开 Spring,而 SpringBoot 的横空出世更是帮助我们开发者可以快速迭代一个项目!


SpringBoot 之所以强大,是因为支持自动化配置,可以快速装配组件,如持久化框架缓存消息队列日志等等。


今天给大家介绍一个 SpringBoot 集成各种场景的项目,可以用来学习,也可以开箱即用无需重复造轮子


项目简介


spring boot demo 是一个用来深度学习并实战 spring boot 的项目,目前总共包含 66 个集成 demo,已经完成 55 个。


目前已经集成功能:



  • actuator:监控

  • admin:可视化监控

  • logback:日志

  • aopLog:通过 AOP 记录 Web 请求日志

  • 统一异常处理:json 级别和页面级别

  • freemarker:模板引擎

  • thymeleaf:模板引擎

  • Beetl:模板引擎

  • Enjoy:模板引擎

  • JdbcTemplate:通用 JDBC 操作数据库

  • JPA:强大的 ORM 框架

  • Mybatis:强大的 ORM 框架

  • 通用 Mapper:快速操作 Mybatis

  • PageHelper:通用的 Mybatis 分页插件

  • Mybatis-plus:快速操作 Mybatis

  • BeetlSQL:强大的 ORM 框架

  • upload:本地文件上传和七牛云文件上传

  • Redis:缓存

  • ehcache:缓存

  • email:发送各种类型邮件

  • task:基础定时任务

  • quartz:动态管理定时任务

  • xxl-job:分布式定时任务

  • swaggerAPI 接口管理测试

  • security:基于 RBAC` 的动态权限认证

  • SpringSessionSession 共享

  • Zookeeper:结合 AOP 实现分布式锁

  • RabbitMQ:消息队列

  • Kafka:消息队列

  • websocket:服务端推送监控服务器运行信息

  • socket.io:聊天室

  • ureport2:中国式报表

  • 打包成 War 文件

  • 集成 ElasticSearch:基本操作和高级查询

  • Async:异步任务

  • 集成Dubbo:采用官方的starter

  • MongoDB:文档数据库

  • neo4j:图数据库

  • Docker:容器化

  • JPA 多数据源

  • Mybatis 多数据源

  • 代码生成器

  • GrayLog:日志收集

  • JustAuth:第三方登录

  • LDAP:增删改查

  • 动态添加/切换数据源

  • 单机限流:AOP + Guava RateLimiter

  • 分布式限流:AOP + Redis + Lua

  • ElasticSearch 7.x:使用官方 Rest High Level Client

  • HTTPS

  • Flyway:数据库初始化

  • UReport2:中国式复杂报表


项目地址


https://github.com/xkcoding/spring-boot-demo

运行使用


开发环境



  • JDK 1.8 +

  • Maven 3.5 +

  • IntelliJ IDEA ULTIMATE 2018.2 + (注意:务必使用 IDEA 开发,同时保证安装 lombok 插件)

  • Mysql 5.7 + (尽量保证使用 5.7 版本以上,因为 5.7 版本加了一些新特性,同时不向下兼容。项目会尽量避免这种不兼容的地方,但还是建议尽量保证 5.7 版本以上)


代码导入



  1. 使用 git 克隆代码:


git clone https://github.com/xkcoding/spring-boot-demo.git

2. 使用 IDEA 打开 clone 下来的项目



  1. 找到各个 ModuleApplication 类就可以运行各个 demo



注意:



  1. 每个 demo 均有详细的 README,运行 demo 之前记得先看看

  2. 有些 demo 需要事先初始化数据库数据的



模块代码介绍


模块代码介绍


模块代码介绍


模块代码介绍


模块代码介绍


模块代码介绍


最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/


大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



作者:Java陈序员
来源:juejin.cn/post/7297665681017339958
收起阅读 »

和斯坦福博士写代码的一个月

近一个月,闭关开发了一个面向海外的项目,合作的对象是斯坦福和麻省理工的博士,很荣幸能够和这些全球顶尖大学毕业的大牛合作,今天,趁着周末总结下自己的一些感受。 1. 英语是硬伤 因为项目是面向海外,整个合作全程是英语,这就一览无遗地暴露出自己的英文短板,特别是口...
继续阅读 »

近一个月,闭关开发了一个面向海外的项目,合作的对象是斯坦福和麻省理工的博士,很荣幸能够和这些全球顶尖大学毕业的大牛合作,今天,趁着周末总结下自己的一些感受。


1. 英语是硬伤


因为项目是面向海外,整个合作全程是英语,这就一览无遗地暴露出自己的英文短板,特别是口语,在技术评审过程和讨论中,自己捉襟见肘的英文,只能不断的通过技术流程图和文字来弥补口语表达的不足。


最开始合作时比较羞涩,毕竟知道自己的英语有几斤几两,后面慢慢的放开,把自己的英语老本和盘托出,不过这也坚定了自己加强英文的决心,毕竟做程序开发,英语功底直接决定你理解英文资料的速度和程度。


2. 编程语言要多样


我自己最常用的是 java,但这次项目开发的主要语言是python,另外还涉及到C/C++, java, nodejs。尽管python,C/C++都有所了解,但是,要像 Java一样轻松驾驭去开发这么庞大的工程还是有点吃力。而在合作的过程中,发现他们语言的全能性,python,Java,C++,前端语言,他们都可以平滑切换,绝对的全栈工程师。


这个或许跟每个国家IT环境不一样有很大的关系,但是,作为 Java程序员,我个人还是比较建议再掌握一门动态语言,首选是python,像目前比较主流的数据分析、机器学习、科学计算、自动化和游戏开发等,Python绝对功不可没。另外,通过两种语言的对比,你能更好的看清每种语言的优点和不足,在日常开发中,或许就能借鉴另外一种语言的优点。


3. CR的重要性


CR,就是我们常说的Code Review(代码审查)。在国内的公司工作了这么多年,会做 CR的公司很少,包括一线大厂,能严格执行 CR的更是微乎其微,很多人说是因为业务太多,没有时间做CR,但是,究其原因是国内没有 CR的基因,技术管理者不推动,底层开发人员借着业务繁忙的理由,所以 CR就形同虚设。


下面给了几张项目CR的截图,因为涉及到商业隐私,所以部分信息被打码了:


图一:对于错误的实现,CR 会指正和给出正解。


图片


图二:展示了 CR 甚至细化到了Doc文档级别。


图片


图三:展示了 CR 过程中的探讨,给出自己意见的同时也征询组员更多的idea


图片


CR看似很费时,反反复复的修改和讨论,但是,它和技术方案的讨论不一样,CR针对的是技术方案已经成型的代码实现,所以这个过程更能体现出不同技术人员对同一个技术方案的的思考和实现方式,对于整个项目的质量有大大的提高,另外对于开发者之间的磨合是一个很好的润滑剂。


4. 包容


越优秀的人越懂得包容。这句话放到这个项目上最合适,一个是对于我英语水平的包容。一个是对我 Python 掌握程度的包容。通过上面部分 CR截图也能体现出来。有了这些包容,让我可以更快,更有信心的融入到项目开发。


5. 较真


对于代码实现,技术方案的较真,通过上面的 CR截图也可以体现,他们对于代码的较真可以小到一个Doc文档和标点符号,以及代码的格式化,对于代码性能可以通过大量的技术测试和对比,甚至这块代码不是他们负责的,这个或许就是我们说的技术洁癖和极客精神。


依然记得几年前的一个技术峰会上,某大牛说:中国的码农千千万万,如何让你的代码有亮点,那就是不断的抠细节,大到技术架构,小到标点符号,然后不断的重构,重构。


总结

  1. 通过这个项目,让我有幸见识了从 Google,AWS出来的优秀程序员的编码习惯。
  2. 学英语绝对不是崇洋媚外,它在一定程度上绝对了程序员的技术高度和深度,可以观察你身边优秀的技术人员,英语是不是都不差。
  3. 一定要向优秀的人靠近,和他们一起写代码,勇敢的让他们帮你CR,在交流和讨论中获取更多的葵花宝典;
  4. 程序员是一个终身学习的职业,一定要持续学习;


作者:猿java
来源:juejin.cn/post/7296111218721095715
收起阅读 »

你看现在的程序员,是怎么解bug的

大家好,我是伍六七。 干程序员的都会碰到各种各样的 bug,有的 bug 我们看一眼、或者多看几眼就能看出来问题。 但是,也有很多问题,我们看不出来,这个时候,我们不妨把我们的代码扔给 GPT,看看它能不能帮我们找到 bug,Fix the bug! 我们来看...
继续阅读 »

大家好,我是伍六七。


干程序员的都会碰到各种各样的 bug,有的 bug 我们看一眼、或者多看几眼就能看出来问题。


但是,也有很多问题,我们看不出来,这个时候,我们不妨把我们的代码扔给 GPT,看看它能不能帮我们找到 bug,Fix the bug!


我们来看个例子:


阿七来到这个公司之前,一直在传统 IT 公司工作,从来没用过什么分布式锁、多线程等高端技术。


这次,有一个业务场景,需要使用分布式锁解决问题。阿七按照自己的了解,写了一个分布式锁,结果死活有问题。


看了半天,头都挠凸了,就是不知道怎么回事,这个时候,他想到了大模型,他把问题扔给了 GPT。


prompt:



long currentTimeMillis = System.currentTimeMillis();
final String sequenceId = UUID.randomUUID().toString();
redisLockService.lock(username + currentTimeMillis, sequenceId, 2, TimeUnit.SECONDS);
---
请你分析上面的Java代码,如果有bug,请指出来,并给出正确写法



大模型返回:


这段代码的目的是使用 Redis 锁来对某个资源进行加锁操作。下面是对代码的分析:




  1. 获取当前时间的毫秒数,存储在 currentTimeMillis 变量中。




  2. 生成一个随机的 UUID 字符串,存储在 sequenceId 变量中。




  3. 调用 redisLockService.lock 方法,传入参数 username + currentTimeMillissequenceId2TimeUnit.SECONDS,对资源进行加锁操作。




如果存在 bug,可能是以下几个方面:




  1. username 变量的值是否正确,确保它是唯一的且能够标识资源的唯一性。




  2. redisLockService.lock 方法的实现是否正确,确保它能够正确地加锁并设置过期时间。




  3. 锁的释放是否在适当的时机进行,以避免资源被长时间锁定。




正确的写法可能如下所示:


long currentTimeMillis = System.currentTimeMillis();
final String sequenceId = UUID.randomUUID().toString();
String lockKey = username + currentTimeMillis;
boolean lockAcquired = redisLockService.lock(lockKey, sequenceId, 2, TimeUnit.SECONDS);
try {
if (lockAcquired) {
// 执行需要加锁的操作
} else {
// 锁未获取到,处理相应逻辑
}
} finally {
if (lockAcquired) {
redisLockService.unlock(lockKey, sequenceId);
}
}

在正确的写法中,我们首先获取锁,并在获取到锁的情况下执行需要加锁的操作。然后,在 finally 块中,我们释放锁,确保无论是否成功获取到锁,都能够正确释放资源。




一顿瞎扯,就是没找到最严重的那个 bug,我们继续问它。


prompt:



你没有找到这段代码的最严重的 bug,请你再深入分析分析,找到代码中的严重 bug



大模型返回:


非常抱歉,我之前的回答有误。这段代码存在一个严重的 bug,即在分布式环境下,由于锁的 key 值不唯一,可能会导致锁的误解锁或者锁的覆盖,从而导致并发问题。


正确的写法应该是使用 usernamesequenceId 作为锁的 key 值,以确保锁的唯一性。修改后的代码如下:


long currentTimeMillis = System.currentTimeMillis();
final String sequenceId = UUID.randomUUID().toString();
String lockKey = username + "_" + sequenceId;
redisLockService.lock(lockKey, sequenceId, 2, TimeUnit.SECONDS);
try {
// 执行需要加锁的操作
} finally {
redisLockService.unlock(lockKey, sequenceId);
}

在正确的写法中,我们使用 usernamesequenceId 作为锁的 key 值,以确保锁的唯一性。同时,在 finally 块中,我们释放锁,确保无论是否成功获取到锁,都能够正确释放资源。


这次,我们发现,它找到了代码中最严重的 bug,但是它给出的代码依然没有解决这个 bug,我们继续问它。


prompt;



lockKey 还是没有保证唯一性,并不能解决分布式锁资源不唯一的问题。请你深入思考,再给出正确的设置 lockKey 的代码.



大模型返回:


非常抱歉之前的回答仍然有误。你是正确的,lockKey 的生成方式仍然存在问题,无法保证在分布式环境下的唯一性。


为了确保锁的唯一性,可以使用 Redis 的分布式锁实现方式,例如使用 Redisson 或者 RedLock 等库来实现分布式锁。这些库提供了可靠的分布式锁实现,可以确保在分布式环境下的唯一性和正确性。


下面是使用 Redisson 实现分布式锁的示例代码:


long currentTimeMillis = System.currentTimeMillis();
final String lockKey = "lock:" + username; // 使用固定的前缀加上 username 作为锁的唯一标识
final String sequenceId = UUID.randomUUID().toString();
RLock lock = redissonClient.getLock(lockKey);
boolean lockAcquired = false;
try {
lockAcquired = lock.tryLock(2, TimeUnit.SECONDS);
if (lockAcquired) {
// 执行需要加锁的操作
} else {
// 锁未获取到,处理相应逻辑
}
} finally {
if (lockAcquired) {
lock.unlock();
}
}

在上述代码中,我们使用 Redisson 提供的 RLock 接口来获取分布式锁。通过调用 tryLock 方法来尝试获取锁,设置超时时间为 2 秒。如果成功获取到锁,则执行需要加锁的操作;否则,处理相应的逻辑。在 finally 块中,我们根据是否成功获取到锁来决定是否释放锁。


请确保你已经正确地引入了 Redisson 依赖,并且配置了正确的 Redis 连接信息。


这次,它成功的理解了我们的意思,并且修改了代码,解决了分布式锁资源 key 不唯一的问题。




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


作者:程序员伍六七
来源:juejin.cn/post/7296111218720981027
收起阅读 »

揭秘小米手机被疯狂吐槽的存储扩容技术

前段时间,在小米14的发布会上,雷布斯公布了名为“Xiaomi Ultra Space存储扩容”的技术,号称可以在512G的手机中再搞出来16G,256G的手机中再搞出8G。对于普通用户来说,能多得一些存储空间,无异是个很好的福利,不过也有网友说这是以损害存储...
继续阅读 »

image.png
前段时间,在小米14的发布会上,雷布斯公布了名为“Xiaomi Ultra Space存储扩容”的技术,号称可以在512G的手机中再搞出来16G,256G的手机中再搞出8G。对于普通用户来说,能多得一些存储空间,无异是个很好的福利,不过也有网友说这是以损害存储使用寿命为代价的,那么真相到底如何呢?这篇文章我就从技术角度来给大家详细分析下。


认识闪存


首先让我们来了解一些手机存储的基本知识。


手机存储使用的是闪存技术,其本质和U盘、固态硬盘都是一样的。


在闪存中读写的基本单位是页(Page),比页更大的概念是块(Block),一个块会包含很多页。


虽然读写的基本单位都是页,但是写实际操作的很可能是块,这是为什么呢?


这要从删除谈起,在闪存中删除数据时不会立即删除页上的数据,而只是给页打上一个空闲的标签。这是因为谁也不知道这个页什么时候会再写入数据,这样处理起来比较简单快速。


再看写操作,如果写入分配的页是因为删除而空闲的,数据并不能立即写入,根据闪存的特性,此时需要先把页上之前存储的数据擦除,然后才能写入;但是闪存中擦除操作的基本单位是块,此时就需要先把整个块中的有效数据读出来,然后再擦除块,最后再向块中写入修改后的整块数据;这整个操作称为“读-改-写”。当然如果写入分配的页是空白的,并不需要先进行擦除,此时直接写入就可以了。


预留空间


小米这次抠出来的存储空间来源于一个称为“预留空间”的区域,它的英文全称是Over Provisio,简称 OP。


那么“预留空间”是什么呢?我将通过5个方面来介绍它的用途,让大家近距离认识下。


提高写入速度


在上面介绍闪存的基本知识时,我们谈到闪存的写操作存在一种“读-改-写”的情况,因为额外的读和擦除操作,这种方法的耗时相比单纯的写入会增加不少,闪存使用的时间越长,空白的空间越少,这种操作越容易出现,闪存的读写性能下降的越快。


为了提升写入的性能,我们可以先将新数据写入到预留空间,此时上层系统就可以认为已经写入完成,然后我们在后台将预留空间中的新数据和原数据块中需要保留的数据合并到一个新的数据块中,这样就避免了频繁的读-修改-写操作,从而可以大大提高写入速度。


垃圾回收和整理


在上面介绍闪存的基本知识时,我们还谈到删除数据并不是立即清除空间,而是给数据页打一个标签,这样做的效率比较高。这样做就像我们标记了垃圾,但是并没有把它们运走,时间久了,这些垃圾会占用很多的空间。这些垃圾空间就像一个个的小碎片,所以有时也把这个问题称为碎片化问题。


虽然我们可以通过“读-改-写”操作来重新利用这些碎片空间,包括通过异步的“读-改-写”操作来提升上层应用的写入效率,但无疑还是存在写入的难度,实际写入之前还是要先进行擦除。


为了解决上述问题,聪明的设计师们又想到了新办方法:让存储器在后台自动检测、自动整理存储中的数据碎片,而不是等到写入数据时再进行整理。


考虑到闪存的读擦写特性,当需要移除数据块中部分碎片或者将不同数据碎片合并时,就得把需要保留的数据先放到一个临时空间中,以免数据出现丢失,待存储中的数据块准备好之后再重新写入,预留空间就可以用作这个临时空间。


磨损均衡


闪存中每个块的写入次数都是有限制的,超过这个限制,块就可能会变得不可靠,不能再被使用。这就是我们通常所说的闪存的磨损。


为了尽可能延长闪存的使用寿命,我们需要尽量均匀地使用所有的闪存块,确保每个块的使用频率大致相同。这就是磨损均衡的主要目标。


假设我们发现块A的使用频率过高,我们需要将它的数据移动到没怎么用过的块B去,以达到磨损均衡的目的。首先,我们需要读取块A中的数据,然后将这些数据暂时存储到预留空间。然后,我们擦除块A,将它标记为空闲。最后,我们从预留空间中取出数据,写入到块B。实际上,磨损均衡的策略比这更复杂,不仅仅是看使用频率,还需要考虑其他因素,比如块的寿命,数据的重要性等。


可以看到,预留空间在这个过程中起到了临时存储数据的作用。


不过你可能会问,为什么不直接将块A的数据复制到块B,而需要一个临时空间?


这是因为在实际操作中直接复制块A的数据到块B会带来一些问题和限制。


假如直接进行这种数据复制,那么在数据从块A复制到块B的过程中,块A和块B中都会存在一份相同的数据,如果有其他进程在这个过程中访问了这份数据,可能会产生数据一致性的问题。此外,如果移动过程中发生意外中断,如电源故障,可能会导致数据在块B中只复制了一部分,而块A中的数据还未被擦除,这样就可能导致数据丢失或者数据不一致的问题。


而如果我们使用预留空间,也就是引入一个第三方,就可以缓解这些问题。我们先将数据从块A复制到预留空间,然后擦除块A,最后再将预留空间中的数据写入到块B。在这个过程中,我们可以借助预留空间来实现一些原子性的机制,来保证数据不会丢失和数据的一致性。


错误校正


预留空间还可以用来存储错误校正码(ECC)。如果在读取数据时发现有错误,可以用错误校正码来修复这些错误,提高数据的可靠性。


很多同学可能也不了解这个错误校正码的来龙去脉,这里多说几句。


我们知道计算机中的数据最终都是二进制的0和1,0和1使用硬件比较好表达,比如我们使用高电压表示1,低电压表示0。但是硬件有时候会出错,本来写进去的是1,读出来的却是0。为了解决这个问题,设计师们就搞出来个错误校正码,这个校正码是使用某些算法基于要存储的数据算出来的,存储数据的时候把它一起保存起来。读取数据的时候再使用相同的算法进行计算,如果两个校正码对不上,就说明存储的数据出现错误了。然后ECC算法可以通过计算知道是哪一位出现了错误,改正它就可以恢复正确的数据了。


注意ECC能够修正的二进制位数有限,因为可以修复的位数越多,额外需要的存储空间也越大,具体能修复几位要考虑出现坏块的概率以及数据的重要性。


坏块管理


当闪存单元变为坏块时,预留空间可以提供新的闪存单元来替代坏块,此时读取对应数据时不再访问坏块,而是通过映射表转到预留空间中读取,从而保证数据的存储和读取不受影响,提高了固态硬盘的可靠性和耐用性。


综上所述,预留空间在提升固态硬盘性能,延长其使用寿命,提高数据的可靠性等方面发挥着重要的作用。


小米的优化


根据公开资料,小米将预留空间的占比从6.9%压缩到了约3%。


那么小米是怎么做到的呢?以下是官方说法:


小米在主机端也基于文件管理深度介入了 UFS 的资源管理,通过软件实现“数据非必要不写入(UFS)”,通过软件 + 固件实现“写入数据非必要不迁移”,减少写入量的同时也实现了更好的 wear-leveling 和 WAF


还有一张图:



优化解读


这里用了一些术语,文字也比较抽象,我这里解读下:


UFS(Universal Flash Storage)即通用闪存存储,可以理解为就是手机中的存储模块。


“数据非必要不写入(UFS)”也就是先把数据写入到缓冲区,然后等收到足够的数据之后(比如1页),再写入闪存单元,这样就可以减少闪存单元的擦写次数,自然就能延长闪存单元的使用寿命,推迟坏块的产生。这个缓冲区类似于计算机的内存,如果突然掉电可能会丢失一部分数据,但是对于手机来说,突然掉电这个情况发生的几率极低,所以小米在这里多缓存点数据对数据丢失的影响很小,不过还是需要注意缓冲空间有限,这个值也不能太大,具体多少小米应该经过大量测试之后做了评估。


“写入数据非必要不迁移” 没有细说怎么做的,大概率说的是优化磨损均衡、垃圾回收和整理策略,没事别瞎整理,整理的时候尽量少擦写,目的还是延长闪存单元的使用寿命。


“增加坏块预留” 小米可以根据用户的使用情况调整坏块预留区的大小,比如用户是个重度手机使用狂,他用1年相当于别人用4年,小米系统就会增加坏块预留区,以应对擦写次数增加带来的坏块几率增加。注意这个调整是在云端实现的,如果手机不联网,这个功能还用不上。


wear-leveling:就是上面提到的磨损均衡,小米优化了均衡算法,减少擦写。


WAF:写放大,Write Amplification Factor,缩写WAF。写放大就是上面提到的“读-改-写”操作引起的,因为擦除必须擦掉整个块的数据,所以上层系统只需要写一个页的情况下,底层存储可能要重写一个块,从页到块放大了写操作的数据量。因为闪存的寿命取决于擦除次数,所以写放大会影响到闪存的使用寿命。


概括来说就是,小米从存储的预留空间中抠出来一部分作为用户存储,不过预留空间的减小,意味着坏块管理、错误纠正等可以使用的空间变小,这些空间变小会减少存储的使用寿命,所以小米又通过各种算法延缓了手机存储的磨损速度,如此则对大家的使用没有什么影响,而用户又能多得一些存储空间。


小米的测试结果


对于大家担心小米手机存储的寿命问题,小米手机系统软件部总监张国全表示:“按照目前重度用户的模型来评估,在每天写入40GB数据的条件下, 256GB的扩容芯片依然可以保证超过10年, 512GB可以超过20年,请大家放心。”


同时一般固态硬盘往往都拥有5年的质保,而很多消费者往往会5年之内更换手机。因此按着这个寿命数据来看,普通消费者并不用太担心“扩容芯片”的寿命问题。所以如果你的手机用不了10年,可以不用担心这个问题。


当然更多的测试细节,小米并没有透漏,比如读写文件的大小等。不过按照小米的说法,存储的供应商也做了测试,没有什么问题。这个暂时只能相信小米是个负责任的企业,做好了完备的测试。




最后小米搞了这个技术,申请了专利,但是又把标准和技术方案贡献给了UFS协会,同时还要求存储芯片厂商设置了半年的保护期,也就是说技术可以分享给大家,但是请大家体谅下原创的辛苦,所以半年后其它手机厂商才能用上。


大家猜一下半年后其它手机厂商会跟进吗?


关注微/信/公/众/号:萤火架构,提升技术不迷路。


作者:萤火架构
来源:juejin.cn/post/7297423930225639465
收起阅读 »

游戏服务器搭建过程中Maven多模块编译遇到的一些问题

游戏服务器有好几个项目,所以涉及到多个模块,这个开发中也是经常遇到的,但是因为之前一直没怎么关注,所以在开发的过程中的一些细节问题还是不懂,这次记录下,也分享下同样的问题给没试过的同学 1、多模块的创建 使用idea进行模块的创建,主要分为以下几步 1.1 父...
继续阅读 »

游戏服务器有好几个项目,所以涉及到多个模块,这个开发中也是经常遇到的,但是因为之前一直没怎么关注,所以在开发的过程中的一些细节问题还是不懂,这次记录下,也分享下同样的问题给没试过的同学


1、多模块的创建


使用idea进行模块的创建,主要分为以下几步


1.1 父模块的创建


直接创建一个Maven项目,在这个过程中选择了Spring web 和Lombok,也可以选择其他的包,到时候省的手写


image.png


1.2 删除垃圾文件


删除所有的文件,只留下pom.xml就行,因为父模块只是做一个模块和依赖管理的作用,因此不需要代码。


image.png


1.3 修改pom.xml


修改这个父模块的pom.xml文件,首先把节点、节点和全部删除:然后修改版本号为自己定义的(方便后续子模块指定父模块)


    <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.13</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>MultMoudle</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>MultMoudle</name>
<description>MultMoudle</description>
<packaging>pom</packaging>
</project>


1.4 创建子模块继承


继承父模块


image.png


可以看到父模块的pom中,已经有子模块的配置了。


2、子模块之间的互相引用


因为有一些接口文件需要在几个项目中共同使用,所以就要抽取相同的文件到common项目中,这个是非常合理的,在room和game中怎么引用呐,非常简单,和其他的jar包一样


    
<dependency>
<groupId>com.pdool</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

使用坐标和版本进行引用,刷新引用就可以在引用中看到了


image.png


3、多个模块间版本的管理


多个模块中使用的依赖很有可能会有重复,但是怎么管理各个版本呐?


假设上述module-one和module-two都需要依赖fastjson2,我们之前会在两个模块的pom.xml中加入依赖,但是这样重复的配置看起来似乎不太优雅,如果需要升级要改2个地方,这个时候parent模块的管理功能就可以发挥了


3.1 dependencis


image.png


在上图中,在dependencis中加入了两个常用的工具库,在子模块中即使不加入也可以使用这个库了!因为子模块除了可以使用自己的依赖之外,还会向上查找父模块的依赖,也就是说,父模块的依赖是向下继承的,因此对于所有模块都要使用的依赖,我们可以写在父模块中。


所以,两个模块都依赖于Spring Web话,也可以将两个模块的Spring Web依赖移至父模块。


所以说父模块和子模块中,依赖也有着继承的关系!父模块的properties也是向下继承的。


3.2 dependencyManagement


dependencyManagement用于管理依赖的版本,我们在父模块的pom.xml加入这个标签:


image.png


dependencyManagement的注意事项:


dependencyManagement仅用于管理版本,而不会为自己以及子模块导入依赖,因此在dependencyManagement中声明依赖后,对应的子模块仍然需要在dependencies中加入依赖
在pom.xml中dependencyManagement和dependencies同级,并且dependencyManagement中也需要有一个dependencies
dependencyManagement不仅可以管理子模块的依赖版本,也可以管理自身的依赖版本
若不想让某个子模块使用父模块dependencyManagement的版本,那就在这个子模块的dependencies中声明对应版本
4、依赖执行
因为在项目中使用了mybatis-plus ,在common模块中定义了一些mapper,在room和game中需要使用,在项目中增加了componentscan ,但是依然在运行的时候报错,提示找不到common中的类,在编辑器中并不报错,这很奇怪,需要在依赖项目中增加一个标签。


这是由于Spring Boot打包的模式问题,我们打开被依赖模块module-two的pom.xml文件找到最下面节点中,在spring-boot-maven-plugin插件部分中加入下面配置:



exec
最终common中pom的定义



    
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>execute</classifier>
</configuration>
</plugin>
</plugins>

</build>

5、在Spring Boot项目中加载依赖项目的组件有几种常用的方法


在Spring Boot项目中加载依赖项目的组件有几种常用的方法:


5.1. 使用@ComponentScan注解:


在主应用程序类上使用@ComponentScan注解,指定要扫描的包路径。这将使Spring Boot扫描并加载依赖项目中带有@Component、@Service、@Repository等注解的组件。例如:


    
@SpringBootApplication
@ComponentScan(basePackages = "com.example.dependencyproject")
public class MyApplication {
// ...
}

2. 使用@Import注解:


在主应用程序类上使用@Import注解,引入依赖项目中的配置类或组件类。这将使Spring Boot加载这些配置类并注册其中的组件。例如:


     @SpringBootApplication
@Import(com.example.dependencyproject.MyConfiguration.class)
public class MyApplication {
// ...
}

3. 使用@Configuration注解和@Bean方法:


如果依赖项目中有@Configuration注解的配置类,可以在主应用程序类中使用@Bean方法来加载其中的组件。例如:


    
@SpringBootApplication
public class MyApplication {
// ...

@Bean
public MyComponent myComponent() {
return new MyComponent();
}
}

这样,MyComponent将作为一个Bean被加载到Spring应用程序上下文中。


根据你的具体情况和依赖项目的结构,你可以选择适合的方法来加载依赖项目的组件。请注意,为了能够加载依赖项目的组件,确保依赖项目已被正确地添加为项目的依赖项,并且在构建和部署过程中能够被正确地引用和访问。


6、@ComponentScan 扫描依赖项的包


@ComponentScan 注解可以用于指定要扫描的包,它的作用不限于只扫描依赖项的包。@ComponentScan`可以扫描指定包及其子包下的组件,并将其加载到应用程序的上下文中。


当你在 Spring Boot 应用程序的主类上使用 @ComponentScan 注解时,它将扫描指定的包及其子包,并注册在这些包中使用了 @Component@Service@Repository@Controller 等注解的组件。


如果你指定的包路径包括了依赖项的包,那么它将扫描并加载依赖项中的组件。但是需要注意,@ComponentScan 不会限制只扫描依赖项的包,它将扫描所有指定的包路径下的组件。


举个例子,假设你的 Spring Boot 应用程序的主类上使用了以下的 @ComponentScan 注解:


    @SpringBootApplication
@ComponentScan(basePackages = {"com.example.myapp", "com.example.dependency"})
public class MyApplication {
// ...
}

在上述示例中,@ComponentScan 将扫描 com.example.myapp 包及其子包以及 com.example.dependency 包及其子包下的组件,并将它们加载到应用程序的上下文中。这样,你可以同时加载依赖项中的组件和应用程序本身的组件。


总之,@ComponentScan 注解不仅限于扫描依赖项的包,它可以扫描指定包及其子包下的所有组件,并将它们加载到应用程序的上下文中。


注:


如果你在 Spring Boot 应用程序的主类上使用了 @ComponentScan 注解,并指定了包路径,只有在指定的包路径下的本项目组件会被自动加载到应用程序的上下文中。


@ComponentScan 注解仅扫描指定的包及其子包下的组件,并将它们加载到应用程序的上下文中。如果本项目中的组件不在指定的包路径下,它们将不会被自动加载。


7、总结


不做不知道,手高眼低不行,必须得实践


作者:香菜菜
来源:juejin.cn/post/7297848688244441122
收起阅读 »

Coremail重磅发布2023年Q3企业邮箱安全性报告

10月25日,Coremail邮件安全联合北京中睿天下信息技术有限公司发布《2023年第三季度企业邮箱安全性研究报告》。2023年第三季度企业邮箱安全呈现出何种态势?作为邮箱管理员,我们又该如何做好防护?一、国内垃圾邮件激增,环比增长31.63%根据Corem...
继续阅读 »

10月25日,Coremail邮件安全联合北京中睿天下信息技术有限公司发布2023年第三季度企业邮箱安全性研究报告》。2023年第三季度企业邮箱安全呈现出何种态势?作为邮箱管理员,我们又该如何做好防护?

一、国内垃圾邮件激增,环比增长31.63%

根据Coremail邮件安全人工智能实验室(以下简称“AI实验室”)数据,2023 年Q3国内企业邮箱用户共收到近 亿封的垃圾邮件,环比增长 7.89%,同比去年同期增长 0.91%尤其是国内垃圾邮件激增,环比增长31.63%

经 AI 实验室分析,在 TOP100 接收列表中,教育领域收到的垃圾邮件高达 2.41 亿封,环比上涨13.8%,持续处于前列。

二、境内钓鱼邮件数量激增,首次超过境外

2023 年Q3,全国的企业邮箱用户共收到钓鱼邮件高达 8606.4 万封,同比激增 47.14%,环比也有 23.67%的上升。从总的钓鱼邮件数量来看,境内和境外的钓鱼邮件都呈现增长趋势。但在 2023 年第三季度,境内钓鱼邮件的数量显著增长,超过了境外钓鱼邮件的数量。

Coremail 邮件安全人工智能实验室发现黑产越来越多利用国内的云平台的监管漏洞发送钓鱼邮件,这对国内云服务提供商而言是巨大的挑战

三、Q3垃圾邮件呈现多元化趋势

2023 年 Q3 的垃圾邮件呈现出多元化的趋势,利用各种语言、主题和策略来达成发送垃圾邮件的目的,包括测试邮件、多语言内容、退税和通知等,数量巨大,层出不穷。

而钓鱼邮件常伪装为系统通知或补贴诈骗,这增加了账户被劫和数据泄露的风险。钓鱼邮件主题常利用紧迫性日常相关性模糊性专业性来吸引受害者,建议用户对此类钓鱼邮件保持高度警惕。

四、关键发现:基于邮件的高级威胁

1横向钓鱼攻击

横向钓鱼攻击直接利用了人们的信任关系,已经成为组织面临的重大威胁,而生成型 AI 为攻击者提供了更加强大的工具,使得这些攻击更加难以防范。

以下为 Coremail 在第三季度的横向钓鱼 (也称为内域钓鱼邮件)的检测和拦截数据分析解读:

① 嵌入式钓鱼 URL 的利用:高达 95%的横向钓鱼攻击使用嵌入钓鱼 URL 的邮件。

② 攻击频率:平均每月,约 25%的组织或企业会遭受一次横向钓鱼攻击。

③ 检测挑战:79%的横向钓鱼邮件需要动态分析嵌入的钓鱼URL,这增加了检测的复杂性和时间成本。

④ 更高的威胁等级:接收横向钓鱼邮件的人员的中招率上升了 200%。

2、商业电子邮件欺诈

商业电子邮件欺诈(BEC)涉及网络罪犯伪装成高管或受信任的供应商,以操纵员工转移资金或敏感信息。

针对商业电子邮件欺诈,以下为 Coremail 在第三季度的数据分析解读:

① 账号失陷与社交工程:高达 90%的 BEC 攻击与账户失陷同时发生,而 9%采用社交工程方法。

② 攻击方法:BEC 攻击主要侧重于直接诈骗钱财或信息。

③ 仿冒策略:85%的 BEC 攻击使用以下仿冒策略。

④ 邮件内容分析:70%的邮件为“银行信息变更请求”,15%为催促付款,12%为银行信息变更。

基于 AI 的新威胁

当然,BEC 攻击不仅仅是技术挑战,它更多的是一个人为问题。这类攻击强调了员工培训和安全意识的重要性,因为员工是这类攻击的第一道防线。同时,技术如双因素身份验证、邮件过滤防护和 AI 驱动的安全工具可以提供额外的防护。

五、新措施:监控,响应与安全意识

邮件作为企业沟通的主要方式,不幸地成为了许多网络威胁的首要入口。鉴于此,维护邮件安全不仅是技术问题,还涉及到组织的多个层面。以下分析了邮件安全厂商、邮箱管理员和用户在邮件安全中的作用以及他们分别在监控、响应和安全意识三个方面的关键角色。

1、组织安全的关键挑战

① 员工的安全意识

员工经常成为安全的最弱环节。安全意识方面的缺乏、不够严格的密码策略、轻率地点击可疑链接或不当地处理敏感信息,都可能导致严重的安全事件。

② 威胁响应流程

一个好的安全响应不仅要能有效地解决问题,还要迅速执行。然而,许多组织的反馈机制和响应矩阵的复杂性导致了繁琐的流程,最终导致效率低下和暴露更多风险。

2Coremail 针对性解决方案

① 利用 LLM 进行用户报告的预分类

为了应对迫在眉睫的网络威胁,Coremail 策略性地利用了大语言模型(LLM)即时预分类用户报告的电子邮件。通过 LLM 系统进行即时评估,安全团队可以迅速优先处理威胁,确保高风险邮件得到及时处理。这不仅极大地提高了威胁管理的效率,而且显著降低了由于延迟响应而可能出现的损害风险。

② 让用户成为安全架构的一部分

对于 Coremail 来说,用户不仅仅是被动的实体,而是安全生态系统中的主动参与者。用户是企业安全中的重要角色。通过培养用户主动报告潜在威胁的文化,不仅强化了安全防御,而且增强了用户的安全意识,从而减轻了管理负担。

如上图是“仿冒发信人,仿冒系统通知”的钓鱼漏判响应处理案例的流程。这个流程中,积极的用户参与、即时的邮件威胁响应以及管理员和邮件厂商的紧密合作,得以确保邮件系统的安全性和邮件威胁管理效率。

收起阅读 »

【Java集合】单列集合Collection常用方法详解,不容错过!

嗨~ 今天的你过得还好吗?路途漫漫终有一归幸与不幸都有尽头🌞在上篇文章中,我们简单介绍了下Java 集合家族中的成员,那么本篇文章,我们就来看看 Java在单列集合中,为我们提供的一些方法,以及单列集合的常用遍历玩法,一起来进入学...
继续阅读 »


嗨~ 今天的你过得还好吗?

路途漫漫终有一归

幸与不幸都有尽头

🌞

在上篇文章中,我们简单介绍了下Java 集合家族中的成员,那么本篇文章,我们就来看看 Java在单列集合中,为我们提供的一些方法,以及单列集合的常用遍历玩法,一起来进入学习吧。

在Java基础中我们也学过,在类实现接口后,该类就会将接口中的抽象方法继承过来,此时该类需要重写该抽象方法,完成具体的逻辑。


Collection 常用功能

Collection 是所有单列集合的父接口,因此在Collection 中定义了单列集合(List 和 Set)通用的一些方法,这些方法可用于操作所有的单列集合。

20190428183815681.png

1.1 方法如下:

image.png

打开api文档,我们可以看到Collection 在 java.util 下,我们通过练习来演示下这些方法的使用:


640 (28).gif


1.2方法演示

public class Demo1Collection {    

       public static void main(String[] args) {

           //创建集合对象

           //使用多态的形式 定义
           Collection<String> person = new ArrayList<>();
           //输出不是一个对象地址,所以说重写了toString 方法

           System.out.println(person);

   

   //        boolean add(Object o) 向集合中添加一个元素
   //        返回值是一个boolean值,一般可以不用接收
           person.add("科比");
           person.add("姚明");
           person.add("库里");

           person.add("麦迪");

           //添加完我们在输出一下这个集合

           System.out.println(person);


   //        boolean remove(Object o) 删除该集合中指定的元素

   //        返回 集合中存在元素,删除元素,返回true;集合中不存在,删除返回false

           boolean res1 = person.remove("科比");

           boolean res2 = person.remove("奥尼尔");

           System.out.println("res1=" +res1);

           System.out.println("res2=" +res2);

   //        boolean isEmpty() 判断该集合是否为空

           boolean empty = person.isEmpty();
           System.out.println("empty=" + empty);    
   //        boolean contains(Object o) 判断该集合中是否包含某个元素
           boolean contains = person.contains("麦迪");

           System.out.println("contains=" + contains);

   //        int size() 获取该集合元素个数

           int size = person.size();

           System.out.println("size = " + size);

   

   //        public Object[] toArray() 把集合总的元素,存储到数组中

           Object[] personArray = person.toArray();

           for (int i = 0; i < personArray.length; i++) {

               System.out.println("数组--" + personArray[i]);
           }    
   //        void clear() 删除该集合中的所有元素,但是集合还存在
           person.clear();
           System.out.println(person);    
   
           //通过多态的方式,如果我们把arrayList 换成HashSet,发现也能使用,这就是我们实现接口的好处
       }
   }

注意:有关Collection中的方法不止上面这些,其他方法可以自行查看API学习。


查询集合中的元素-Iterator 迭代器

2.1 Iterator 接口

在程序开发中,经常需要遍历集合中的所有元素,就是要看看里面所有的元素,那我们怎么办呢? 

20201216223250385.png

针对这种需求,JDK 专门提供了一个接口: java.util.Iterator。该接口也是 Java集合中的一员,但它与 Collection、Map 接口有所不同,Collection 接口 与 Map 接口 主要用于存储元素,而 Iterator 主要用于迭代访问(即 遍历) Collection中的元素,因为Iterator 对象也被称为迭代器。 


下面介绍一下迭代器的概念:

迭代即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续再判断,如果还有就再取出来。一直把集合中的所有元素全部取出。这种方式专业术语称为迭代。


通过文档,我们可以看到 Iterator 是一个接口,我们无法直接使用,而需要使用Iterator接口的实现类对象,通过Collection接口中的 Iterator()方法,就可以返回迭代器的实现类对象:

public Iterator iterator():  获取结合对应的迭代器,用来遍历集合中的元素。

通过API文档,我们可以看到 Collection 中 Itrerator 接口的常用方法如下:

  • public E next()  返回迭代的下一个元素

  • public boolean hasNext() 如果仍有元素可以迭代,则返回true

image.png

接下来我们通过案例学习,如何使用Iterator 迭代集合中的元素:

 /**
    *  Iterator 迭代器使用
    */

   public class Demo1Iterator {    
       public static void main(String[] args) {            /**
            * 使用步骤:
            * 1. 使用集合中的方法 iterator() 获取迭代器的实现类对象,使用Iterator接口接收 (使用接口接收返回值,这就是我们说的多态)
            * 2. 使用Iterator接口中的方法 hashNext() 判断有没有下一个元素
            * 3. 使用Iterator接口中的方法 next() 取出集合中的下一个元素
            *
            */

           Collection<String> ball = new ArrayList<>();
           ball.add("篮球");
           ball.add("足球");
           ball.add("排球");
           ball.add("乒乓球");    
           //我们来获取一个迭代器,多态
           Iterator<String> iterator = ball.iterator();    
           //判断
           boolean b = iterator.hasNext();
           System.out.println("是否有元素--" + b);            //取出
           String next = iterator.next();
           System.out.println("元素--" + next);    
           //判断
           boolean b1 = iterator.hasNext();
           System.out.println("是否有元素--" + b1);            //取出
           String next1 = iterator.next();
           System.out.println("元素--" + next1);    
           //判断
           boolean b2 = iterator.hasNext();
           System.out.println("是否有元素--" + b2);            //取出
           String next2 = iterator.next();
           System.out.println("元素--" + next2);    
           //判断
           boolean b3 = iterator.hasNext();
           System.out.println("是否有元素--" + b3);            //取出
           String next3 = iterator.next();
           System.out.println("元素--" + next3);    
           //判断
           boolean b4 = iterator.hasNext();
           System.out.println("是否有元素--" + b4);            //取出
   //        String next4 = iterator.next();
   //        System.out.println("元素--" + next4);
           //如果没有元素,在取的话,会报一个NoSuchElementException 的错误
   
   
           /**
            *
            * 代码优化 上面这些步骤是一个重复的过程,我们可以使用循环来优化,那我们选择哪种来呢
            * 我们说 知道元素个数,使用for
            * 不知道元素个数,使用while
            *
            * 那当前我们迭代器的个数,我们不知道,所以使用while循环,而我们的hasNext 就可以作为
            * while的条件来判断
            *
            */

           while (iterator.hasNext()) {
               String ballResult = iterator.next();
               System.out.println("--优化--" + ballResult);
           }
   
       }
   }

分析: 

在进行集合元素取出时,如果集合中已经没有元素了,还继续使用迭代器的next方法,将会发生java.util.NoSuchElementException 没有集合元素的错误。


640 (28).gif


2.2 迭代器的实现原理

我们在之前的案例中已经完成了Iterator 遍历集合的整个过程。当遍历集合时,首先通过调用集合的iterator() 方法获得迭代器对象,然后使用hasNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已经到达集合末尾,停止遍历元素。

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

Itearator 迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,为了让初学者能更好的理解迭代器的工作原理,接下来通过一个图例来演示 Iterator 对象迭代元素的过程:

Description

在获取迭代器的实现类对象是,会把索引指向集合的-1位置,也就是在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,不指向任何元素。

  • 当第一次调用迭代器的next方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,

  • 当再次调用next方法时,迭代器的索引会指向第二个元素并将该元素返回,

  • 以此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对元素的遍历。


查询集合中的元素-增强for

3.1  概念

增强for循环(也称为 for each 循环)是JDK5以后出来的一个高级for循环,专门用来遍历数组和集合的。 通过api文档,Collection 继承了一个Iterable 接口 ,而实现这个接口允许对象成为 “foreach” 语句目标,也就是所有的单列集合都可以使用增强for。 

它的内部原理其实是个Iterator 迭代器,只是用for循环的方式来简化迭代器的写法,所以在遍历的过程中,不能对集合中的元素进行增删改查操作。

格式:

for (元素类型 元素名 : 集合名或数组) {
       访问元素
   }

它用于遍历 Collection 和数组。通常只进行遍历元素,不在遍历的过程中对集合元素进行增删操作。


640 (28).gif


3.2 练习1:遍历数组

public class Demo2Foreach {    
       public static void main(String[] args) {    
           int[] array = {1,2,3,4,5};    
           for (int i : array) {
               System.out.println("--数组元素--" + i);    
               if (i == 2) {
                   i = 19;
               }
           }    
           //在增强for中修改 元素,并不能赋值
           System.out.println(Arrays.toString(array));
       }
   }

640 (28).gif


3.3 练习2:遍历集合

public class Demo3Foreach {    
       public static void main(String[] args) {
           Collection<String> ball = new ArrayList<>();
           ball.add("篮球");
           ball.add("足球");
           ball.add("排球");    
           for (String s : ball) {
               System.out.println("---" + s);
           }    
           //相对于Iterator遍历方式,增强for 简化了很多,所以优先使用该方式。
       }
   }

新for 循环必须有被遍历的目标。目标只能是Collection 或者是数组。仅仅作为遍历操作出现。


总结

本篇中主要介绍了单列集合接口Collection为我们提供的常用接口,也通过代码的方式带大家体会了一下。在后面的内容中为大家介绍了如何把单列集合中的内容查看出来(遍历),通过讲解一些底层的原理,让大家感受了一下迭代器的使用。


当然集合的遍历不仅仅限于这两种方式,例如java8为我们提供的流式遍历集合,希望大家下去后自己也能搜搜相关的遍历方式,尝试使用一下,ok,本文就到这里了。



收起阅读 »

以订单退款流程为例,聊聊如何优化策略模式

如果有人问你什么是策略模式?你可以尝试这样回答 策略模式是一种行为设计模式,它允许在运行时根据不同的情况选择不同的算法策略。这种模式将算法的定义与使用的代码分离开来,使得代码更加可读、可维护和可扩展。 在策略模式中,通常有一个抽象的策略接口,定义了一系列可以...
继续阅读 »

如果有人问你什么是策略模式?你可以尝试这样回答



策略模式是一种行为设计模式,它允许在运行时根据不同的情况选择不同的算法策略。这种模式将算法的定义与使用的代码分离开来,使得代码更加可读、可维护和可扩展。


在策略模式中,通常有一个抽象的策略接口,定义了一系列可以被不同策略实现类所实现的方法。在使用时,可以根据需要选择合适的具体策略类,并通过接口进行调用。



虽然解释了策略模式,但是面试官可能会认为你在吊书袋,完全没有自己的理解。我因为被领导卷到了,所以对策略模式有一些其他的理解,接下来我从业务中台实际遇到的问题出发,谈谈怎么被领导卷到,谈谈我对策略模式的优化和理解。


在业务中台,可以根据具体情况选择合适的策略类来执行相应的业务逻辑,从而满足不同业务方的要求。


由于各个业务方的业务逻辑存在差异性和相似性,编写中台代码时需要考虑这些差异性,并预留扩展点以供不同业务方根据自身需求提供策略类。在这种情况下,可以应用策略模式。


image.png


当业务方有特殊的业务逻辑时,只需要添加新的策略实现类即可,不需要修改现有的代码,方便了后续的维护和扩展。


然而在我们在应用策略模式时遇到了几个问题


遇到的实际问题


由于业务中台逻辑非常复杂,每个业务线的业务场景都很难保证完全一样,在代码实践中我们的系统出现了非常庞大复杂的策略类,将大量业务逻辑整合到同一个策略中,这导致系统能力复用非常困难。


举个例子,在退款校验逻辑中 业务场景 A,(售卖红包,类似于美团饿了么会员)。 需要判断如下校验逻辑,如果命中,则不允许退款!




  1. 订单是否在生效期,过期不允许退




  2. 订单是否在已使用,已使用不可退




  3. 订单如果是某类特殊商品订单,则不可退




  4. 如果超过当天最大退款次数,则不可退。




于是我们在编写代码时,就新定义 ConcreteStrategyA ,将以上 4 个业务逻辑放到策略类中。


过一段时间,又出现了业务场景 B(售卖红包,类似于美团饿了么会员)。它的校验逻辑和A 大同小异




  1. 订单是否在生效期,过期不允许退




  2. 订单售卖的红包完全使用不可退,部分使用可以退。




  3. 如果超过当天最大退款次数,则不可退。




业务场景B 相比A 而言,少校验了 “特殊商品订单不可退”的逻辑,同时增加了部分使用可以退的逻辑。


如何设计代码实现两种业务场景的校验逻辑呢? 这是非常具体的问题,如果是你如何实现呢?


完全独立的策略类


我们在最开始写代码时,分别独立实现了 ConcreteStrategyA、ConcreteStrategyB。两者的校验逻辑各自独立,没有代码复用。


此时的系统类图如下


image.png


这种实现方式是大量的代码拷贝,例如退款次数限制、生效期校验等代码都大量重复拷贝了。


后来我们发现了这个问题,将相关校验方法抽到父类中。


继承共同的父类


为了更好的复用校验策略,我们将校验生效期的方法、校验退款次数的方法抽取到共同的父级策略类。由具体的子策略类继承BaseStrategy 父策略类,在校验逻辑中,使用父级的校验方法。


如下图的类图所示
image.png


在相当长的一段时间里,我们认为这已经是最优的实现方式了。


但是被领导卷到了!


image.png


被卷到了


“新增业务场景时,为什么校验逻辑都是复用的原有能力,还需要新增扩展类,还需要开发代码呢?” 领导这样问道。


我尝试回答领导的问题:“开发这段代码,并不算太难,只需要增加一个扩展类就可以了!”


“你数数现在有多少个扩展类了?” 领导似乎有些生气,


我一看扩展类的数量,被吓到了,已经有了15个扩展类。这一刻真的非常尴尬,平时领导从不亲自看代码。估计是突然心血来潮。从表情来看,他好像很生气,估计是代码没看懂!


“我看到这部分代码时,想要查看具体的策略类,但Idea直接刷出了15个策略类……为什么会有这么多的扩展类呢?” 领导进一步补充道。


场面僵住了,我也没什么好办法……。当然领导向来的传统是:只提问题,不给解决办法! 我只能自己想解决办法!如何解决策略类过多、扩展类膨胀的问题呢?


image.png


策略类过多怎么办!


当业务场景非常多,业务逻辑非常复杂时,确实会出现非常多的策略类。但领导的问题是,现有业务场景有很多相似性,某些新增业务场景和原有业务场景类似,校验逻辑也是类似的,为什么还需要新增扩展类呢?


经过深思熟虑,让我发现问题所在!


策略类粒度太粗,导致系统复用难


目前我们的系统设计是每个业务场景都有单独的策略,这是大多数人认可的做法。不同的业务场景需要不同的策略。


然而,仔细思考一下我们会发现,不同业务场景的退款校验逻辑实际上是由一系列细分校验逻辑组合而成的,退款校验逻辑在不同的业务场景中是可以被重复使用的。


为什么不能将这些细分退款校验逻辑抽象为策略呢?例如,将过期不允许退款、已生效不可退款、超过最大退款次数不允许退款等校验逻辑抽象成为校验策略类。


各个业务场景组合多个校验策略,这样新增业务场景时,只需要组合多个校验策略即可。


如何组合校验策略


首先需要抽象校验策略接口: VerifyStrategy


classDiagram
class VerifyStrategy{
+void verify(VerifyContext context);
}

然后定义 VerifyScene 类


classDiagram
class VerifyScene{
+ Biz biz;
+ List<VerifyStrategy> strategies;
}

如何把对应具体策略类配置到VerifyScene中呢?本着 ”能不开发代码,就不要开发代码,优先使用配置!“的原则,我们选择使用Spring XML 文件配置。(在业务中台优先使用配置而非硬编码,否则这个问题不好回答。“业务方和中台都需要开发,为啥走你中台?”)


使用Spring XML 组合校验策略


在Spring XML文件中,可以声明VerifyScene类的Bean,初始化属性时,可以引用相关的校验策略。


 <bean name="Biz_A_Strategy" p:biz="A" class="com.XX.VerifyScene">
<property name="strategies">
<list>
<ref bean="checkPeriodVerifyStrategy"/> <!--校验是否未过期-->
<ref bean="checkUsageInfoVerifyStrategy"/> <!--校验使用情况-->
<ref bean="checkRefundTimeVerifyStrategy"/><!--校验退款次数-->
</list>
</property>
</bean>

当需要新增业务场景时,首先需要评估现有的校验策略是否满足需求,不满足则新增策略。最终在XML文档中增加 VerifyScene 校验场景,引用相关的策略类。


这样新增业务场景时,只要校验逻辑是复用的,就无需新增扩展类,也无需开发代码,只需要在XML中配置策略组合即可。


在XML文档中可以添加注释,说明当前业务场景每一个校验单元的业务逻辑。在某种程度上,这个XML文档就是所有业务的退款校验的业务文档。甚至无需再写文档说明每个业务场景的退款策略如何如何~


和领导汇报以后,领导很是满意。对业务方开始宣称,我们的中台系统支持零开发,配置化接入退款能力。


结束了吗?没有 ,我们后来想到更加优雅的方式。


使用Spring Configuration 和 Lamada


Spring 提供了@Bean注解注入Bean,我们可以使用Spring @Bean方式声明校验策略类


@Bean
public VerifyStrategy checkPeriodVerifyStrategy(){
return (context)->{
//校验生效期
};
}

通过以上方式,可以把checkPeriodVerifyStrategy 校验策略注入到Spring中,spring beanName就是方法名checkPeriodVerifyStrategy。


在Spring XML中可以使用 <ref bean="checkPeriodVerifyStrategy"/> 引用这个bean。


并且当点击XML中beanName时,可以直接跳转到 被@Bean修饰的checkPeriodVerifyStrategy方法。这样在梳理校验流程时,可以很方便地查看代码


点击这个BeanName,会跳转到对应的方法。(付费版Idea支持,社区版 Idea 不支持这个特性)
image.png


总结


总结几个问题




  1. 策略模式目的是:根据不同的业务场景选择不同的策略来执行相应的逻辑




  2. 策略模式一定要进行细化,通过组合多个细分策略模式为一个更大的策略,避免使用继承方案。




  3. 使用Spring XML 组合多个策略模式,可以避免开发。减少新增策略类




  4. 使用Spring Configuration @Bean 将策略类注入Spring 更加优雅




作者:他是程序员
来源:juejin.cn/post/7295010992122101801
收起阅读 »

从码农到工匠-怎么写好一个方法

感谢你阅读本文 今天我们来分享一下怎么怎么写好一个方法,我记得大四的时候彪哥推荐我去读《代码精进之路,从码农到工匠》这本书,那时候由于项目经验不多,所以对于书中很多东西没有感同身受,所以不以为然。 后面做的项目越来越多,从痛苦中不断思考,也看了一些优秀开源框架...
继续阅读 »

感谢你阅读本文


今天我们来分享一下怎么怎么写好一个方法,我记得大四的时候彪哥推荐我去读《代码精进之路,从码农到工匠》这本书,那时候由于项目经验不多,所以对于书中很多东西没有感同身受,所以不以为然。


后面做的项目越来越多,从痛苦中不断思考,也看了一些优秀开源框架的实现,逐渐对于开始理解书中的思想。


下文只列举了书中的的“方法封装”,“参数”和“方法短小”这三个,其他的没有列举,因为我觉得其实只要这三点我们执行到位,写出来的代码虽然谈不上优秀,但是维护性和可读性一定会大大提高。


封装的艺术


我们在学习面向对象编程时就知道面向对象的几大特征是抽象,继承,封装,多态,所以学会合理的封装是一件很重要的事情,这也是我们每个程序员应该有的意识。


判断封装


在方法中,总是难免会有很多判断,判断越多,程序就会变得越复杂,但是又不能不判断,我们可以使用设计模式来改善判断很多的方法,使用设计模式来优化判断,比如策略模式,但是其实它并没有消除判断,换句话说,判断是消除不了,我们能做的只能是让判断的代码可读,下面我们来看一段代码。


如下是一个判断,对文件小于1G,类型为txt,文件名中包含user_click_data或者super_user_click_data的文件可以进行处理


对于这个判断,虽然条件并不复杂,但是在代码中就显得“不清洁”,特别是随着系统的不断迭代,加入的人员不断增多,那么判断将会越来越多,如果开发人员加上清晰的注释,那么就比较容易读懂,但是并不是谁都有这样的习惯。


之前我遇到的代码中,有些一个方法中四五十个判断,而且判断还特别复杂,各种&&,||等一大堆,读起来十分费劲,对于这样的代码,遇到真的会让人抓狂。


如何解决呢?


封装判断就是一个好的方案,上面的判断代码我们就可以进行封装,如下。



上面我判断我们将其抽出来作为一个单独的方法,在加上可理解的方法名,那么别人在读你代码的时候就很容易读懂,也保证了代码的清洁,当然,方法命名是一件很难的事情,不过如果命名得不够准确,我们可以加上清晰的注释来解决。


虽然上面这段代码看似没啥改变,但是随着系统的不断复杂,就能体现出它的作用了,我经常看到别人在主流程的代码中加入很多复杂的判断,在我刚开始写代码的时候,也做过这种事,很多时候如果注释不清,命名不规范,上下文不清晰,那么读起来就很痛苦,而且整个方法看起来让人特别难受。


所以,一个看似很小的改动,却能在时间的作用下显得如此重要,保持这样一个习惯,不仅是对自己负责,也是对他人负责。


参数问题


参数问题也是我们应该去考虑的问题,当在一个方法中,如果参数特别多是很不友好的,我之前碰到过一个方法有十五六个参数,首先这么多个参数,在传递的时候,如果使用对IDEA使用比较熟悉,那么可以使用快捷键查看下一个参数是什么类型,如果使用不熟悉,那么就需要点进方法中去看,这是一个体力活。


加上参数越多,维护成本就越高,比如我需要加一个参数,但是这个参数并不是必须的,有些方法用到,有些不用,但是由于使用的是同一个方法,所以调用这个方法的地方都需要加上这个参数,调用的地方越多,成本就越大,如果是一个RPC接口,修改的成本更大。


复杂的参数封装成类。


对于复杂的参数,推荐封装成类然后进行传递,在添加或者减少字段的时候是在类中进行,没有对形参进行增加或者减少,对调用此方法的地方基本没任何影响,我们只需要在需要添加或者减少这个参数的地方进行操作,不需要的地方就保持不动。


方法尽量保持短小


方法短小更容易让人阅读和维护,当我去阅读别人的代码的时候,有些类有七八千行代码,一个方法有五六百行代码的时候,其实内心是奔溃的!


当然,在我刚开始写代码的时候,我也不懂,所以一个方法从来不去优化,也是一个方法一口气干到底,我记得当时给老师做一个项目,一个类我在Controller层直接写了几百行,Service层都不用,还有写Vue,也不会去使用组件和封装函数,所以一个Vue文件几千行,当需要修改的时候,十分麻烦,找一个方法和变量也特别费力。


记得上一个星期,我和前端同事联调,我看他去找一个方法找了足足三四分钟,因为项目十分庞大,加上设计有很大的缺陷,我看他们一个VUE文件可达到八九千行,所以才出现这个问题。


其实无论对于前端还是后端,Java还是golang等等,代码的短小都比较容易去阅读和维护,如果发现方法越写越大,那我们就要考虑去提取,去拆分了,《代码精进之路》作者推荐Java一个方法不要超过20行,不过也不是绝对的。


对于像Spring这种特别复杂的框架,因为不断升级和迭代,所以有些方法也变得特别长,代码也变得可读性不那个高,但是也不影响它是Java生态中使用最广泛,最强大的框架。


只不过对于我们普通人来说,因为水平肯定没有Spring团队的强,所以保证方法的可读性是十分重要的。


今天的分享就到这里,感谢你的观看,我们下期见。


参考:《代码精进之路,从码农到工匠》weread.qq.com/web/reader/…?


作者:刘牌
来源:juejin.cn/post/7295237661617995828
收起阅读 »

工作六年,看到这样的代码,内心五味杂陈......

工作六年,看到这样的代码,内心五味杂陈...... 那天下午,看到了令我终生难忘的代码,那一刻破防了...... 🔊 本文记录那些年的 Java 代码轶事 ヾ(•ω•`)🫥 故事还得从半年前数据隔离的那个事情说起...... 📖一、历史背景 1.1 数据隔...
继续阅读 »

工作六年,看到这样的代码,内心五味杂陈......


那天下午,看到了令我终生难忘的代码,那一刻破防了......



🔊 本文记录那些年的 Java 代码轶事



ヾ(•ω•`)🫥 故事还得从半年前数据隔离的那个事情说起......


📖一、历史背景


1.1 数据隔离


预发,灰度,线上环境共用一个数据库。每一张表有一个 env 字段,环境不同值不同。特别说明: env 字段即环境字段。如下图所示:
image.png


1.2 隔离之前


🖌️插曲:一开始只有 1 个核心表有 env 字段,其他表均无该字段;
有一天预发环境的操作影响到客户线上的数据。 为了彻底隔离,剩余的二十几个表均要添加上环境隔离字段。


当时二十几张表已经大量生产数据,隔离需要做好兼容过渡,保障数据安全。


1.3 隔离改造


其他表历史数据很难做区分,于是新增加的字段 env 初始化 all ,表示预发线上都能访问。以此达到历史数据的兼容。


每一个环境都有一个自己独立标志;从 application.properties 中读该字段;最终到数据库执行的语句如下:


SELECT XXX FROM tableName WHERE env = ${环境字段值} and ${condition}

1.4 隔离方案


最拉胯的做法:每一张表涉及到的 DO、Mapper、XML等挨个添加 env 字段。但我指定不能这么干!!!


image.png


具体方案:自定义 mybatis 拦截器进行统一处理。 通过这个方案可以解决以下几个问题:



  • 业务代码不用修改,包括 DO、Mapper、XML等。只修改 mybatis 拦截的逻辑。

  • 挨个添加补充字段,工程量很多,出错概率极高

  • 后续扩展容易


1.5 最终落地


在 mybatis 拦截器中, 通过改写 SQL。新增时填充环境字段值,查询时添加环境字段条件。真正实现改一处即可。 考虑历史数据过渡,将 env = ${当前环境}
修改成 en 'all')


SELECT xxx FROM ${tableName} WHERE env in (${当前环境},'all') AND ${其他条件}

具体实现逻辑如下图所示:


image.png



  1. 其中 env 字段是从 application.properties 配置获取,全局唯一,只要环境不同,env 值不同

  2. 借助 JSqlParser 开源工具,改写 sql 语句,修改重新填充、查询拼接条件即可。链接JSQLParser


思路:自定义拦截器,填充环境参数,修改 sql 语句,下面是部分代码示例:


@Intercepts(
{@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}
)

@Component
public class EnvIsolationInterceptor implements Interceptor {
......
@Override
public Object intercept(Invocation invocation) throws Throwable {
......
if (SqlCommandType.INSERT == sqlCommandType) {
try {
// 重新 sql 执行语句,填充环境参数等
insertMethodProcess(invocation, boundSql);
} catch (Exception exception) {
log.error("parser insert sql exception, boundSql is:" + JSON.toJSONString(boundSql), exception);
throw exception;
}
}

return invocation.proceed();
}
}

一气呵成,完美上线。


image.png


📚二、发展演变


2.1 业务需求


随着业务发展,出现了以下需求:



  • 上下游合作,我们的 PRC 接口在匹配环境上与他们有差异,需要改造


SELECT * FROM ${tableName} WHERE bizId = ${bizId} and env in (?,'all')


  • 有一些环境的数据相互相共享,比如预发和灰度等

  • 开发人员的部分后面,希望在预发能纠正线上数据等


2.2 初步沟通


这个需求的落地交给了来了快两年的小鲜肉。 在开始做之前,他也问我该怎么做;我简单说了一些想法,比如可以跳过环境字段检查,不拼接条件;或者拼接所有条件,这样都能查询;亦或者看一下能不能注解来标志特定方法,你想一想如何实现......


image.png


(●ˇ∀ˇ●)年纪大了需要给年轻人机会。


2.3 勤劳能干


小鲜肉,没多久就实现了。不过有一天下午他遇到了麻烦。他填充的环境字段取出来为 null,看来很久没找到原因,让我帮他看看。(不久前也还教过他 Arthas 如何使用呢,这种问题应该不在话下吧🤔)


2.4 具体实现


大致逻辑:在需要跳过环境条件判断的方法前后做硬编码处理,同环切面逻辑, 一加一删。填充颜色部分为小鲜肉的改造逻辑。


image.png


大概逻辑就是:将 env 字段填充所有环境。条件过滤的忽略的目的。


SELECT * FROM ${tableName} WHERE env in ('pre','gray','online','all') AND ${其他条件}

2.5 错误原因


经过排查是因为 API 里面有多处对 threadLoal 进行处理的逻辑,方法之间存在调用。 简化举例: A 和 B 方法都是独立的方法, A 在调用 B 的过程,B 结束时把上下文环境字段删除, A 在获取时得到 null。具体如下:


image.png


2.6 五味杂陈


当我看到代码的一瞬间,彻底破防了......


image.png


queryProject 方法里面调用 findProjectWithOutEnv,
在两个方法中,都有填充处理 env 的代码。


2.7 遍地开花


然而,这三行代码,随处可见,在业务代码中遍地开花.......


image.png


// 1. 变量保存 oriFilterEnv
String oriFilterEnv = UserHolder.getUser().getFilterEnv();

// 2. 设置值到应用上下文
UserHolder.getUser().setFilterEnv(globalConfigDTO.getAllEnv());

//....... 业务代码 ....

// 3. 结束复原
UserHolder.getUser().setFilterEnv(oriFilterEnv);

image.png


改了个遍,很勤劳👍......


2.8 灵魂开问


image.png


难道真的就只能这么做吗,当然还有......



  • 开闭原则符合了吗

  • 改漏了应该办呢

  • 其他人遇到跳过的检查的场景也加这样的代码吗

  • 业务代码和功能代码分离了吗

  • 填充到应用上下文对象 user 合适吗

  • .......


大量魔法值,单行字符超500,方法长度拖几个屏幕也都睁一眼闭一只眼了,但整这一出,还是破防......


内心涌动😥,我觉得要重构一下。


📒三、重构一下


3.1 困难之处


在 mybatis intercept 中不能直接精准地获取到 service 层的接口调用。 只能通过栈帧查询到调用链。


3.2 问题列表



  • 尽量不要修改已有方法,保证不影响原有逻辑;

  • 尽量不要在业务方法中修改功能代码;关注点分离;

  • 尽量最小改动,修改一处即可实现逻辑;

  • 改造后复用能力,而不是依葫芦画瓢地添加这种代码


3.3 实现分析



  1. 用独立的 ThreadLocal,不与当前用户信息上下文混合使用

  2. 注解+APO,通过注解参数解析,达到目标功能

  3. 对于方法之间的调用或者循环调用,要考虑优化


同一份代码,在多个环境运行,不管如何,一定要考虑线上数据安全性。


3.4 使用案例


改造后的使用案例如下,案例说明:project 表在预发环境校验跳过


@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})


@SneakyThrows
@GetMapping("/importSignedUserData")
@InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})
public void importSignedUserData(
......
HttpServletRequest request,
HttpServletResponse response)
{
......
}

在使用的调用入口处添加注解。


3.5 具体实现



  1. 方法上标记注解, 注解参数定义规则

  2. 切面读取方法上面的注解规则,并传递到应用上下文

  3. 拦截器从应用上下文读取规则进行规则判断


image.png


注解代码


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface InvokeChainSkipEnvRule {

/**
* 是否跳过环境。 默认 true,不推荐设置 false
*
* @return
*/

boolean isKip() default true;

/**
* 赋值则判断规则,否则不判断
*
* @return
*/

String[] skipEnvList() default {};

/**
* 赋值则判断规则,否则不判断
*
* @return
*/

String[] skipTableList() default {};
}


3.6 不足之处



  1. 整个链路上的这个表操作都会跳过,颗粒度还是比较粗

  2. 注解只能在入口处使用,公共方法调用尽量避免


🤔那还要不要完善一下,还有什么没有考虑到的点呢? 拿起手机看到快12点的那一刻,我还是选择先回家了......


📝 四、总结思考


4.1 隔离总结


这是一个很好参考案例:在应用中既做了数据隔离,也做了数据共享。通过自定义拦截器做数据隔离,通过自定注解切面实现数据共享。


4.2 编码总结


同样的代码写两次就应该考虑重构了



  • 尽量修改一个地方,不要写这种边边角角的代码

  • 善用自定义注解,解决这种通用逻辑

  • 可以妥协,但是要有底线

  • ......


4.3 场景总结


简单梳理,自定义注解 + AOP 的场景


场景详细描述
分布式锁通过添加自定义注解,让调用方法实现分布式锁
合规参数校验结合 ognl 表达式,对特定的合规性入参校验校验
接口数据权限对不同的接口,做不一样的权限校验,以及不同的人员身份有不同的校验逻辑
路由策略通过不同的注解,转发到不同的 handler
......

自定义注解很灵活,应用场景广泛,可以多多挖掘。


4.4 反思总结



  • 如果一开始就做好技术方案或者直接使用不同的数据库是否可以解决这个问题

  • 是否可以拒绝那个所谓的需求

  • 先有设计再有编码


4.5 最后感想


image.png



在这个只讲业务结果,不讲技术氛围的环境里,突然有一些伤感;身体已经开始吃不消了,好像也过了那个对技术较真死抠的年纪; 突然一想,这么做的意义又有多大呢?



作者:uzong
来源:juejin.cn/post/7294844864020430902
收起阅读 »

一亿数据大表,我们是如何做分页的

本文是基于我们公司的情况做的大表分页方案简单记录,不一定适合所有业务场景,大家感兴趣的读一下,可以在评论区讨论,技术交流,文明用语。 最近在做一个功能,有一张大概一亿的数据需要做分页查询,所以存在深分页的问题,也就是越靠后的页访问起来就会越慢,所以需要对这个功...
继续阅读 »

本文是基于我们公司的情况做的大表分页方案简单记录,不一定适合所有业务场景,大家感兴趣的读一下,可以在评论区讨论,技术交流,文明用语。


最近在做一个功能,有一张大概一亿的数据需要做分页查询,所以存在深分页的问题,也就是越靠后的页访问起来就会越慢,所以需要对这个功能进行优化。


对于深分页问题,一般都是采用传递主键ID的做法来解决,也就是每次查询下一页数据时,把上一页数据中最大的ID传递给下一页,这样在查询某一页时就能利用主键索引来快速查询了。


但是由于历史原因,那张一亿数据的大表没有主键ID,-_-,所以这是需要解决的问题之一。


另外,就算能利用主键索引快速查询分页数据,但是毕竟是一亿数据,最终查询比较靠后的页数时,因为数据量大就算走索引最后还是会比较慢,所以就需要分表了。


我们先使用SQL脚本的方式把一亿数据迁移到分表,我们按照时间季度进行分表,比如2022q1、2022q2、2023q1这种方式来进行分表,这样每张分表的数据就不会太多了,大概1000万左右,并且在创建分表时顺便给分表添加主键ID字段,然后在迁移数据时需要按照原始大表的交易时间进行升序排序,保证ID的自增顺序与交易时间顺序是一致的。


不过这种方案存在两个问题,第一个问题就是查询分页时跨分表了怎么办,比如我查第10页数据时,一部分数据在2023q1,一部分数据在2023q2,首先在查询时本身就需要选择年份和季度,所以直接就确定了对应的是哪张分表,查询时根据年份和季度直接定位到分表,然后再进行分页查询,所以不会出现跨表,这个问题就自然而然解决了。


另外,每张表的主键ID要不要连续,比如2022q1的主键是1-1000000,那么2022q1的主键要不要是1000001-2000000,其实我个人觉得需要这么设计,因为两个分表的数据逻辑上其实一张表,所以主键最好不冲突,但是考虑到实现难度,暂时没有采取,而是每张分表的主键ID都是从1开始的,这种方案在目前看来是没有问题的,因为每次都只会从一张分表中查询数据,不会出现跨表的情况。


另外,在设计时也想到过能不能直接用交易时间做为主键来进行分表,但是想到交易时间的精度是秒,所以很有可能出现交易时间相同的记录,这样在做分页时可能会出现下一页数据和上一页数据有重复的,所以还是需要单独设计一个主键ID。


继续思考,假如这张一亿数据的大表要做分页,但是不根据年份和季度做查询来分页,而就是直接分页,那又该如何呢?


首先,肯定还是得利用分表和主键索引,我的思路是,先给原始表添加主键ID并生成自增ID,然后再按主键ID进行分表,分表记录数也可以控制在1000万左右,前端查询时仍然按ID来查分页就可以了,但是此时就存在跨表的问题,比如每页如果是20条,查第10页数据时,如果从分表一只查出了5条,那么后端接口判断5小于20,就尝试从分表二中继续查。


当然了,如果既有复杂的查询条件,又需要进行分页,那用关系型数据库就不太好做了,就可以考虑ES之类的了。


大家有什么更好的经验或思路,欢迎不吝赐教。


我是爱分享技术的大都督周瑜,欢迎关注我的公众号:Hoeller。公众号里有更多高质量干货系列文章和精品面试题。


记得点赞、分享哦!!!


作者:爱读源码的大都督
来源:juejin.cn/post/7294823722807017499
收起阅读 »

怎么办,代码发布完出问题了

作者:蒋静 前言 回滚是每个开发者必须熟悉的操作,它的重要性不言而喻,必要的时候我们可以通过回滚减少错误的代码对用户影响的时间。回滚的方式有很多种,方式有好也有坏,比如说使用 git 仓库回滚有可能会覆盖其他业务的代码,不稳定,构建产物的回滚最安全,便于优先...
继续阅读 »

作者:蒋静



前言


回滚是每个开发者必须熟悉的操作,它的重要性不言而喻,必要的时候我们可以通过回滚减少错误的代码对用户影响的时间。回滚的方式有很多种,方式有好也有坏,比如说使用 git 仓库回滚有可能会覆盖其他业务的代码,不稳定,构建产物的回滚最安全,便于优先解决线上问题。


构建部署之“痛”


我的几段公司的工作经历:



  1. 第一段经历,是在一个传统的公司,没有运维,要我们自己登录一个跳板机,把文件部署到服务器,非常麻烦。

  2. 第二段经历,是在一个初创公司,基建几乎没有,前端的规模也很小,发布就是打个 zip 包发给运维,运维去上线。但是久而久之,运维也就不耐烦了。

  3. 后来去了稍微大些的公司,构建、部署有一套比较完善的体系,在网页上点击按钮就可以了。


那么构建部署是如何实现的呢?下面我要来介绍古茗的部署和回滚代码机制。


发布分析


我们的最终目的是发布上线,我们发布的是什么呢?是一条分支,所以我们需要先创建一条分支(更加规范的步骤应该是:基于某个需求和某个应用去拉一条分支)。在分支上开发完我们就可以进行发布的操作啦!


这个时候我们就可以操作发布,我们填写需要的配置项后就可以点击发布按钮了。但是肯定不能让所有人随随便便就发布成功,所以我们要进行一些前置校验。比如说你有没有发布的权限、代码有没有冲突、是不是节假日或非发布窗口期、这个应用有没有正在被发布。。。等等的校验,总之就是确保代码是可以被你发布的。


然后我们的发布平台就会叫 Jenkins 拿着仓库信息、分支信息,以及其他等等的配置信息去仓库拉取代码了,拉到代码之后根据不同类型的应用进行区分,进行编译打包(这个过程不同应用之间是不同的),生成对应的产物。


1. 容器化发布


image.png



注:图中Wukong是我们自研DevOps平台



容器化发布发布的是镜像,镜像 id 代表了这次发布和这个镜像的关联关系。回滚的时候只需要找到这次发布对应的 id ,运维脚本根据这个 docker 的 id 找到 docker 镜像,直接部署这次 docker 镜像,做到回滚。由于发布的是 docker 的镜像,不仅可以保证产物是相同的,发布还很快。


容器化之前的发布:先找到对应的发布,根据这次发布找到对应的 tag,然后打包发布,但是这样只能保证业务代码是相同的,不能保证机器环境、打包机的环境、依赖的版本、打包的产物等等是一样的,并且需要的时间比容器化的方式慢得多。


2. oss发布


image.png


oss 发布和容器化发布流程的区别在于不用打包镜像而是将js、css等资源传到了 oss。通过 oss 发布的应用,只需要记住版本和 oss 上面资源路径的对应关系就可以了。


例如在我们这里的实现是:每次发布完成之后会记下有 hash 的 manifest 的地址,点击回滚后会根据发布 id 找到当次的产物,通过 oss 将 manifest 内容替换为有hash 的,从而就切换了访问的资源(html 的 manifest 地址不变,改变的是 manifest 文件的内容)。


3. 小程序


image.png
钉钉小程序的回滚就比较简单了,一般在我们点击回滚之后,内部会通过 http 接口调用小程序的 api 传递需要回滚的版本好后即回滚完成。或者你也可以选择手动到开发者后台的历史版本点回滚。
例如: open.dingtalk.com/document/or…


未来展望


有了完善的部署回滚机制,我们的产研团队才能有更好的交付体验。工作中的业务价值在我们整个交付内容占比应当是比较高的,而不应当把大量的时间花费在处理部署等流程上,让我们能够更快的去完成业务交付。


更好更稳定的回滚方式,能够让我们做到出现问题时快速恢复。这样才能保证一个较低的试错成本。


对于古茗来说,我认为一个很大的优势是,我们的规模不算很大,可以更好地做好研发流程对应的工具服务的统一,打通研发流程的各个流程,每个环节之间更好地进行串联,更好的助力业务发展。


作者:古茗前端团队
来源:juejin.cn/post/7295160228878106650
收起阅读 »

接手了一个外包开发的项目,我感觉我的头快要裂开了~

嗨,大家好,我是飘渺。 最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代码截图,相信很多同学一看就能认出来)。然而,...
继续阅读 »

嗨,大家好,我是飘渺。


最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代码截图,相信很多同学一看就能认出来)。然而,在这段时间里,我受到了来自"外包"和"微服务"这双重debuff的折磨。


image-20231016162237399


今天,我想和大家分享一下我在这几天中遇到的问题。希望这几个问题能引起大家的共鸣,以便在未来的微服务开发中避免再次陷入相似的困境。


1、服务模块拆分不合理


绝大部分网上的微服务开源框架都是基于后台管理进行模块拆分的。然而在实际业务开发中,应该以领域建模为基础来划分子服务。


目前的服务拆分方式往往是按照团队或功能来拆分,这种不合理的拆分方式导致了服务调用的混乱,同时增加了分布式事务的风险。


2、微服务拆分后数据库并没拆分


所有服务都共用同一个数据库,这在物理层面无法对数据进行隔离,也导致一些团队为了赶进度,直接读取其他服务的数据表。


这里不禁要问:如果不拆分数据库,那拆分微服务还有何意义?


3、功能复制,不是双倍快乐


在项目中存在一个基础设施模块,其中包括文件上传、数据字典、日志等基础功能。然而,文件上传功能居然在其他模块中重复实现了一遍。就像这样:


image-20231017185809403


4、到处都是无用组件堆彻


在项目的基础模块中,自定义了许多公共的Starter,并且这些组件在各个微服务中被全都引入。比如第三方登录组件、微信支付组件、不明所以的流程引擎组件、验证码组件等等……


image.png


拜托,我们已经有自己的SSO登录,不需要微信支付,还有自己的流程引擎。那些根本用不到的东西,干嘛要引入呢?


5、明显的错误没人解决


这个问题是由上面的问题所导致的,由于引入了一个根本不需要的消息中间件,项目运行时不断出现如下所示的连接异常。


image-20231013223714103


项目开发了这么久,出错了这么久,居然没有一个人去解决,真的让人不得不佩服他们的忍受力。


6、配置文件一团乱麻


你看到服务中这一堆配置文件,是不是心里咯噔了一下?


image-20231017190214587


或许有人会说:"没什么问题呀,按照不同环境划分不同的配置文件”。可是在微服务架构下,已经有了配置中心,为什么还要这么做呢?这不是画蛇添足吗?


7、乱用配置中心


项目一开始就明确要使用Apollo配置中心,一个微服务对应一个appid,appid一般与application.name一致。


但实际上,多个服务却使用了相同的appid,多个服务的配置文件还塞在了同一个appid下。


更让人费解的是,有些微服务又不使用配置中心。


8、Nacos注册中心混乱


由于项目有众多参与的团队,为了联调代码,开发人员在启动服务时不得不修改配置文件中Nacos的spring.cloud.nacos.discovery.group属性,同时需要启动所有相关服务。


这导致了两个问题:一是某个用户提交了自己的配置文件,导致其他人的服务注册到了别的group,影响他人的联调;二是Nacos注册中心会存在一大堆不同的Gr0up,查找服务变得相当麻烦。


其实要解决这个问题只需要重写一下网关的负载均衡策略,让流量调度到指定的服务即可。据我所知,他们使用的开源框架应该支持这个功能,只是他们不知道怎么使用。


9、接口协议混乱


使用的开源脚手架支持Dubbo协议和OpenFeign调用,然而在我们的项目中并不会使用Dubbo协议,微服务之间只使用OpenFeign进行调用。然而,在对外提供接口时,却暴露了一堆支持Dubbo协议的接口。


10、部署方式混乱


项目部署到Kubernetes云环境,一般来说,服务部署到云上的内部服务应该使用ClusterIP的方式进行部署,只有网关服务需要对外访问,网关可以通过NodePort或Ingress进行访问。


这样做可以避免其他人或服务绕过网关直接访问后端微服务。


然而,他们的部署方式是所有服务都开启了NodePort访问,然后在云主机上还要部署一套Nginx来反向代理网关服务的NodePort端口。


image-20231016162150035


结语


网络上涌现着众多微服务开源脚手架,它们吸引用户的方式是将各种功能一股脑地集成进去。然而,它们往往只是告诉你“如何集成”却忽略了“为什么要集成”。


尽管这些开源项目能够在学习微服务方面事半功倍,但在实际微服务项目中,我们不能盲目照搬,而应该根据项目的实际情况来有选择地裁剪或扩展功能。这样,我们才能更好地应对项目的需求,避免陷入不必要的复杂性,从而更加成功地实施微服务架构。


最后,这个开源项目你们认识吗?


image-20231017190633190



关注公众号 Java日知录 获取更多精彩文章



作者:飘渺Jam
来源:juejin.cn/post/7291480666087964732
收起阅读 »

手把手教你压测

前言 身为后端程序员怎么也要会一点压力测试相关的技术吧, 不然无脑上线项目万一项目火了进来大量请求时出现程序执行缓慢, 宕机等情况你肯定稳稳背锅, 而且这个时候短时间内还没办法解决, 只能使用物理扩容CPU, 内存, 更换网络等几种方式来解决问题, 妥妥的为公...
继续阅读 »

前言


身为后端程序员怎么也要会一点压力测试相关的技术吧, 不然无脑上线项目万一项目火了进来大量请求时出现程序执行缓慢, 宕机等情况你肯定稳稳背锅, 而且这个时候短时间内还没办法解决, 只能使用物理扩容CPU, 内存, 更换网络等几种方式来解决问题, 妥妥的为公司增加支出好吧, 下一个被开的就是你


都是想跑路拿高薪的打工仔, 身上怎么可以背负污点, 赶紧学一手压力测试进行保命, 我先学为敬


本篇文章主打一个学完就会, 奥利给



文中出现软件的版本



  • JMeter: 5.5

  • ServerAgent: 2.2.3



性能调优对各个开发岗位的区别


各个岗位对性能调优的关键节点



  • 前端工程师:

    • 首屏时间: 初次访问项目等待加载时间

    • 白屏时间: 刷新页面到数据全部展示时间

    • 可交互时间

    • 完全加载时间



  • 后端工程师

    • RT: 响应时间

    • TRS: 每秒事务数

    • 并发数: 这应该不会解释了吧



  • 移动端工程师

    • 端到端相应时间

    • Crash率

    • 内存使用率

    • FPS





主要讲一下后端工程师(Java), 毕竟这是吃饭的家伙

对于后端工程师来说, 影响性能的地方主要有两个



  • 数据库读写, RPC, 网络IO, 代码逻辑复杂度, 缓存

  • JVM(Throughput)
    - JVM(Throughput)



影响性能的关键要素



  • 产品设计

    • 产品逻辑

    • 功能交互

    • 动态效果

    • 页面元素



  • 基础网络

  • 代码质量&架构

    • 架构不合理

    • 研发功底和经验不足

    • 没有性能意识: 只实现功能不注重代码性能, 当业务上量后系统出现连锁反应, 导致性能问题增加

    • 数据库: 慢查询, 过多查询, 索引使用不当, 数据库服务器瓶颈



  • 用户移动端环境

    • 设备类型&性能

    • 系统版本

    • 网络(WiFi, 2G, 3G, 4G, 5G)

    • 硬件及云服务(服务器硬件, CPU, 内存..)




1. 初步了解压力测试


1.1压力测试是什么


压力测试是针对特定系统或组件, 为要确定其稳定性而特意进行的严格测试. 会让系统在超过正常使用条件下运作, 然后再确认其结果
对系统不断施加压力, 来预估系统`负载能力`的一种测试

一般而言, 只有在系统基础功能测试验证完成, 系统趋于稳定的情况下, 才会进行压力测试

1.2压力测试的目的


当负载主键增加时, 观察系统各项性能指标的变化情况是否有异常
发现系统的性能短板, 进行针对性的性能优化
判断系统在**高并发情况下是否会报错**, 进行是否会挂掉
测试在系统某个方面达到瓶颈时, 粗略估计系统性能上限

1.3 压力测试的指标


指标含义
相应时间(RT)是指系统对请求作出响应的平均时间, 对于单用户的系统, 响应时间可以很好地度量系统的性能
吞吐量(Throughput)是指系统在单位时间内处理的数量, 每秒事务数TPS 也算是吞吐量的一种
资源利用率CPU占用率, 内存使用率, 系统负载, 网络IO
并发用户数是指系统可以同时承载的正常使用系统功能的用户的数量, 用户不同的使用模式会导致不同用户在单位时间发出不同数量的请求
错误率失败请求占比, 在测试时添加响应断言, 验证不通过即标记为错误, 若不添加, 响应码非200则为错误


评判系统性能, 主要考虑三个性能指标 RT, TPS, 资源利用率



image.png


上图充分的展示了响应时间, 吞吐量, 利用率和并发用户数之间的关系


随着并发用户的增加经过轻负载区, 达到最优并发数, 此时利用率高,吞吐量高, 响应时间短


但是如果用户数继续增加, 就会到达重负载区, 此时性能最大化, 但是当超过某一临界值(最大并发数)之后, 响应时间会急剧增加, 利用率平缓, 吞吐量急速下降


我们进行压测的目的主要就是测试出这个临界值的大小, 或者说, 我们系统当前能承受住的最大并发数


2. 压力测试工具 JMeter


老规矩, 先来一波软件介绍
JMeter是 Apache组织开发的基于 Java的开源压力测试工具, 具有体积小, 功能全, 使用方便等特点. 最初被设计用于 Web应用测试, 后来被扩展到其他测试领域.


常用压测工具:



  • Apache JMeter可视化的测试工具

  • LoadRunner 预测系统行为和性能的负载测试工具

  • Apache的 ab压力测试

  • nGrinder韩国研发的一款性能测试工具

  • PAS阿里测试工具


压测目标:



  • 负载上升各项指标是否正常

  • 发现性能短板

  • 高并发下系统是否稳定

  • 预估系统最大负载


2.1 安装 JMeter



写在前面, 需要 Java8环境, 没有的话需要去安装, 教程百度上一大堆



官网地址: jmeter.apache.org/


image.png


熟悉的download, 点他


image.png


开始下载(是真的慢)


image.png


解压之后进入 bin目录下, 双击 jmeter.bat, 就可以启动 JMeter了


image.png


上图可以看出, 在我们第一次打开界面时是英文的, 多少有点不友好, 接下来讲解一下怎么将语言更改为中文


2.2 设置 JMeter界面为中文


还是我们的 bin目录下, 有一个 jmeter.properties文件


image.png


双击打开, 搜索 language


image.png


去除 #号, 值更改为 zh_CN, 保存文件然后重启软件(双击jmeter.bat)


image.png


可以看到, 我们的 jmeter成功更改为了中文界面, 这对于我这种英语白痴来说是很舒服的


image.png


2.3 初步使用 JMeter


我们先随便创建一个测试用例, 就是简单测试, 同时讲解一下常用的参数


本次测试采用 20线程, 1秒启动时间, 循环100次, Get请求


2.3.1 创建线程组


image.png


image.png



  • 线程数: 虚拟的用户数, 一个用户占一个线程

  • Ramp-Up: 等待时间, 设置的虚拟用户(线程数)需要多长时间全部启动

  • 循环次数: 单个线程发送请求的次数

  • 调度器:

    • 持续时间: 该任务执行的时间

    • 启动延迟: 等待多少秒开始执行




2.3.2 创建 http请求


右键线程组-添加HTTP请求


image.png


这个中文讲解的很明白, 应该都看得懂的, 有疑问的评论区留言


image.png


2.3.3 结果树



结果树, 聚合报告, 图形结果只有新增, 解释在测试



线程组右键-添加-监听器-查看结果树


image.png


image.png


执行结果分析(启动之后显示界面)


image.png


列表列出了每一次的HTTP请求, 绿色的是成功, 红色的话就是失败



  • 取样器结果参数详解

    • Thread Name:线程组名称

    • Sample Start: 启动开始时间

    • Load time:加载时长

    • Latency:等待时长

    • Size in bytes:发送的数据总大小

    • Headers size in bytes:发送数据的其余部分大小

    • Sample Count:发送统计

    • Error Count:交互错误统计

    • Response code:返回码

    • Response message:返回信息

    • Response headers:返回的头部信息



  • 请求

    • 基本数据

    • 入参

    • 请求头



  • 相应数据

    • 响应码

    • 响应头




2.3.4 聚合报告


线程组右键-添加-监听器-聚合报告


image.png


执行结果分析(启动之后界面)


image.png


参数解释



  • 样本: 并发量

  • 平均值: 接口请求用时(单位毫秒)

  • 中位数: 请求用时中位数(单位毫秒), 例如2000请求以请求时间排序, 排名1000的用时时长

  • 90%百分位, 95%百分位, 99%百分位和中位数同理

  • 最小, 最大值: 请求用时最小和最大

  • 异常% : 请求中异常的百分比

  • 吞吐量: 单位时间内请求次数


2.3.5 图形结果


线程组右键-添加-监听器-图形结果


image.png


执行结果分析(启动之后显示界面)


image.png



  • 样本数目:总共发送到服务器的请求数。

  • 最新样本:代表时间的数字,是服务器响应最后一个请求的时间。

  • 吞吐量:服务器每分钟处理的请求数。

  • 平均值:总运行时间除以发送到服务器的请求数。

  • 中间值:有一半的服务器响应时间低于该值而另一半高于该值。

  • 偏离:表示服务器响应时间变化、离散程度测量值的大小。


2.3.6 断言


断言主要用来判断结果返回是否符合预期


线程组右键-添加-断言-响应断言


image.png


image.png


假设我们接口的返回状态码字段为code, 200为成功, 那么就可以在断言这里进行配置, 来判断请求是否成功


image.png


3. JMeter插件


3.1 插件安装


首先说明 JMeter是不支持插件的, 所以我们先要 JMeter的插件允许插件下载, 这句话多少有点拗口


网址: Install :: JMeter-Plugins.org


image.png


点击上图红框即可下载插件, 前面说过了 JMeter是 Java8开发的, 插件对应的也是一个 jar包


image.png


下好之后就可以放在 JMeter安装目录下的 lib/ext/ 下了, 具体下载页面也有说明


image.png


上述操作结束之后, 在选项里面就可以看到插件中心Plugins Manager


image.png


弹出以下界面, 点击 Available Plugins搜索我们需要的插件Basic GraphsAdditional Graphs, 勾选上, 然后安装


image.png


Basic Graphs主要显示显示平均响应时间,活动线程数,成功/失败交易数等


image.png


Additional Graphs主要显示吞吐量,连接时间,每秒的点击数等


image.png


在安装成功之后, 在监听器会相应的多出很多的 jc开头的, 这就代表安装成功了



我使用的是 5.5版本的, 之前版本安装之后好像要手动重启, 5.5安装完会自动重启



image.png


4. Linux硬件监控


在压测过程中, 我们需要实时了解服务器的CPU, 内存, 网络, 服务器负载等情况的变化, 这个时候我们就需要对我们的 Linux系统进行监控, 通常来讲, 我们查询 Linux系统的资源占用情况可以使用以下几种方法



  • 使用命令: top, iostat, iotop等

  • 使用 Linux远程连接工具 FinalShell等

  • 宝塔

  • JMeter压测工具 PerfMon


在 JMeter中, 如果需要监控服务器硬件, 那么我们还需要安装 PerfMon插件


image.png
PerfMon监控服务器硬件,如CPU,内存,硬盘读写速度等


进入下述地址开始下载监控包: github.com/undera/perf…


image.png


下载好之后我们可以直接解压放到服务器上, 会看到有两个startAgent文件, 分别是Windows系统和Linux系统的启动脚本


image.png


我们直接启动就可以了, 如果脚本启动连接不上的话可以考虑更改脚本内容


例: Linux系统脚本更改为以下内容


## 默认启动运行 startAgent.sh 脚本即可

## 服务启动默认4444端口,根本连接不上,因此自己创建一个部署脚本文件对此进行部署,且把端口修改为7879

nohup java -jar ./CMDRunner.jar --tool PerfMonAgent --udp-port 7879 --tcp-port 7879 > log.log 2>&1 &

## 赋予可执行权限

chmod 755 startAgent.sh

启动成功之后, 脚本同级路径下会多出 log.log的日志文件


image.png


然后我们就可以配置 JMeter了, 线程组-监听器-jp@gc - PerfMon Metrics Collector


image.png


image.png


我是在本地启动了ServerAgent.bat进行测试, 执行结果如下所示:


image.png



注: 文件必须配置, 不然没有图像



具体的配置指标信息建议看官方文档, 太多了....
github.com/undera/perf…


image.png


image.png


ServerAgent闪退问题


Windows系统配置好ServerAgent启动之后窗口闪退可能是 jre版本问题, 可以从下面的链接下载老版的 jre


http://www.aliyundrive.com/s/Yzw3DZ74w…


下载好之后, 建议安装目录设置在ServerAgent/jre


image.png


并更改startAgent.bat脚本, cd 到老版本 jre路径


image.png


作者:宁轩
来源:juejin.cn/post/7248511603883638844
收起阅读 »

一不小心真删库了

事情经过 就是今天看到web前端的页面因为某个字段是Null,就想给Prisma的某个字段加上一个默认值,就像这样: 感觉也比较常规,当我改完这个字段后,我想的就是直接跑prisma migrate然后去迁移数据库,然后自动把null全部填值了。 md,然后...
继续阅读 »

事情经过


就是今天看到web前端的页面因为某个字段是Null,就想给Prisma的某个字段加上一个默认值,就像这样:


image.png


感觉也比较常规,当我改完这个字段后,我想的就是直接跑prisma migrate然后去迁移数据库,然后自动把null全部填值了。


md,然后因为我觉得这个操作好像再正常不过了,所以我就没看prisma在说啥,直接一路y到底了。


image.png


然后我一看prisma的客户端,茫然->刷几下->看看链接->茫然->再刷几下->啊?->无语凝噎->亚历山大、血压升高、心跳加速。md我的数据那,我的表里怎么一条数据没了,几万条啊?????


所以这个事情就告诉了我们,在做一些操作的时候不要觉得烦就一路y到底,认真看看命令行的反馈,那tm红红的一段 All data will be lost.......


问题追溯


在问题追溯前我先简单介绍下Prisma是啥,以及db操作和migrate操作(就是如果完全不了解数据库Orm的已经可以直接跳过了)。


Prisma


简单的说就是Prisma通过自己创建的特定语言DSL,把自己DSL结构映射为表结构,然后可以通过一系列cli去生成client或者迁移洗数据推拉数据库之类的操作,去帮助开发者在 Node.jsTypeScript 项目中更轻松地处理数据库操作.


Prisma db


db操作pullpushseedexecute,这一系列命令用于将 Prisma schema 中的定义同步到数据库 schema 中。db push 会检查当前数据库并根据您的 Prisma schema 对其进行更改。这是一种直接将 Prisma schema 变更应用到数据库的方法,但不会创建或使用迁移文件。在使用 db类操作 时,不会记录更改的历史(也就是不会生成提交记录,记住这一点很重要)、而我理解迁移历史的意义就是在于回滚、备份、恢复和版本控制,因此适用于原型设计和开发过程中的快速迭代


它的工作流分解一下可以分为以下几步:



  1. 自检Prisma 工具检查当前连接的数据库,并收集有关现有表、列、索引和其他数据库对象的详细信息。

  2. 分析Prisma 分析收集到的数据库信息,并尝试推断当前数据库结构与 Prisma schema 之间的差异。结合推断出的信息,Prisma 将生成相应的数据库操作(例如创建、修改或删除表、列、索引等),执行后,当前已连接的数据库结构和 本地Prisma schema 应该保持一致。

  3. 生成触发器:默认情况下,在将更改应用到数据库架构后,Prisma 会触发生成器(例如,Prisma Client)。不需要手动调用 prisma generate


Prisma migrate


它帮助我们根据 Prisma schema 中的更改自动创建、处理和执行迁移,同时保持数据库 schema 与迁移历史记录同步。migrate dev 通过影子数据库(shadow database)来检测 schema 偏移和冲突,并在需要时提示我们进行数据库重置。也就是说它可以确保在团队中正确地记录和同步数据库更改。


它的工作流分解一下可以分为以下几步:



  1. 重播现有迁移历史:在影子数据库(shadow database)中重播现有迁移历史,以便检测 schema 偏移(编辑或删除的迁移文件,或者对数据库 schema 的手动更改)。

  2. 应用待执行的迁移:将待执行的迁移应用到影子数据库(例如,由同事创建的新迁移)。

  3. 从 Prisma schema 更改中生成新迁移:在运行 migrate dev 命令之前,从我们对 Prisma schema 所做的任何更改中生成新迁移。

  4. 将所有未应用的迁移应用到开发数据库并更新 _prisma_migrations:将所有未应用的迁移应用到开发数据库,并更新 _prisma_migrations 表以记录已执行的迁移。

  5. 生成触发器和迁移历史


Why?


刚刚提到了做db操作是不会生成提交记录的,那到底发生了什么呐,我旁边的老大哥给我复盘了一下,首先A同事对数据库进行了db push操作(因为他正在设计一张表),这时候数据库schema其实已经偏移了但这时候,我并不知道因为没有任何的留存,然后接下来我在没有进行db pull的情况下直接去做了migrate操作,其实在这个时候prisma通过生成的影子数据库已经检测到了有偏移了,但我并没有注意,直接y到底,导致直接进行了reset操作清空了数据。


反思



  • 在团队协作项目中优先使用数据库迁移工具(如 migrate dev)。

  • 在执行关键数据库操作之前,确保同步所有的更改并了解同事所做的修改。

  • 当迁移工具检测到偏移和冲突时,确保花时间了解问题并采取适当措施。

  • 这其实就是缺乏适当沟通和不遵循较好实践带来的后果。在执行关键操作前了解同事的更改和确认是非常重要的。遇到迁移工具检测到的偏移和冲突时,应确保彻底了解问题并采取适当措施解决,而不是忽略警告。


作者:溪饱鱼
来源:juejin.cn/post/7293765765126324259
收起阅读 »

Shell 命令奇淫技巧,就是有点短

1、在任意目录之间快速移动 你发现自己要在两个或更多目录之间频繁移动,一会切换到这里,一会切换到那里,来回跳转。这些目录之间隔得还挺远,反复输入冗长的路径让人疲惫不堪。 使用内建命令 pushd 和 popd 来管理目录栈,轻松地在目录之间切换。下面是一个简单...
继续阅读 »

1、在任意目录之间快速移动


你发现自己要在两个或更多目录之间频繁移动,一会切换到这里,一会切换到那里,来回跳转。这些目录之间隔得还挺远,反复输入冗长的路径让人疲惫不堪。


使用内建命令 pushd 和 popd 来管理目录栈,轻松地在目录之间切换。下面是一个简单的示例:


$ cd /tmp/tank
$ pwd
/tmp/tank

$
pushd /var/log/cups
/var/log/cups /tmp/tank

$
pwd
/var/log/cups

$
ls
access_log error_log page_log

$
popd
/tmp/tank

$
ls
empty full

$
pushd /var/log/cups
/var/log/cups /tmp/tank

$
pushd
/tmp/tank /var/log/cups

$
pushd
/var/log/cups /tmp/tank

$
pushd
/tmp/tank /var/log/cups

$
dirs
/tmp/tank /var/log/cups

栈是一种后进先出的结构,这两个命令也正是这么做的。如果对一个新目录使用 pushd,那么它会将前一个目录压入栈中。当使用 popd时,它会弹出栈顶保存的当前位置,切换到新的栈顶目录。使用这些命令更改位置时,会从左到右输出目录栈中的值,对应于栈中自顶向下的顺序。


如果使用 pushd 时没有指定目录,那么它会交换栈顶的两个目录的位置,这样就可以重复使用 pushd 命令来实现两者之间的切换。cd命令也能够达到相同效果。


如果不记得目录栈中都有哪些目录,可以使用内建命令 dirs 按照从左到右的顺序显示。加上 -v 选项后,显示形式更形象。


$ dirs -v
0 /opt/yongheng
1 /opt/yongheng/Shell
$

数字可用来调整栈内目录的位置。pushd +1 会将编号为 1 的目录置为栈顶(并切换到该目录)并将其他目录下压。


$  pushd +1
/opt/yongheng/Shell /opt/yongheng

$
dirs -v
0 /opt/yongheng/Shell
1 /opt/yongheng

要想看到类似于栈的目录列表,但又不希望出现编号,可以使用 -p选项。


# dirs -p                                                                                              /opt/yongheng/Shell                                                                                    /opt/yongheng     

2、重复上一个命令


你刚刚输入了一个又长又麻烦的命令,其中包含了冗长的路径名和一堆复杂的参数。现在需要重新执行该命令。难道还得再输入一次?


这个问题有两种解决方法。第一种方法只需要在提示符下输入两个惊叹号,然后 bash 就会显示并重复执行上一个命令。例如:


$  cd /opt/                                                                                            $ !!                                                                                                   cd /opt/  

另一种(更现代的)方法是使用箭头键。按上箭头键会回滚到执行过的上一个命令。如果找到了需要的命令,按下 Enter 键就可以(再次)执行该命令。


3、参数重用


重用上一个命令很简单,使用 !! 就行了,但你需要的未必总是整个命令。如何只重用最后一个参数呢?


用 !$ 指明上一个命令中的最后一个参数。!:1 表示第一个参数,!:2 表示第二个参数,以此类推。


多个命令使用相同的文件名为参数是司空见惯的事情。最常见的场景之一就是程序员编辑源代码文件,然后编译、再编辑,再编译……有了 !$,事情就方便多了。如下:


$ vi /some/long/path/name/you/only/type/once
...
$ gcc !$
gcc /some/long/path/name/you/only/type/once
...
$ vi !$
vi /some/long/path/name/you/only/type/once
...
$ gcc !$
gcc /some/long/path/name/you/only/type/once
...
$

明白其中的意思了吗?这不仅省去了大量的键盘输入,还避免了错误。如果编译时输错文件名,那编译的可就不是刚刚编辑好的源代码文件了。有了 !$,就可以始终得到刚刚用过的文件名。要是想重用的参数位于命令行内部,可以使用带编号的 !: 命令来获取。


4、安全第一,命令测试


一不小心就会输错字符。 (不信你瞧!)即便是简单的 bash 命令,由此带来的后果也非常严重:你会移动错或删错文件。如果再加上模式匹配,结果更让人心跳,因为模式中的输入错误会导致南辕北辙的结果。小心谨慎的用户会怎么做?


可以使用命令历史特性和键盘便捷方式来重复参数,无须从头输入,因此能够减少输入错误。如果要用到棘手的模式来匹配文件,先用echo 测试一下模式能否正常匹配,然后再用 !$ 进行实际操作。例如:


$ ls
ab1.txt ac1.txt jb1.txt wc3.txt

$
echo *1.txt
ab1.txt ac1.txt jb1.txt

$
echo [aj]?1.txt
ab1.txt ac1.txt jb1.txt

$
echo ?b1.txt
ab1.txt jb1.txt

$
rm !$
rm ?b1.txt
$

echo 是检查模式匹配结果的一种方法。一旦确信结果符合预期,就可以将模式用于实际命令。这里我们要删除有特定名称的文件,没人愿意在这种事上犯错。


作者:博学谷_狂野架构师
来源:juejin.cn/post/7187268796753641531
收起阅读 »

压缩炸弹,Java怎么防止

一、什么是压缩炸弹,会有什么危害 1.1 什么是压缩炸弹 压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不...
继续阅读 »

一、什么是压缩炸弹,会有什么危害


1.1 什么是压缩炸弹


压缩炸弹(ZIP):一个压缩包只有几十KB,但是解压缩后有几十GB,甚至可以去到几百TB,直接撑爆硬盘,或者是在解压过程中CPU飙到100%造成服务器宕机。虽然系统功能没有自动解压,但是假如开发人员在不细心观察的情况下进行一键解压(不看压缩包里面的文件大小),可导致压缩炸弹爆炸。又或者压缩炸弹藏在比较深的目录下,不经意的解压缩,也可导致压缩炸弹爆炸。


以下是安全测试几种经典的压缩炸弹


graph LR
A(安全测试的经典压缩炸弹)
B(zip文件42KB)
C(zip文件10MB)
D(zip文件46MB)
E(解压后5.5G)
F(解压后281TB)
G(解压后4.5PB)

A ---> B --解压--> E
A ---> C --解压--> F
A ---> D --解压--> G

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px


压缩炸弹(也称为压缩文件炸弹、炸弹文件)是一种特殊的文件,它在解压缩时会迅速膨胀成极其庞大的文件,可能导致系统资源耗尽、崩溃或磁盘空间耗尽。


压缩炸弹的原理是利用文件压缩算法中的重复模式和递归压缩的特性。它通常是一个非常小的压缩文件,但在解压缩时会生成大量的重复数据,导致文件大小迅速增长。这种文件的设计意图是迫使系统进行大量的解压缩操作,以消耗系统资源或填满磁盘空间。


压缩炸弹可能对系统造成严重的影响,包括系统崩溃、资源耗尽、拒绝服务攻击等。因此,它被视为一种恶意的计算机攻击工具,常常被用于恶意目的或作为安全测试中的一种工具。



1.2 压缩炸弹会有什么危害


graph LR
A(压缩炸弹的危害)
B(资源耗尽)
C(磁盘空间耗尽)
D(系统崩溃)
E(拒绝服务攻击)
F(数据丢失)

A ---> B
A ---> C
A ---> D
A ---> E
A ---> F

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#00FFFF,stroke:#00FFFF,stroke-width:2px

压缩炸弹可能对计算机系统造成以下具体的破坏:




  1. 资源耗尽:压缩炸弹在解压缩时会生成大量的重复数据,导致系统的CPU、内存和磁盘资源被迅速占用。这可能导致系统反应迟缓、无法响应用户的请求,甚至系统崩溃。




  2. 磁盘空间耗尽:由于压缩炸弹的膨胀特性,它可能在解压缩过程中填满磁盘空间。这会导致系统无法写入新的数据,造成磁盘空间耗尽,影响系统的正常运行。




  3. 系统崩溃:当一个压缩炸弹被解压缩时,系统可能由于资源耗尽或磁盘空间耗尽而崩溃。这可能导致系统重启或需要进行紧急修复,造成数据丢失或系统不可用的情况。




  4. 拒绝服务攻击:大规模的解压缩操作可能消耗大量系统资源,导致系统无法正常提供服务。这被恶意攻击者利用,用于进行拒绝服务攻击,使目标系统无法响应合法用户的请求。




  5. 数据丢失:在某些情况下,压缩炸弹可能会导致系统文件或数据的损坏或丢失。这可能发生在磁盘空间被耗尽时,写入操作无法成功完成的情况下。





重要提示:压缩炸弹可能对计算机系统造成不可逆的损害,请不要尝试创建、传播或使用压缩炸弹,以保护计算机和网络的安全。



二、怎么检测和处理压缩炸弹,Java怎么防止压缩炸弹


2.1 个人有没有方法可以检测压缩炸弹?


有一些方法可以识别和处理潜在的压缩炸弹,以防止对系统造成破坏。以下是一些常见的方法:


graph LR
A(个人检测压缩炸弹)
B(安全软件和防病毒工具)
C(文件大小限制)
D(文件类型过滤)

A ---> B --> E(推荐)
A ---> C --> F(太大的放个心眼)
A ---> D --> G(注意不认识的文件类型)

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style F fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style G fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px



  1. 安全软件和防病毒工具(推荐):使用最新的安全软件和防病毒工具可以帮助检测和阻止已知的压缩炸弹。这些工具通常具备压缩文件扫描功能,可以检查文件是否包含恶意的压缩炸弹。




  2. 文件大小限制:设置对文件大小的限制可以帮助防止解压缩过程中出现过大的文件。通过限制解压缩操作的最大文件大小,可以减少对系统资源和磁盘空间的过度消耗。




  3. 文件类型过滤:识别和过滤已知的压缩炸弹文件类型可以帮助阻止这些文件的传输和存储。通过检查文件扩展名或文件头信息,可以识别潜在的压缩炸弹,并阻止其传输或处理。




2.2 Java怎么防止压缩炸弹


在java中实际防止压缩炸弹的方法挺多的,可以采取以下措施来防止压缩炸弹:


graph LR
A(Java防止压缩炸弹)
B(解压缩算法的限制)
C(设置解压缩操作的资源限制)
D(使用安全的解压缩库)
E(文件类型验证和过滤)
F(异步解压缩操作)
G(安全策略和权限控制)

A ---> B
A ---> C
A ---> D
A ---> E
A ---> F
A ---> G

style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px



  1. 解压缩算法的限制:限制解压缩算法的递归层数和重复模式的检测可以帮助防止解压缩过程无限膨胀。通过限制递归的深度和检测重复模式,可以及时中断解压缩操作并避免过度消耗资源。




  2. 设置解压缩操作的资源限制:使用Java的java.util.zipjava.util.jar等类进行解压缩时,可以设置解压缩操作的资源限制,例如限制解压缩的最大文件大小、最大递归深度等。通过限制资源的使用,可以减少对系统资源的过度消耗。




  3. 使用安全的解压缩库:确保使用的解压缩库是经过安全验证的,以避免存在已知的压缩炸弹漏洞。使用官方或经过广泛验证的库可以减少受到压缩炸弹攻击的风险。




  4. 文件类型验证和过滤:在解压缩之前,可以对文件进行类型验证和过滤,排除潜在的压缩炸弹文件。通过验证文件的类型、扩展名和文件头信息,可以识别并排除不安全的压缩文件。




  5. 异步解压缩操作:将解压缩操作放在异步线程中进行,以防止阻塞主线程和耗尽系统资源。这样可以保持应用程序的响应性,并减少对系统的影响。




  6. 安全策略和权限控制:实施严格的安全策略和权限控制,限制用户对系统资源和文件的访问和操作。确保只有受信任的用户或应用程序能够进行解压缩操作,以减少恶意使用压缩炸弹的可能性。




2.2.1 使用解压算法的限制来实现防止压缩炸弹


在前面我们说了Java防止压缩炸弹的一些策略,下面我将代码实现通过解压缩算法的限制来实现防止压缩炸弹。


先来看看我们实现的思路


graph TD
A(开始) --> B[创建 ZipFile 对象]
B --> C[打开要解压缩的 ZIP 文件]
C --> D[初始化 zipFileSize 变量为 0]
D --> E{是否有更多的条目}
E -- 是 --> F[获取 ZIP 文件的下一个条目]
F --> G[获取当前条目的未压缩大小]
G --> H[将解压大小累加到 zipFileSize 变量]
H --> I{zipFileSize 是否超过指定的大小}
I -- 是 --> J[调用 deleteDir方法删除已解压的文件夹]
J --> K[抛出 IllegalArgumentException 异常]
K --> L(结束)
I -- 否 --> M(保存解压文件) --> E
E -- 否 --> L

style A fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
style F fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style G fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style H fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style I fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
style J fill:#00FFFF,stroke:#00FFFF,stroke-width:2px
style K fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
style L fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
style M fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px

实现流程说明如下:




  1. 首先,通过给定的 file 参数创建一个 ZipFile 对象,用于打开要解压缩的 ZIP 文件。




  2. zipFileSize 变量用于计算解压缩后的文件总大小。




  3. 使用 zipFile.entries() 方法获取 ZIP 文件中的所有条目,并通过 while 循环逐个处理每个条目。




  4. 对于每个条目,使用 entry.getSize() 获取条目的未压缩大小,并将其累加到 zipFileSize 变量中。




  5. 如果 zipFileSize 超过了给定的 size 参数,说明解压后的文件大小超过了限制,此时会调用 deleteDir() 方法删除已解压的文件夹,并抛出 IllegalArgumentException 异常,以防止压缩炸弹攻击。




  6. 创建一个 File 对象 unzipped,表示解压后的文件或目录在输出文件夹中的路径。




  7. 如果当前条目是一个目录,且 unzipped 不存在,则创建该目录。




  8. 如果当前条目不是一个目录,确保 unzipped 的父文件夹存在。




  9. 创建一个 FileOutputStream 对象 fos,用于将解压后的数据写入到 unzipped 文件中。




  10. 通过 zipFile.getInputStream(entry) 获取当前条目的输入流。




  11. 创建一个缓冲区 buffer,并使用循环从输入流中读取数据,并将其写入到 fos 中,直到读取完整个条目的数据。




  12. 最后,在 finally 块中关闭 fos 和 zipFile 对象,确保资源的释放。




实现代码工具类


import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
* 文件炸弹工具类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/

public class FileBombUtil {

/**
* 限制文件大小 1M(限制单位:B)[1M=1024KB 1KB=1024B]
*/

public static final Long FILE_LIMIT_SIZE = 1024 * 1024 * 1L;

/**
* 文件超限提示
*/

public static final String FILE_LIMIT_SIZE_MSG = "The file size exceeds the limit";

/**
* 解压文件(带限制解压文件大小策略)
*
* @param file 压缩文件
* @param outputfolder 解压后的文件目录
* @param size 限制解压之后的文件大小(单位:B),示例 3M:1024 * 1024 * 3L (FileBombUtil.FILE_LIMIT_SIZE * 3)
* @throws Exception IllegalArgumentException 超限抛出的异常
* 注意:业务层必须抓取IllegalArgumentException异常,如果msg等于FILE_LIMIT_SIZE_MSG
* 要考虑后面的逻辑,比如告警
*/

public static void unzip(File file, File outputfolder, Long size) throws Exception {
ZipFile zipFile = new ZipFile(file);
FileOutputStream fos = null;
try {
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
long zipFileSize = 0L;
ZipEntry entry;
while (zipEntries.hasMoreElements()) {
// 获取 ZIP 文件的下一个条目
entry = zipEntries.nextElement();
// 将解缩大小累加到 zipFileSize 变量
zipFileSize += entry.getSize();
// 判断解压文件累计大小是否超过指定的大小
if (zipFileSize > size) {
deleteDir(outputfolder);
throw new IllegalArgumentException(FILE_LIMIT_SIZE_MSG);
}
File unzipped = new File(outputfolder, entry.getName());
if (entry.isDirectory() && !unzipped.exists()) {
unzipped.mkdirs();
continue;
} else if (!unzipped.getParentFile().exists()) {
unzipped.getParentFile().mkdirs();
}

fos = new FileOutputStream(unzipped);
InputStream in = zipFile.getInputStream(entry);

byte[] buffer = new byte[4096];
int count;
while ((count = in.read(buffer, 0, buffer.length)) != -1) {
fos.write(buffer, 0, count);
}
}
} finally {
if (null != fos) {
fos.close();
}
if (null != zipFile) {
zipFile.close();
}
}

}

/**
* 递归删除目录文件
*
* @param dir 目录
*/

private static boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
//递归删除目录中的子目录下
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
// 目录此时为空,可以删除
return dir.delete();
}

}

测试类


import java.io.File;

/**
* 文件炸弹测试类
*
* @author bamboo panda
* @version 1.0
* @date 2023/10
*/

public class Test {

public static void main(String[] args) {
File bomb = new File("D:\temp\3\zbsm.zip");
File tempFile = new File("D:\temp\3\4");
try {
FileBombUtil.unzip(bomb, tempFile, FileBombUtil.FILE_LIMIT_SIZE * 60);
} catch (IllegalArgumentException e) {
if (FileBombUtil.FILE_LIMIT_SIZE_MSG.equalsIgnoreCase(e.getMessage())) {
FileBombUtil.deleteDir(tempFile);
System.out.println("原始文件太大");
} else {
System.out.println("错误的压缩文件格式");
}
} catch (Exception e) {
e.printStackTrace();
}
}

}

三、总结


文件炸弹是一种恶意的计算机程序或文件,旨在利用压缩算法和递归结构来创建一个巨大且无限增长的文件或文件集
合。它的目的是消耗目标系统的资源,如磁盘空间、内存和处理能力,导致系统崩溃或无法正常运行。文件炸弹可能是有意制造的攻击工具,用于拒绝服务(DoS)攻击或滥用资源的目的。


文件炸弹带来的危害极大,作为开发人员,我们必须深刻认识到文件炸弹的危害性,并始终保持高度警惕,以防止这种潜在漏洞给恐怖分子以可乘之机。


总而言之,我们作为开发人员,要深刻认识到文件炸弹的危害性,严防死守,不给恐怖分子任何可乘之机。通过使用安全工具、限制文件大小、及时更新软件、定期备份数据以及加强安全意识,我们可以有效地防止文件炸弹和其他恶意活动对系统造成损害。


在中国,网络安全和计算机犯罪问题受到相关法律法规的管理和监管。以下是一些中国关于网络安全和计算机犯罪方面的法律文献,其中也涉及到文件炸弹的相关规定:





  1. 《中华人民共和国刑法》- 该法律规定了各种计算机犯罪行为的法律责任,包括非法控制计算机信息系统、破坏计算机信息系统功能、非法获取计算机信息系统数据等行为,这些行为可能涉及到使用文件炸弹进行攻击。




  2. 《中华人民共和国网络安全法》- 该法律是中国的基本法律,旨在保障网络安全和维护国家网络空间主权。它规定了网络安全的基本要求和责任,包括禁止制作、传播软件病毒、恶意程序和文件炸弹等危害网络安全的行为。




  3. 《中华人民共和国计算机信息系统安全保护条例》- 这是一项行政法规,详细规定了计算机信息系统安全的保护措施和管理要求。其中包含了对恶意程序、计算机病毒和文件炸弹等威胁的防范要求。





作者:独爱竹子的功夫熊猫
来源:juejin.cn/post/7289667869557178404
收起阅读 »

学弟说他面试时被问到了HashMap,差点就遭老罪了

面试官:小伙子,了解HashMap吗? 学弟:哎呦,你干嘛~ 真的问这个呀.... 面试官:呦,练习时长两年半?待会答不上来,你可就遭老罪喽! 那行吧,那开始吧...唱跳rap篮球🏀...... 一、HashMap的底层结构 说一下你理解的HashM...
继续阅读 »

面试官:小伙子,了解HashMap吗?


学弟:哎呦,你干嘛~ 真的问这个呀....


面试官:呦,练习时长两年半?待会答不上来,你可就遭老罪喽!



在这里插入图片描述



那行吧,那开始吧...唱跳rap篮球🏀......



一、HashMap的底层结构



说一下你理解的HashMap底层?



hashMap是由数值和链表组合而成的数据结构,存储为key Value形式。


在java7中叫entry,数据形式为数组+链表。java8中叫Node,数据形式为数组+链表+红黑树(当链表长度大于8时转为红黑树)。


每一个节点都会保存自身的hash、key、value、以及next属性指向下一个节点。


在这里插入图片描述


二、为什么使用数组+链表数据结构



你刚提到了使用数组+链表,可以讲讲为什么使用这个结构吗?



HashMap内部使用数组来存储键值对,这个数组就是 HashMap 的主体。


在这里插入图片描述


在数组中存储的每个位置上,可能会有多个键值对,这些键值对通过链表的形式链接在一起。


在这里插入图片描述


使用数组+链表的数据结构是为了解决散列表中的键冲突问题。在散列表中,每个键都会被映射到一个桶中,但是不同的键可能会被映射到同一个桶中,这种情况被称为键冲突。


为了解决键冲突问题,HashMap 采用了链表的形式将所有映射到同一个桶中的键值对链接在一起,这样就可以通过遍历链表来查找指定键的值当链表长度过长时,查找效率就会下降,因此在链表长度超过一定阈值(8)后,HashMap会将链表转换为红黑树,以提高查找效率


同时,数组的优势在于支持通过下标快速访问元素,因此HashMap可以将每个桶的位置映射到数组的一个元素上,通过下标访问该元素即可访问到对应的链表或红黑树


我们都知道:数组的查询效率很高,添加和删除的效率低。链表的查询效率很低,添加和删除的效率高。


因此:使用数组加链表形式,不仅可以解决散列表中的键冲突问题,且数组的查询效率高、链表的添加和删除效率高。结合在一起,增删查效率都很高


请添加图片描述



嗯,确实不错。不愧是练习时长两年半的程序员.....



三、数组+链表+红黑树



你刚说数组+链表+红黑树,什么情况下会转化红黑树?什么情况下转数组呢?



链表中元素过多,会影响查找效率,当其个数达到8的时候转换为红黑树。红黑树是平衡二叉树,在查找性能方面比链表要高


当红黑树的节点数小于等于6时,红黑树转换为链表,是为了减少内存开销


需要注意的是:将链表转换为红黑树、红黑树转换为链表的操作会影响HashMap的性能,因此需要尽可能避免这种情况的发生。同时,当HashMap中的元素数量较小时,不会出现链表转换为红黑树的情况,因此使用HashMap时,可以考虑在元素数量较少的情况下使用HashMap,以提高性能。


在这里插入图片描述


四、头插法和尾插法



说一下什么是头插法,什么是尾插法?



哇,这不是为难我胖虎吗?啥是头插法?啥是尾插法?


在这里插入图片描述


4.1、头插法


顾名思义,头插法就是新增元素时,放在最前面嘛。


举个栗子🌰,楼主画了一个简单的框框。用来表示原有存储顺序依次为1、2、3的数组。
在这里插入图片描述


假设现在加入了一个4,如果使用头插法,就会变为4123。


在这里插入图片描述


4.2、尾插法


同样道理,尾插法就是新增元素时,放在最后面。


还是原有存储顺序依次为1、2、3的数组。
在这里插入图片描述
假设现在加入了一个4,如果使用尾插法,就会变为1234。


在这里插入图片描述



头插法为什么要调整为尾插法呢?



为什么头插法要调整为尾插法?这是个好问题!!!
请添加图片描述


java7中使用头插法,新来的值会取代原有的值,原有的值就顺推到链表中。在这种情况下,引用关系可能会乱掉,严重会造成死循环。java8使用尾插法,把元素放到最后,就不会出现这种情况。


五、HashMap如何运算存储索引



向一个hashMap中存入数据时,如何知道数据放在哪个位置呢?



当向一个hashMap中存入数据时,会先根据key的哈希值决定放在数组中哪个索引位置。



Hash公式:index = HashCode(Key) & (Length - 1)



如果数组中该索引位置是空的,直接将元素放入,如果该索引位置已经存在元素了,就根据equals方法判断下已有的元素是否和我们新放入的元素是同一个,如果返回true是同一个,则覆盖掉。不是同一元素则在原有元素下面使用链表进行存储


每个元素都有一个next属性指向下一个节点(数组+链表)


    /**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
.........
}

六、HashMap初始化、扩容



嗯,你知道HashMap默认初始化大小是多少吗?还有它的扩容?



HashMap默认初始化容量大小是16,最大容量为2的30次方,负载因子是0.75


在这里插入图片描述


扩容时,会把原有数组中的值取出再次hash到新的数组中(长度扩大以后,Hash的规则也随之改变),因此性能消耗也相对较大。


当HashMap中的元素数量超过负载因子(默认为 0.75)乘以数组长度时,就会触发扩容操作,将数组长度增加一倍,并重新计算每个元素在新数组中的位置。


七、hash碰撞是什么



你听说过hash碰撞吗?



hash碰撞就是不同的Key,经过同一散列算法之后得到的hashCode值相同。


hashCode不同,key一定不同。hashCode相同,key却不一定相同。


当两个key的hashCode()返回值不同时,它们对应哈希表索引也一定不同。不同的key对象,即使它们包含相同的属性、值或状态,它们的hashCode()返回值也是不相同的。


在这里插入图片描述


当两个key的hashCode()返回值相同时,它们可能对应同一个哈希表索引,但它们并不一定相等。在哈希表中,不同的key可能会产生相同的哈希值(哈希碰撞)。


因此,当 key的hashCode相同时,还需要比较key的相等性。需要调用key的equals() 方法来判断它们是否相等。只有当hashCode相等,且equals方法返回true时。才可以认为这两个key相等


八、如何解决hash碰撞



解决hash碰撞的方法有哪些呢?



在哈希表中,哈希碰撞可能会导致性能下降或者安全问题。


常见的解决方法有:


1、开放地址法:在发生哈希碰撞时,通过一定的算法在哈希表中寻找一个空闲的位置,并将元素插入该位置。


2、链式哈希表:在每个哈希表的元素位置上,存储一个链表,哈希碰撞时,将元素插入到相应的链表中。


3、再哈希法:如果一个哈希函数产生的哈希值发生了碰撞,就再次使用另一个哈希函数计算哈希值。


4、负载因子调整:通过调整哈希表的容量、负载因子等参数,可以减少哈希碰撞的发生。


九、HashMap为什么线程不安全



HashMap线程安全吗?为什么?



HashMap是非线程安全的。在多线程环境下,如果多个线程同时修改HashMap中的数据,就可能会导致数据的不一致性。


说白了就是没加锁。


在这里插入图片描述


当多个线程同时调用HashMap的put()方法,一旦他们计算出的hash值相同,就会发生冲突,导致数据被覆盖。


所以,对于多线程并发访问的情况,建议使用线程安全的Map实现


例如ConcurrentHashMap,或者使用Collections.synchronizedMap()方法将HashMap包装成一个线程安全的Map


十、HashMap、HashTable、ConcurrentHashMap的区别



最后一个问题:说一下HashMap、HashTable、ConcurrentHashMap的区别?



麻了! 真的麻了....救救孩子吧....


在这里插入图片描述


HashMap、HashTable、ConcurrentHashMap都是Java中常用的哈希表实现。


区别主要在以下几个方面:


1、线程安全性:HashTable是线程安全的,HashMap是非线程安全的,ConcurrentHashMap通过分段锁的方式保证了线程安全。


2、是否可为空:HashTable不允许value为空,ConcurrentHashMap不允许null值作为key或value,而HashMap则允许null作为key或value。


3、迭代器:HashTable的迭代器是通过Enumeration实现的,而HashMap和ConcurrentHashMap使用的是Iterator实现的。


4、扩容:HashTable在扩容时,将容量扩大一倍加一,而HashMap和ConcurrentHashMap的扩容机制是将容量扩大一倍。


5、初始容量:HashTable的初始容量为11,而HashMap和ConcurrentHashMap的初始容量为16。


6、性能:HashMap通常比HashTable性能更好,因为它没加锁。所以弊端就是线程不安全。但后者加了锁,是线程安全的,缺点就是消耗性能。ConcurrentHashMap在多线程并发访问时,比HashTable和HashMap性能更好,因为它使用了分段锁来保证线程安全


所以,不建议使用HashTable。至于选择HashMap还是ConcurrentHashMap取决于并发访问量的大小,若并发访问量不高,则选用HashMap。若并发访问量较大,则选用ConcurrentHashMap。



ok,那今天先到这里吧。练习时长两年半的程序员.....唱跳rap篮球🏀....差点就遭老罪喽~



还有,别忘记给那个练习时长两年半的三婶儿也点个赞哈~她唱跳rap篮球也还行......


在这里插入图片描述


作者:三婶儿
来源:juejin.cn/post/7209826725365137465
收起阅读 »

为啥一个 main 方法就能启动项目

在 Spring Boot 出现之前,我们要运行一个 Java Web 应用,首先需要有一个 Web 容器(例如 Tomcat 或 Jetty),然后将我们的 Web 应用打包后放到容器的相应目录下,最后再启动容器。 在 IDE 中也需要对 Web 容器进行一...
继续阅读 »

在 Spring Boot 出现之前,我们要运行一个 Java Web 应用,首先需要有一个 Web 容器(例如 Tomcat 或 Jetty),然后将我们的 Web 应用打包后放到容器的相应目录下,最后再启动容器。


在 IDE 中也需要对 Web 容器进行一些配置,才能够运行或者 Debug。而使用 Spring Boot 我们只需要像运行普通 JavaSE 程序一样,run 一下 main() 方法就可以启动一个 Web 应用了。这是怎么做到的呢?今天我们就一探究竟,分析一下 Spring Boot 的启动流程。


概览


回看我们写的第一个 Spring Boot 示例,我们发现,只需要下面几行代码我们就可以跑起一个 Web 服务器:


@SpringBootApplication
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}


去掉类的声明和方法定义这些样板代码,核心代码就只有一个 @SpringBootApplication 注解和 SpringApplication.run(HelloApplication.class, args) 了。而我们知道注解相当于是一种配置,那么这个 run() 方法必然就是 Spring Boot 的启动入口了。


接下来,我们沿着 run() 方法来顺藤摸瓜。进入 SpringApplication 类,来看看 run() 方法的具体实现:


public class SpringApplication {
......
public ConfigurableApplicationContext run(String... args) {
// 1 应用启动计时开始
StopWatch stopWatch = new StopWatch();
stopWatch.start();

// 2 声明上下文
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;

// 3 设置 java.awt.headless 属性
configureHeadlessProperty();

// 4 启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 5 初始化默认应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

// 6 准备应用环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);

// 7 打印 Banner(Spring Boot 的 LOGO)
Banner printedBanner = printBanner(environment);

// 8 创建上下文实例
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);

// 9 构建上下文
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

// 10 刷新上下文
refreshContext(context);

// 11 刷新上下文后处理
afterRefresh(context, applicationArguments);

// 12 应用启动计时结束
stopWatch.stop();
if (this.logStartupInfo) {
// 13 打印启动时间日志
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}

// 14 发布上下文启动完成事件
listeners.started(context);

// 15 调用 runners
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
// 16 应用启动发生异常后的处理
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}

try {
// 17 发布上下文就绪事件
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
......
}


Spring Boot 启动时做的所有操作都这这个方法里面,当然在调用上面这个 run() 方法之前,还创建了一个 SpringApplication 的实例对象。因为上面这个 run() 方法并不是一个静态方法,所以需要一个对象实例才能被调用。


可以看到,方法的返回值类型为 ConfigurableApplicationContext,这是一个接口,我们真正得到的是 AnnotationConfigServletWebServerApplicationContext 的实例。通过类名我们可以知道,这是一个基于注解的 Servlet Web 应用上下文(我们知道上下文(context)是 Spring 中的核心概念)。


上面对于 run() 方法中的每一个步骤都做了简单的注释,接下来我们选择几个比较有代表性的来详细分析。


应用启动计时


在 Spring Boot 应用启动完成时,我们经常会看到类似下面内容的一条日志:


Started AopApplication in 2.732 seconds (JVM running for 3.734)

应用启动后,会将本次启动所花费的时间打印出来,让我们对于启动的速度有一个大致的了解,也方便我们对其进行优化。记录启动时间的工作是 run() 方法做的第一件事,在编号 1 的位置由 stopWatch.start() 开启时间统计,具体代码如下:


public void start(String taskName) throws IllegalStateException {
if (this.currentTaskName != null) {
throw new IllegalStateException("Can't start StopWatch: it's already running");
}
// 记录启动时间
this.currentTaskName = taskName;
this.startTimeNanos = System.nanoTime();
}


然后到了 run() 方法的基本任务完成的时候,由 stopWatch.stop()(编号 12 的位置)对启动时间做了一个计算,源码也很简单:


public void stop() throws IllegalStateException {
if (this.currentTaskName == null) {
throw new IllegalStateException("Can't stop StopWatch: it's not running");
}
// 计算启动时间
long lastTime = System.nanoTime() - this.startTimeNanos;
this.totalTimeNanos += lastTime;
......
}


最后,在 run() 中的编号 13 的位置将启动时间打印出来:


if (this.logStartupInfo) {
// 打印启动时间
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}


打印 Banner


Spring Boot 每次启动是还会打印一个自己的 LOGO,如图:


在这里插入图片描述


这种做法很常见,像 Redis、Docker 等都会在启动的时候将自己的 LOGO 打印出来。Spring Boot 默认情况下会打印那个标志性的“树叶”和 “Spring” 的字样,下面带着当前的版本。


在 run() 中编号 7 的位置调用打印 Banner 的逻辑,最终由 SpringBootBanner 类的 printBanner() 完成。这个图案定义在一个常量数组中,代码如下:


class SpringBootBanner implements Banner {

private static final String[] BANNER = {
"",
" . ____ _ __ _ _",
" /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\",
"( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\",
" \\\\/ ___)| |_)| | | | | || (_| | ) ) ) )",
" ' |____| .__|_| |_|_| |_\\__, | / / / /",
" =========|_|==============|___/=/_/_/_/"
};
......

public void printBanner(Environment environment, Class sourceClass, PrintStream printStream) {
for (String line : BANNER) {
printStream.println(line);
}
......
}

}


手工格式化了一下 BANNER 的字符串,轮廓已经清晰可见了。真正打印的逻辑就是 printBanner() 方法里面的那个 for 循环。


记录启动时间和打印 Banner 代码都非常的简单,而且都有很明显的视觉反馈,可以清晰的看到结果。拿出来咱们做个热身,配合断点去 Debug 会有更加直观的感受,尤其是打印 Banner 的时候,可以看到整个内容被一行一行打印出来,让我想起了早些年用那些配置极低的电脑(还是 CRT 显示器)运行着 Win98,经常会看到屏幕内容一行一行加载显示。


创建上下文实例


下面我们来到 run() 方法中编号 8 的位置,这里调用了一个 createApplicationContext() 方法,该方法最终会调用 ApplicationContextFactory 接口的代码:


ApplicationContextFactory DEFAULT = (webApplicationType) -> {
try {
switch (webApplicationType) {
case SERVLET:
return new AnnotationConfigServletWebServerApplicationContext();
case REACTIVE:
return new AnnotationConfigReactiveWebServerApplicationContext();
default:
return new AnnotationConfigApplicationContext();
}
}
catch (Exception ex) {
throw new IllegalStateException("Unable create a default ApplicationContext instance, "
+ "you may need a custom ApplicationContextFactory", ex);
}
};


这个方法就是根据 SpringBootApplication 的 webApplicationType 属性的值,利用反射来创建不同类型的应用上下文(context)。而属性 webApplicationType 的值是在前面执行构造方法的时候由 WebApplicationType.deduceFromClasspath() 获得的。通过方法名很容易看出来,就是根据 classpath 中的类来推断当前的应用类型。


我们这里是一个普通的 Web 应用,所以最终返回的类型为 SERVLET。所以会返回一个 AnnotationConfigServletWebServerApplicationContext 实例。


构建容器上下文


接着我们来到 run() 方法编号 9 的 prepareContext() 方法。通过方法名,我们也能猜到它是为 context 做上台前的准备工作的。


private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner)
{
......
// 加载资源
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}


在这个方法中,会做一些准备工作,包括初始化容器上下文、设置环境、加载资源等。


加载资源


上面的代码中,又调用了一个很关键的方法——load()。这个 load() 方法真正的作用是去调用 BeanDefinitionLoader 类的 load() 方法。源码如下:


class BeanDefinitionLoader {
......
void load() {
for (Object source : this.sources) {
load(source);
}
}

private void load(Object source) {
Assert.notNull(source, "Source must not be null");
if (source instanceof Class) {
load((Class) source);
return;
}
if (source instanceof Resource) {
load((Resource) source);
return;
}
if (source instanceof Package) {
load((Package) source);
return;
}
if (source instanceof CharSequence) {
load((CharSequence) source);
return;
}
throw new IllegalArgumentException("Invalid source type " + source.getClass());
}
......
}


可以看到,load() 方法在加载 Spring 中各种资源。其中我们最熟悉的就是 load((Class) source) 和 load((Package) source) 了。一个用来加载类,一个用来加载扫描的包。


load((Class) source) 中会通过调用 isComponent() 方法来判断资源是否为 Spring 容器管理的组件。 isComponent() 方法通过资源是否包含 @Component 注解(@Controller、@Service、@Repository 等都包含在内)来区分是否为 Spring 容器管理的组件。


而 load((Package) source) 方法则是用来加载 @ComponentScan 注解定义的包路径。


刷新上下文


run() 方法编号10 的 refreshContext() 方法是整个启动过程比较核心的地方。像我们熟悉的 BeanFactory 就是在这个阶段构建的,所有非懒加载的 Spring Bean(@Controller、@Service 等)也是在这个阶段被创建的,还有 Spring Boot 内嵌的 Web 容器要是在这个时候启动的。


跟踪源码你会发现内部调用的是 ConfigurableApplicationContext.refresh(),ConfigurableApplicationContext 是一个接口,真正实现这个方法的有三个类:AbstractApplicationContext、ReactiveWebServerApplicationContext 和 ServletWebServerApplicationContext。


AbstractApplicationContext 为后面两个的父类,两个子类的实现比较简单,主要是调用父类实现,比如 ServletWebServerApplicationContext 中的实现是这样的:


public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
}
catch (RuntimeException ex) {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.stop();
}
throw ex;
}
}


主要的逻辑都在 AbstractApplicationContext 中:


@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

// 1 准备将要刷新的上下文
prepareRefresh();

// 2 (告诉子类,如:ServletWebServerApplicationContext)刷新内部 bean 工厂
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// 3 为上下文准备 bean 工厂
prepareBeanFactory(beanFactory);

try {
// 4 允许在子类中对 bean 工厂进行后处理
postProcessBeanFactory(beanFactory);

StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// 5 调用注册为 bean 的工厂处理器
invokeBeanFactoryPostProcessors(beanFactory);

// 6 注册拦截器创建的 bean 处理器
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();

// 7 初始化国际化相关资源
initMessageSource();

// 8 初始化事件广播器
initApplicationEventMulticaster();

// 9 为具体的上下文子类初始化特定的 bean
onRefresh();

// 10 注册监听器
registerListeners();

// 11 实例化所有非懒加载的单例 bean
finishBeanFactoryInitialization(beanFactory);

// 12 完成刷新发布相应的事件(Tomcat 就是在这里启动的)
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// 遇到异常销毁已经创建的单例 bean
destroyBeans();

// 充值 active 标识
cancelRefresh(ex);

// 将异常向上抛出
throw ex;
} finally {
// 重置公共缓存,结束刷新
resetCommonCaches();
contextRefresh.end();
}
}
}


简单说一下编号 9 处的 onRefresh() 方法,该方法父类未给出具体实现,需要子类自己实现,ServletWebServerApplicationContext 中的实现如下:


protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}

private void createWebServer() {
......
if (webServer == null && servletContext == null) {
......

// 根据配置获取一个 web server(Tomcat、Jetty 或 Undertow)
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
......
}
......
}


factory.getWebServer(getSelfInitializer()) 会根据项目配置得到一个 Web Server 实例,这里跟下一篇将要谈到的自动配置有点关系。


作者:刘水镜
来源:juejin.cn/post/7206749400172380219
收起阅读 »

他们在学校里不会教你的编程原则

前言 在大学的时候,学校一般只会教你你写编程语言,比如C、C++、JAVA等编程语言。但是当你离开大学进入这个行业开始工作时,才知道编程不只是知道编程语言、语法等,要想写好代码,必须还要了解一些编程原则才行。本文主要讨论KISS、DRY和SOLID这些常见的编...
继续阅读 »

前言


在大学的时候,学校一般只会教你你写编程语言,比如C、C++、JAVA等编程语言。但是当你离开大学进入这个行业开始工作时,才知道编程不只是知道编程语言、语法等,要想写好代码,必须还要了解一些编程原则才行。本文主要讨论KISSDRYSOLID这些常见的编程原则,而且你会发现随着工作时间越久,越能感受这些编程原则的精妙之处,历久弥香。


KISS原则



Keep It Simple, Stupid!



你是不是有过接手同事的代码感到十分头疼的经历,明明可以有更加简单、明白的写法,非要绕来绕去,看不明白?


其实,我们在写代码的时候应该要遵守KISS原则,核心思想就是尽量保持简单。代码的可读性和可维护性是衡量代码质量非常重要的两个标准。而 KISS 原则就是保持代码可读和可维护的重要手段。代码足够简单,也就意味着很容易读懂,bug 比较难隐藏。即便出现 bug,修复起来也比较简单。


我们写代码的的时候要站在别人的角度出发,就像马丁·福勒说的,我们写的代码不是给机器看的,而是给人看的。


“任何傻瓜都可以编写计算机可以理解的代码。优秀的程序员编写出人类可以理解的代码。” — 马丁·福勒


那么如何才能写出满足KISS原则的代码呢?


如何写出KISS原则的代码?


我们直接上例子,下面的校验IP是否合法的3种实现方式,大家觉得哪个最KISS?



  1. 写法一




  1. 写法二




  1. 写法三




  • 写法一代码量最少,正则表达式本身是比较复杂的,写出完全没有 bug 的正则表达本身就比较有挑战;另一方面,并不是每个程序员都精通正则表达式。对于不怎么懂正则表达式的同事来说,看懂并且维护这段正则表达式是比较困难的。这种实现方式会导致代码的可读性和可维护性变差,所以,从 KISS 原则的设计初衷上来讲,这种实现方式并不符合 KISS 原则。

  • 写法二使用了 StringUtils 类、Integer 类提供的一些现成的工具函数,来处理 IP地址字符串,逻辑清晰,可读性好。

  • 写法三不使用任何工具函数,而是通过逐一处理 IP 地址中的字符,来判断是否合法,容易出bug,不好理解。


所以说,符合KISS原则的代码并不是代码越少越好,还要考虑代码是否逻辑清晰、是否容易理解、是否够稳定。


总结以下如何写出KISS原则的代码:



  1. 不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。

  2. 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出bug 的概率会更高,维护的成本也比较高。

  3. 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。

  4. 主观站在别人的角度上编写代码。你在编写代码的时候就要思考我这个同事看这段代码是不是很快就能够明白理解。


DRY原则



Don't Repeat Yourself



你是不是有过这样的经历,项目中很多重复逻辑的代码,然后修改一个地方,另外一个地方忘记修改,导致测试给你提了很多bug?


DRY原则,英文全称Don’t Repeat Yourself,直译过来就是不要重复你自己。这里的重复不仅仅是代码一模一样,还包括实现逻辑重复、功能语义重复、代码执行重复等。我们不要偷懒,有责任把这些存在重复的地方识别出来,然后优化它们。


如何写出DRY原则的代码呢?


我们直接上例子,代码重复的我就不讲了,很好理解,关于实现逻辑或者功能语义重复的我觉个例子。


还是上面校验IP的例子,团队中两个同事由于不知道就有了两种写法。



  • 同事A写法




  • 同事B写法



尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。我们应该在项目中,统一一种实现思路,所有用到判断 IP 地址是否合法的地方,都统一调用同一个函数。不然哪天校验规则变了,很容易只改了其中一个,另外一个漏改,就会出现莫名其妙的bug


其他的比如逻辑重复的意思是虽然功能是不一致的,但是里面的逻辑都是一模一样的。举个例子,比如校验用户名和校验密码,虽然功能不一致,但是校验逻辑都是相似,判空、字符长度等等,这种情况我们就需要把相似的逻辑抽取到一个方法中,不然也是不符合DRY原则。


那么我们平时写代码注意些什么才是符合DRY原则呢?



  • 使用现成的轮子,不轻易造轮子


其实最关键的就是写代码带脑子,用到一个方法先看看有没有现成的,不要看看不看,就动手在那里造轮子。



  • 减少代码耦合


对于高度耦合的代码,当我们希望复用其中的一个功能,想把这个功能的代码抽取出来成为一个独立的模块、类或者函数的时候,往往会发现牵一发而动全身。移动一点代码,就要牵连到很多其他相关的代码。所以,高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合。



  • 满足单一职责原则


我们前面讲过,如果职责不够单一,模块、类设计得大而全,那依赖它的代码或者它依赖的代码就会比较多,进而增加了代码的耦合。根据上一点,也就会影响到代码的复用性。相反,越细粒度的代码,代码的通用性会越好,越容易被复用。



  • 模块化


这里的“模块”,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。



  • 业务与非业务逻辑分离


越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。所以,为了复用跟业务无关的代码,我们将业务和非业务逻辑代码分离,抽取成一些通用的框架、类库、组件等。



  • 通用代码下沉


从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。一般情况下,在代码分层之后,为了避免交叉调用导致调用关系混乱,我们只允许上层代码调用下层代码及同层代码之间的调用,杜绝下层代码调用上层代码。所以,通用的代码我们尽量下沉到更下层。



  • 继承、多态、抽象、封装


在讲面向对象特性的时候,我们讲到,利用继承,可以将公共的代码抽取到父类,子类复用父类的属性和方法。利用多态,我们可以动态地替换一段代码的部分逻辑,让这段代码可复用。除此之外,抽象和封装,从更加广义的层面、而非狭义的面向对象特性的层面来理解的话,越抽象、越不依赖具体的实现,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。



  • 应用模板等设计模式


一些设计模式,也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活地替换其中的部分代码,整个流程模板代码可复用。


SOLID原则


SOLID原则不是一个单一的原则,而是对软件开发至关重要的 5 条原则,遵循这些原则有助于我们写出高内聚、低耦合、可扩展、可维护性好的代码。


S—单一职责原则



一个类应该有一个,而且只有一个改变它的理由。



单一职责原则在我看来是最容易理解也是最重要的一个原则。它的核心思想就是一个模块、类或者方法只做一件事,只有一个职责,千万不要越俎代庖。它可以带来下面的好处:



  • 可以让代码耦合度更低

  • 使代码更容易理解和维护

  • 使代码更易于测试和维护,使软件更易于实施,并有助于避免未来更改的意外副作用


举个例子,我们有两个类PersonAccount。 两者都负有存储其特定信息的单一责任。 如果要更改Person的状态,则无需修改类Account,反之亦然, 不要把账户的行为比如修改账户名changeAcctName写在Person类中。


    public class Person {
private Long personId;
private String firstName;
private String lastName;
private String age;
private List accounts;

// 错误做法
public void changeAcctName(Account account, String acctName) {
acccount.setAccountName(acctName);
// 更新到数据库
}
}

public class Account {
private Long guid;
private String accountNumber;
private String accountName;
private String status;
private String type;

}

所以大家在编写代码的时候,一定要停顿思考下这个段代码真的写在这里吗?另外很关键的一点是如果发现一个类或者一个方法十分庞大,那么很有可能已经违背单一职责原则了,后续维护可想而知十分痛苦。


O—开闭原则



软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。



对扩展开放,对修改关闭,什么意思?很简单,其实就是我们要尽量通过新增类实现功能,而不是修改原有的类或者逻辑。因为修改已有代码很有可能对已有功能引入bug。


让我们通过一个例子来理解这个原则,比如一个通知服务。


    public class NotificationService {
public void sendOTP(String medium) {
if (medium.equals("email")) {
//email 发送
} else if (medium.equals("mobile")) {
// 手机发送
}
}

现在需要新增微信的方式通知,你要怎么做呢? 是在加一个if else吗? 这样就不符合开闭原则了,我们看下开闭原则该怎么写。



  • 定义一个通知服务接口



public interface NotificationService {
public void sendOTP();
}


  • E-mail方式通知类EmailNotification



public class EmailNotification implements NotificationService{
public void sendOTP(){
// write Logic using JavaEmail api
}
}


  • 手机方式通知类MobileNotification



public class MobileNotification implements NotificationService{
public void sendOTP(){
// write Logic using Twilio SMS API
}
}


  • 同样可以添加微信通知服务的实现WechatNotification



public class WechatNotification implements NotificationService{
public void sendOTP(String medium){
// write Logic using wechat API
}
}

这样的方式就是遵循开闭原则的,你不用修改核心的业务逻辑,这样可能带来意向不到的后果,而是扩展实现方式,由调用方根据他们的实际情况调用。


是不是想到了设计模式中的策略模式,其实设计模式就是指导我们写出高内聚、低耦合的代码。


L—里氏替换原则



派生类或子类必须可替代其基类或父类



这个原则稍微有点难以理解,它的核心思想是每个子类或派生类都应该可以替代/等效于它们的基类或父类。这样有一个好处,就是无论子类是什么类型,客户端通过父类调用都不会产生意外的后果。


理解不了?那我我们通过一个例子来理解一下。


让我们考虑一下我有一个名为 SocialMedia 的抽象类,它支持所有社交媒体活动供用户娱乐,如下所示:


    package com.alvin.solid.lsp;

public abstract class SocialMedia {

public abstract void chatWithFriend();

public abstract void publishPost(Object post);

public abstract void sendPhotosAndVideos();

public abstract void groupVideoCall(String... users);
}

社交媒体可以有多个实现或可以有多个子类,如 FacebookWechatWeiboTwitter 等。


现在让我们假设 Facebook 想要使用这个特性或功能。


    package com.alvin.solid.lsp;

public class Wechat extends SocialMedia {

public void chatWithFriend() {
//logic
}

public void publishPost(Object post) {
//logic
}

public void sendPhotosAndVideos() {
//logic
}

public void groupVideoCall(String... users) {
//logic
}
}

我们都知道Facebook都提供了所有上述的功能,所以这里我们可以认为FacebookSocialMedia类的完全替代品,两者都可以无中断地替代。


现在让我们讨论 Weibo


    package com.alvin.solid.lsp;

public class Weibo extends SocialMedia {
public void chatWithFriend() {
//logic
}

public void publishPost(Object post) {
//logic
}

public void sendPhotosAndVideos() {
//logic
}

public void groupVideoCall(String... users) {
//不适用
}
}

我们都知道Weibo微博这个产品是没有群视频功能的,所以对于 groupVideoCall方法来说 Weibo 子类不能替代父类 SocialMedia。所以我们认为它是不符合里式替换原则。


如果强行这么做的话,会导致客户端用父类SocialMedia调用,但是实现类注入的可能是个Weibo的实现,调用groupVideoCall行为,产生意想不到的后果。


那有什么解决方案吗?


那就把功能拆开呗。


    public interface SocialMedia {   
public void chatWithFriend();
public void sendPhotosAndVideos()
}


public interface SocialPostAndMediaManager {
public void publishPost(Object post);
}



public interface VideoCallManager{
public void groupVideoCall(String... users);
}

现在,如果您观察到我们将特定功能隔离到单独的类以遵循LSP。


现在由实现类决定支持功能,根据他们所需的功能,他们可以使用各自的接口,例如 Weibo 不支持视频通话功能,因此 Weibo 实现可以设计成这样:


    public class Instagram implements SocialMedia,SocialPostAndMediaManager{
public void chatWithFriend(){
//logic
}
public void sendPhotosAndVideos(){
//logic
}
public void publishPost(Object post){
//logic
}
}

这样子就是符合里式替换原则LSP。


I—接口隔离原则



接口不应该强迫他们的客户依赖它不使用的方法。



大家可以看看自己的工程,是不是一个接口类中有很多很多的接口,每次调用API方法的时候IDE工具给你弹出一大堆,十分的"臃肿肥胖"。所以该原则的核心思想要将你的接口拆小,拆细,打破”胖接口“,不用强迫客户端实现他们不需要的接口。是不是和单一职责原则有点像?


例如,假设有一个名为 UPIPayment 的接口,如下所示


    public interface UPIPayments {

public void payMoney();

public void getScratchCard();

public void getCashBackAsCreditBalance();
}

现在让我们谈谈 UPIPayments 的一些实现,比如 Google PayAliPay


Google Pay 支持这些功能所以他可以直接实现这个 UPIPaymentsAliPay 不支持 getCashBackAsCreditBalance() 功能所以这里我们不应该强制客户端 AliPay 通过实现 UPIPayments 来覆盖这个方法。


我们需要根据客户需要分离接口,所以为了满足接口隔离原则,我们可以如下设计:



  • 创建一个单独的接口来处理现金返还。



public interface CashbackManager{
public void getCashBackAsCreditBalance();
}

现在我们可以从 UPIPayments 接口中删除getCashBackAsCreditBalanceAliPay也不需要实现getCashBackAsCreditBalance()这个它没有的方法了。


D—依赖倒置原则



高层模块不应该依赖低层模块,两者都应该依赖于抽象(接口)。抽象不应该依赖于细节(具体实现),细节应该取决于抽象。



这个原则我觉得也不是很好理解,所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。比如大家都知道的MVC模式,controller是调用service层接口这个抽象,而不是实现类。这也是我们经常说的要面向接口编程,而非细节或者具体实现,因为接口意味着契约,更加稳定。


我们通过一个例子加深一下理解。



  • 借记卡



public class DebitCard {
public void doTransaction(int amount){
System.out.println("tx done with DebitCard");
}
}


  • 信用卡



public class CreditCard{
public void doTransaction(int amount){
System.out.println("tx done with CreditCard");
}
}

现在用这两张卡你去购物中心购买了一些订单并决定使用信用卡支付


    public class ShoppingMall {
private DebitCard debitCard;
public ShoppingMall(DebitCard debitCard) {
this.debitCard = debitCard;
}
public void doPayment(Object order, int amount){
debitCard.doTransaction(amount);
}
public static void main(String[] args) {
DebitCard debitCard=new DebitCard();
ShoppingMall shoppingMall=new ShoppingMall(debitCard);
shoppingMall.doPayment("some order",5000);
}
}

上面的做法是一个错误的方式,因为 ShoppingMall 类与 DebitCard 紧密耦合。


现在你的借记卡余额不足,想使用信用卡,那么这是不可能的,因为 ShoppingMall 与借记卡紧密结合。


当然你也可以这样做,从构造函数中删除借记卡并注入信用卡。但这不是一个好的方式,它不符合依赖倒置原则。


那该如何正确设计呢?



  • 定义依赖的抽象接口BankCard



public interface BankCard {
public void doTransaction(int amount);
}


  • 现在 DebitCardCreditCard 都实现BankCard



public class CreditCard implements BankCard{
public void doTransaction(int amount){
System.out.println("tx done with CreditCard");
}
}


public class DebitCard implements BankCard {
public void doTransaction(int amount){
System.out.println("tx done with DebitCard");
}
}


  • 现在重新设计购物中心这个高级类,他也是去依赖这个抽象,而不是直接低级模块的实现类



public class ShoppingMall {
private BankCard bankCard;
public ShoppingMall(BankCard bankCard) {
this.bankCard = bankCard;
}
public void doPayment(Object order, int amount){
bankCard.doTransaction(amount);
}
public static void main(String[] args) {
BankCard bankCard=new CreditCard();
ShoppingMall shoppingMall1=new ShoppingMall(bankCard);
shoppingMall1.doPayment("do some order", 10000);
}
}

我们还可以拿 Tomcat这个 Servlet 容器作为例子来解释一下。


Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Sevlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet规范。


总结


本文总结了软件编程中的黄金原则,KISS原则,DRY原则,SOLID原则。这些原则不仅仅适用于编程,也可以指导我们在架构设计上。虽然其中有些原则很抽象,但是大家多多实践和思考,会体会到这些原则的精妙。


作者:JAVA旭阳
来源:juejin.cn/post/7237037029570641979
收起阅读 »

消息推送的实现方式

短轮询(Long Polling) 网络资源:短轮询会产生大量的网络请求,尤其是当客户端轮询间隔很短时。这可能会导致大量的网络开销。 服务器处理:对于每个轮询请求,服务器需要处理该请求并发送响应,即使没有新的数据。这会导致服务器频繁地处理请求,可能增加CPU...
继续阅读 »

短轮询(Long Polling)



  1. 网络资源:短轮询会产生大量的网络请求,尤其是当客户端轮询间隔很短时。这可能会导致大量的网络开销。

  2. 服务器处理:对于每个轮询请求,服务器需要处理该请求并发送响应,即使没有新的数据。这会导致服务器频繁地处理请求,可能增加CPU和内存的使用。

  3. 总结:如果更新频率很低,但客户端仍然频繁地发送请求,短轮询可能会造成资源浪费,因为大多数响应可能只是告知“无新数据”


长轮询(Long Polling)



  • 客户端发送请求到服务器,服务器如果没有准备好的数据,就保持连接开放,直到有数据可以发送。一旦数据被发送,客户端处理数据后再次发送新的请求,如此循环。

  • 长轮询通常用于实时或近实时的通知和更新,比如活动通知。



  1. 网络资源:相比短轮询,长轮询减少了无效的网络请求。服务器只在有新数据时才发送响应,从而减少了网络流量。

  2. 服务器处理:长轮询可能导致服务器需要维护更多的打开连接,因为它会为每个客户端请求保持一个打开的连接,直到有新数据或超时。这可能会增加服务器的内存使用,并可能达到服务器的并发连接限制。

  3. 总结:长轮询在某些场景下可以提供更高效的资源使用,尤其是当数据更新不频繁但需要快速传递给客户端时。但如果有大量的客户端同时进行长轮询,服务器可能需要处理大量的并发打开连接。


WebSocket:



  • WebSocket提供了一个全双工通信通道,使得服务器和客户端可以在任何时刻发送数据给对方。这是一个非常实时且高效的解决方案。适合实时聊天



  1. 网络资源:WebSocket 在建立连接后只需要一个握手过程,之后数据可以在此连接上双向传输,不需要为每条消息进行新的请求和响应。这极大地减少了网络开销。

  2. 服务器处理:一旦 WebSocket 连接被建立,它将保持打开状态,直到客户端或服务器决定关闭它。这意味着服务器必须维护所有活动的 WebSocket 连接,这可能会消耗内存和其他资源。

  3. 总结:WebSocket 在数据频繁更新并且需要实时传递给客户端的场景中非常有效。尽管需要维护持久连接,但由于减少了网络开销,通常更为高效。


服务器发送事件(Server-Sent Events, SSE) :



  • 服务器发送事件是一种使服务器能够发送新数据到客户端的简单方法。它比WebSocket简单,但只允许服务器向客户端发送数据。活动通知和提醒



  1. 网络资源:与 WebSocket 类似,SSE 也只需要一次握手来建立持久连接。一旦连接建立,服务器可以持续地向客户端推送消息。

  2. 服务器处理:SSE 需要维护持久连接以发送数据,但与 WebSocket 相比,SSE 只是单向的。这意味着服务器不需要处理从客户端发来的消息。

  3. 总结:SSE 是一种高效的技术,适用于只需要服务器向客户端推送数据的场景,例如实时消息通知。


HTTP/2 Server Push:




  • HTTP/2协议支持服务器推送,允许服务器在客户端需要之前预先发送数据。这可以减少延迟,但通常只用于发送关联的资源,如CSS或JavaScript文件,而不是用于通用的消息推送。




  • 主要用于提前发送关联资源如CSS、JavaScript文件,以减少加载时间,提高网页性能。




  • 可以减少网页加载时间,提高用户体验。




  • 不适用于通用的消息推送,且需要HTTP/2协议支持,实现可能需要特定的服务器配置。




MQTT协议


MQTT 全称(Message Queue Telemetry Transport):一种基于发布/订阅(publish/subscribe)模式的轻量级通讯协议,通过订阅相应的主题来获取消息,是物联网(Internet of Thing)中的一个标准传输协议。


该协议将消息的发布者(publisher)与订阅者(subscriber)进行分离,因此可以在不可靠的网络环境中,为远程连接的设备提供可靠的消息服务,使用方式与传统的MQ有点类似。


TCP协议位于传输层,MQTT 协议位于应用层,MQTT 协议构建于TCP/IP协议上,也就是说只要支持TCP/IP协议栈的地方,都可以使用MQTT协议。


为什么要用 MQTT协议?


MQTT协议为什么在物联网(IOT)中如此受偏爱?而不是其它协议,比如我们更为熟悉的 HTTP协议呢?



  • 首先HTTP协议它是一种同步协议,客户端请求后需要等待服务器的响应。而在物联网(IOT)环境中,设备会很受制于环境的影响,比如带宽低、网络延迟高、网络通信不稳定等,显然异步消息协议更为适合IOT应用程序。

  • HTTP是单向的,如果要获取消息客户端必须发起连接,而在物联网(IOT)应用程序中,设备或传感器往往都是客户端,这意味着它们无法被动地接收来自网络的命令。

  • 通常需要将一条命令或者消息,发送到网络上的所有设备上。HTTP要实现这样的功能不但很困难,而且成本极高。


第三方推送服务:



  • 使用如Firebase Cloud Messaging (FCM), Apple Push Notification Service (APNs)等第三方推送服务来处理消息推送。


对比


WebSocket和Server-Sent Events提供了较低的延迟和较高的实时性,但可能需要更多的服务器资源。长轮询可能会有更高的延迟,并且可能不是最高效的解决方案。HTTP/2 Server Push和第三方推送服务可能更适合于不需要高度实时性的应用。消息队列和发布/订阅模型提供了一种解耦服务器和客户端的方式,但可能会增加系统的复杂性。


在选择实现方法时,需要考虑应用的具体需求,例如实时性的要求、服务器资源、网络条件以及开发和维护的复杂性。同时,也可以考虑将几种方法结合使用,以满足不同的需求。



  • 如果有大量的客户端并且数据更新不频繁,长轮询可能比短轮询更为有效,因为它减少了无效的网络请求。

  • 如果服务器有并发连接的限制或资源有限,大量的长轮询请求可能会耗尽资源,导致服务器不稳定。

  • 如果数据更新非常频繁,短轮询可能会比较合适,因为它可以更简单地处理频繁的请求。

  • WebSocket 通常在需要实时通信的应用中更为有效和资源高效。它减少了网络开销,并提供了持续的、低延迟的双向通信。

  • 短轮询长轮询 可能更适合不需要持续连接的场景或当 WebSocket 不可用或不适用时的备选方案。

  • WebSocket:提供双向通信,适用于需要实时双向交互的应用,如在线聊天。由于它是全双工的,可能需要更多的资源来处理双向的消息传输。

  • SSE:提供单向通信,适用于只需要服务器推送数据的应用,如股票行情更新。通常,SSE 比 WebSocket 更轻量,因为它只处理单向通信。

  • 短轮询:可能会产生大量网络开销,特别是在数据更新频繁的场景中。

  • 长轮询:减少了网络开销,但可能需要服务器维护大量的打开连接,直到有新数据或超时。


从资源消耗的角度看:



  • WebSocketSSE 都需要维护持久连接,但通常比短轮询和长轮询更高效,因为它们减少了网络开销。

  • SSE 可能比 WebSocket 更轻量,因为它是单向的。

  • 短轮询 可能是最耗资源的,尤其是在频繁请求且数据更新不频繁的场景中。

  • 长轮询 在某些情况下可能比短轮询更高效,但仍然不如 WebSocket 或 SSE。


作者:Pomelo_刘金
来源:juejin.cn/post/7291464815658172471
收起阅读 »

SQL为什么动不动就N百行以K计?

发明SQL的初衷之一显然是为了降低人们实施数据查询计算的难度。SQL中用了不少类英语的词汇和语法,这是希望非技术人员也能掌握。确实,简单的SQL可以当作英语阅读,即使没有程序设计经验的人也能运用。 然而,面对稍稍复杂的查询计算需求,SQL就会显得力不从心,经常...
继续阅读 »

发明SQL的初衷之一显然是为了降低人们实施数据查询计算的难度。SQL中用了不少类英语的词汇和语法,这是希望非技术人员也能掌握。确实,简单的SQL可以当作英语阅读,即使没有程序设计经验的人也能运用。


然而,面对稍稍复杂的查询计算需求,SQL就会显得力不从心,经常写出几百行有多层嵌套的语句。这种SQL,不要说非技术人员难以完成,即使对于专业程序员也不是件容易的事,常常成为很多软件企业应聘考试的重头戏。三行五行的SQL仅存在教科书和培训班,现实中用于报表查询的SQL通常是以“K”计的。


SQL困难的分析探讨


这是为什么呢?我们通过一个很简单的例子来考察SQL在计算方面的缺点。


设有一个由三个字段构成的销售业绩表(为了简化问题,省去日期信息):


sales_amount销售业绩表
sales销售员姓名,假定无重名
product销售的产品
amount该销售员在该产品上的销售额

现在我们想知道出空调和电视销售额都在前10名的销售员名单。


这个问题并不难,人们会很自然地设计出如下计算过程:


1. 按空调销售额排序,找出前10名;


2. 按电视销售额排序,找出前10名;


3. 对1、2的结果取交集,得到答案;


我们现在来用SQL做。


1. 找出空调销售额前10名,还算简单:


select top 10 sales from sales_amount where product='AC' order by amount desc

2. 找出电视销售额前10名。动作一样:


select top 10 sales from sales_amount where product='TV' order by amount desc

3. 求1、2的交集。这有点麻烦,SQL不支持步骤化,上两步的计算结果无法保存,只能再重抄一遍了:


select * from
( select top 10 sales from sales_amount where product='AC' order by amount desc )
intersect
( select top 10 sales from sales_amount where product='TV' order by amount desc )

一个只三步的简单计算用SQL要写成这样,而日常计算中多达十几步的比比皆是,这显然超出来许多人的可接受能力。


我们知道了SQL的第一个重要缺点:不支持步骤化。把复杂的计算分步可以在很大程度地降低问题的难度,反过来,把多步计算汇成一步则很大程度地提高了问题的难度。


可以想象,如果老师要求小学生做应用题时只能列一个算式完成,小朋友们会多么苦恼(当然,不乏一些聪明孩子搞得定)。


SQL查询不能分步,但用SQL写出的存储过程可以分步,那么用存储过程是否可以方便地解决这个问题呢?


暂先不管使用存储过程的技术环境有多麻烦和数据库的差异性造成的不兼容,我们只从理论上来看用分步SQL是否能让这个计算更简单捷些。


1. 计算空调销售额前10名。语句还是那样,但我们需要把结果存起来供第3步用,而SQL中只能用表存储集合数据,这样我们要建一个临时表:


create temporary table x1 as
select top 10 sales from sales_amount where product='AC' order by amount desc

2. 计算电视销售额前10名。类似地


create temporary table x2 as
select top 10 sales from sales_amount where product='TV' order by amount desc

3. 求交集,前面麻烦了,这步就简单些


select * from x1 intersect x2

分步后思路变清晰了,但临时表的使用仍然繁琐。在批量结构化数据计算中,作为中间结果的临时集合是相当普遍的,如果都建立临时表来存储,运算效率低,代码也不直观。


而且,SQL不允许某个字段取值是集合(即临时表),这样,有些计算即使容忍了繁琐也做不到。


如果我们把问题改为计算所有产品销售额都在前10名的销售员,试想一下应当如何计算,延用上述的思路很容易想到:


1. 将数据按产品分组,将每组排序,取出前10名;


2. 将所有的前10名取交集;


由于我们事先不知道会有多个产品,这样需要把分组结果也存储在一个临时表中,而这个表有个字段要存储对应的分组成员,这是SQL不支持的,办法就行不通了。


如果有窗口函数的支持,可以转换思路,按产品分组后,计算每个销售员在所有分组的前10名中出现的次数,若与产品总数相同,则表示该销售员在所有产品销售额中均在前10名内。


select sales
from ( select sales,
from ( select sales,
rank() over (partition by product order by amount desc ) ranking
from sales_amount)
where ranking <=10 )
group by sales
having count(*)=(select count(distinct product) from sales_amount)

这样的SQL,有多少人会写呢?


况且,窗口函数在有些数据库中还不支持。那么,就只能用存储过程写循环依次计算每个产品的前10名,与上一次结果做交集。这个过程比用高级语言编写程序并不简单多少,而且仍然要面对临时表的繁琐。


现在,我们知道了SQL的第二个重要缺点:集合化不彻底。虽然SQL有集合概念,但并未把集合作为一种基础数据类型提供,这使得大量集合运算在思维和书写时都需要绕路。


我们在上面的计算中使用了关键字top,事实上关系代数理论中没有这个东西(它可以被别的计算组合出来),这不是SQL的标准写法。


我们来看一下没有top时找前10名会有多困难?


大体思路是这样:找出比自己大的成员个数作为是名次,然后取出名次不超过10的成员,写出的SQL如下:


select sales
from ( select A.sales sales, A.product product,
(select count(*)+1 from sales_amount
where A.product=product AND A.amount<=amount) ranking
from sales_amount A )
where product='AC' AND ranking<=10


select sales
from ( select A.sales sales, A.product product, count(*)+1 ranking
from sales_amount A, sales_amount B
where A.sales=B.sales and A.product=B.product AND A.amount<=B.amount
group by A.sales,A.product )
where product='AC' AND ranking<=10

这样的SQL语句,专业程序员写出来也未必容易吧!而仅仅是计算了一个前10名。


退一步讲,即使有top,那也只是使取出前一部分轻松了。如果我们把问题改成取第6至10名,或者找比下一名销售额超过10%的销售员,困难仍然存在。


造成这个现象的原因就是SQL的第三个重要缺点:缺乏有序支持。SQL继承了数学上的无序集合,这直接导致与次序有关的计算相当困难,而可想而知,与次序有关的计算会有多么普遍(诸如比上月、比去年同期、前20%、排名等)。


SQL2003标准中增加的窗口函数提供了一些与次序有关的计算能力,这使得上述某些问题可以有较简单的解法,在一定程度上缓解SQL的这个问题。但窗口函数的使用经常伴随着子查询,而不能让用户直接使用次序访问集合成员,还是会有许多有序运算难以解决。


我们现在想关注一下上面计算出来的“好”销售员的性别比例,即男女各有多少。一般情况下,销售员的性别信息会记在花名册上而不是业绩表上,简化如下:


employee员工表
name员工姓名,假定无重名
gender员工性别

我们已经计算出“好”销售员的名单,比较自然的想法,是用名单到花名册时找出其性别,再计一下数。但在SQL中要跨表获得信息需要用表间连接,这样,接着最初的结果,SQL就会写成:


select employee.gender,count(*)
from employee,
( ( select top 10 sales from sales_amount where product='AC' order by amount desc )
intersect
( select top 10 sales from sales_amount where product='TV' order by amount desc ) ) A
where A.sales=employee.name
group by employee.gender

仅仅多了一个关联表就会导致如此繁琐,而现实中信息跨表存储的情况相当多,且经常有多层。比如销售员有所在部门,部门有经理,现在我们想知道“好”销售员归哪些经理管,那就要有三个表连接了,想把这个计算中的where和group写清楚实在不是个轻松的活儿了。


这就是我们要说的SQL的第四个重要困难:缺乏对象引用机制,关系代数中对象之间的关系完全靠相同的外键值来维持,这不仅在寻找时效率很低,而且无法将外键指向的记录成员直接当作本记录的属性对待,试想,上面的句子可否被写成这样:


select sales.gender,count(*)
from (…) // …是前面计算“好”销售员的SQL
group by sales.gender

显然,这个句子不仅更清晰,同时计算效率也会更高(没有连接计算)。


我们通过一个简单的例子分析了SQL的四个重要困难,这也是SQL难写或要写得很长的主要原因。基于一种计算体系解决业务问题的过程,也就是将业务问题的解法翻译成形式化计算语法的过程(类似小学生解应用题,将题目翻译成形式化的四则运算)。SQL的上述困难会造成问题解法翻译的极大障碍,极端情况就会发生这样一种怪现象:将问题解法形式化成计算语法的难度要远远大于解决问题本身


再打个程序员易于理解的比方,用SQL做数据计算,类似于用汇编语言完成四则运算。我们很容易写出3+5*7这样的算式,但如果用汇编语言(以X86为例),就要写成


    mov ax,3
mov bx,5
mul bx,7
add ax,bx

这样的代码无论书写还是阅读都远不如3+5*7了(要是碰到小数就更要命了)。虽然对于熟练的程序员也算不了太大的麻烦,但对于大多数人而言,这种写法还是过于晦涩难懂了,从这个意义上讲,FORTRAN确实是个伟大的发明。


为了理解方便,我们举的例子还是非常简单的任务。现实中的任务要远远比这些例子复杂,过程中会面临诸多大大小小的困难。这个问题多写几行,那个问题多写几行,一个稍复杂的任务写出几百行多层嵌套的SQL也就不奇怪了。而且这个几百行常常是一个语句,由于工程上的原因,SQL又很难调试,这又进一步加剧了复杂查询分析的难度。


更多例子


我们再举几个例子来分别说明这几个方面的问题。


为了让例子中的SQL尽量简捷,这里大量使用了窗口函数,故而采用了对窗口函数支持较好的ORACLE数据库语法,采用其它数据库的语法编写这些SQL一般将会更复杂。
这些问题本身应该也算不上很复杂,都是在日常数据分析中经常会出现的,但已经很难为SQL了。


计算不分步


把复杂的计算分步可以在很大程度地降低问题的难度,反过来,把多步计算汇成一步完成则会提高问题的复杂度。



任务1 销售部的人数,其中北京籍人数,再其中女员工人数?



销售部的人数


select count(*) from employee where department='sales'

其中北京籍的人数


select count(*) from employee where department='sales' and native_place='Beijing'

再其中的女员工人数


select count (*) from employee
where department='sales' and native_place='Beijing' and gender='female'

常规想法:选出销售部人员计数,再在其中找出其中北京籍人员计数,然后再递进地找出女员工计数。每次查询都基于上次已有的结果,不仅书写简单而且效率更高。


但是,SQL的计算不分步,回答下一个问题时无法引用前面的成果,只能把相应的查询条件再抄一遍。



任务2 每个部门挑选一对男女员工组成游戏小组



with A as
(select name, department,
row_number() over (partition by department order by 1) seq
from employee where gender=‘male’)
B as
(select name, department,
row_number() over(partition by department order by 1) seq
from employee where gender=‘female’)
select name, department from A
where department in ( select distinct department from B ) and seq=1
union all
select name, department from B
where department in (select distinct department from A ) and seq=1

计算不分步有时不仅造成书写麻烦和计算低效,甚至可能导致思路严重变形。


这个任务的直观想法:针对每个部门循环,如果该部门有男女员工则各取一名添进结果集中。但SQL不支持这种逐步完成结果集的写法(要用存储过程才能实现此方案),这时必须转变思路为:从每个部门中选出男员工,从每个部门选出女员工,对两个结果集分别选出部门出现在另一个结果集的成员,最后再做并集。


好在还有with子句和窗口函数,否则这个SQL语句简直无法看了。


集合无序


有序计算在批量数据计算中非常普遍(取前3名/第3名、比上期等),但SQL延用了数学上的无序集合概念,有序计算无法直接进行,只能调整思路变换方法。



任务3 公司中年龄居中的员工



select name, birthday
from (select name, birthday, row_number() over (order by birthday) ranking
from employee )
where ranking=(select floor((count(*)+1)/2) from employee)

中位数是个常见的计算,本来只要很简单地在排序后的集合中取出位置居中的成员。但SQL的无序集合机制不提供直接用位置访问成员的机制,必须人为造出一个序号字段,再用条件查询方法将其选出,导致必须采用子查询才能完成。



任务4 某支股票最长连续涨了多少交易日



select max (consecutive_day)
from (select count(*) (consecutive_day
from (select sum(rise_mark) over(order by trade_date) days_no_gain
from (select trade_date,
case when
closing_price>lag(closing_price) over(order by trade_date)
then 0 else 1 END rise_mark
from stock_price) )
group by days_no_gain)

无序的集合也会导致思路变形。


常规的计算连涨日数思路:设定一初始为0的临时变量记录连涨日期,然后和上一日比较,如果未涨则将其清0,涨了再加1,循环结束看该值出现的最大值。


使用SQL时无法描述此过程,需要转换思路,计算从初始日期到当日的累计不涨日数,不涨日数相同者即是连续上涨的交易日,针对其分组即可拆出连续上涨的区间,再求其最大计数。这句SQL读懂已经不易,写出来则更困难了。


集合化不彻底


毫无疑问,集合是批量数据计算的基础。SQL虽然有集合概念,但只限于描述简单的结果集,没有将集合作为一种基本的数据类型以扩大其应用范围。



任务5 公司中与其他人生日相同的员工



select * from employee
where to_char (birthday, ‘MMDD’) in
( select to_char(birthday, 'MMDD') from employee
group by to_char(birthday, 'MMDD')
having count(*)>1 )

分组的本意是将源集合分拆成的多个子集合,其返回值也应当是这些子集。但SQL无法表示这种“由集合构成的集合”,因而强迫进行下一步针对这些子集的汇总计算而形成常规的结果集。


但有时我们想得到的并非针对子集的汇总值而是子集本身。这时就必须从源集合中使用分组得到的条件再次查询,子查询又不可避免地出现。



任务6 找出各科成绩都在前10名的学生



select name
from (select name
from (select name,
rank() over(partition by subject order by score DESC) ranking
from score_table)
where ranking<=10)
group by name
having count(*)=(select count(distinct subject) from score_table)

用集合化的思路,针对科目分组后的子集进行排序和过滤选出各个科目的前10名,然后再将这些子集做交集即可完成任务。但SQL无法表达“集合的集合”,也没有针对不定数量集合的交运算,这时需要改变思路,利用窗口函数找出各科目前10名后再按学生分组找出出现次数等于科目数量的学生,造成理解困难。


缺乏对象引用


在SQL中,数据表之间的引用关系依靠同值外键来维系,无法将外键指向的记录直接用作本记录的属性,在查询时需要借助多表连接或子查询才能完成,不仅书写繁琐而且运算效率低下。



任务7 女经理的男员工们



用多表连接


select A.*
from employee A, department B, employee C
where A.department=B.department and B.manager=C.name and
A.gender='male' and C.gender='female'

用子查询


select * from employee
where gender='male' and department in
(select department from department
where manager in
(select name from employee where gender='female'
))

如果员工表中的部门字段是指向部门表中的记录,而部门表中的经理字段是指向员工表的记录,那么这个查询条件只要简单地写成这种直观高效的形式:


where gender='male' and department.manager.gender='female'

但在SQL中则只能使用多表连接或子查询,写出上面那两种明显晦涩的语句。



任务8 员工的首份工作公司



用多表连接


select name, company, first_company
from (select employee.name name, resume.company company,
row_number() over(partition by resume. name
order by resume.start_date) work_seq
from employee, resume where employee.name = resume.name)
where work_seq=1

用子查询


select name,
(select company from resume
where name=A.name and
start date=(select min(start_date) from resume
where name=A.name)) first_company
from employee A

没有对象引用机制和彻底集合化的SQL,也不能将子表作主表的属性(字段值)处理。针对子表的查询要么使用多表连接,增加语句的复杂度,还要将结果集用过滤或分组转成与主表记录一一对应的情况(连接后的记录与子表一一对应);要么采用子查询,每次临时计算出与主表记录相关的子表记录子集,增加整体计算量(子查询不能用with子句了)和书写繁琐度。


SPL的引入


问题说完,该说解决方案了。


其实在分析问题时也就一定程度地指明了解决方案,重新设计计算语言,克服掉SQL的这几个难点,问题也就解决了。


这就是发明SPL的初衷!


SPL是个开源的程序语言,其全名是Structured Process Language,和SQL只差一个词。目的在于更好的解决结构化数据的运算。SPL中强调了步骤化、支持有序集合和对象引用机制、从而得到彻底的集合化,这些都会大幅降低前面说的“解法翻译”难度。


这里的篇幅不合适详细介绍SPL了,我们只把上一节中的8个例子的SPL代码罗列出来感受一下:



任务1



AB
1=employee.select(department=="sales")=A1.len()
2=A1.select(native_place=="Beijing")=A2.len()
3=A2.select(gender=="female")=A3.len()

SPL可以保持记录集合用作中间变量,可逐步执行递进查询。



任务2



ABC
1for employee.group(department)=A1.group@1(gender)
2>if B1.len()>1=@|B1

有步骤和程序逻辑支持的SPL能很自然地逐步完成结果。



任务3



A
1=employee.sort(birthday)
2=A1((A1.len()+1)/2)

对于以有序集合为基础的SPL来说,按位置取值是个很简单的任务。



任务4



A
1=stock_price.sort(trade_date)
2=0
3=A1.max(A2=if(close_price>close_price[-1],A2+1,0))

SPL按自然的思路过程编写计算代码即可。



任务5



A
1=employee.group(month(birthday),day(birthday))
2=A1.select(~.len()>1).conj()

SPL可以保存分组结果集,继续处理就和常规集合一样。



任务6



A
1=score_table.group(subject)
2=A1.(~.rank(score).pselect@a(~<=10))
3=A1.(~(A2(#)).(name)).isect()

使用SPL只要按思路过程写出计算代码即可。



任务7



A
1=employee.select(gender=="male" && department.manager.gender=="female")

支持对象引用的SPL可以简单地将外键指向记录的字段当作自己的属性访问。



任务8



A
1=employee.new(name,resume.minp(start_date).company:first_company)

SPL支持将子表集合作为主表字段,就如同访问其它字段一样,子表无需重复计算。


SPL有直观的IDE,提供了方便的调试功能,可以单步跟踪代码,进一步降低代码的编写复杂度。


imagepng


对于应用程序中的计算,SPL提供了标准的JDBC驱动,可以像SQL一样集成到Java应用程序中:



Class.forName("com.esproc.jdbc.InternalDriver");
Connection conn =DriverManager.getConnection("jdbc:esproc:local://");
Statement st = connection.();
CallableStatement st = conn.prepareCall("{call xxxx(?,?)}");
st.setObject(1, 3000);
st.setObject(2, 5000);
ResultSet result=st.execute();
...


SPL资料



作者:苏三说技术
来源:juejin.cn/post/7189609501559881784
收起阅读 »

通过问题透析 IDEA Debug

引言 本来通过问题引入,一步一步对问题进行分析,重点学习 IDEA Debug 的能力解决问题。阅读本文可以学习如何通过 IDEA 的 Debug 功能解决实际问题。本文适合学生和刚工作的朋友,把 IDEA 作为开发工具,并且有 Spring 和 JPA 的使...
继续阅读 »

引言


本来通过问题引入,一步一步对问题进行分析,重点学习 IDEA Debug 的能力解决问题。阅读本文可以学习如何通过 IDEA 的 Debug 功能解决实际问题。本文适合学生和刚工作的朋友,把 IDEA 作为开发工具,并且有 Spring 和 JPA 的使用经验。


问题引入


最近看了 eclipse 开源的集合 Eclipse Collections,觉得它的使用相比 JDK 集合更加简洁,想在实际项目中使用。部分 API 对比如下。


JDK API


 //users is List<User> 
users.stream.map(user -> user.getCity()).collect(Collectors.toList());

Eclipse Collections API


 //users is MutableList<User>
users.collect(user -> user.getCity);

可以看到后者比前者要简洁不少。实际开发中集合数据大多还是来自数据库查询,使用 JPA 查询如下。


JDK API


List<User> findByCity(String city);

我想改成 Eclipse Collections API


MutableList<User> findByCity(String city);

然而报错了


org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.util.ArrayList<?>] to type [org.eclipse.collections.api.list.MutableList<?>] for value '[]'; nested exception is java.lang.IllegalArgumentException: Unsupported Collection interface: org.eclipse.collections.api.list.MutableList
at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:47)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:192)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:175)

如果不想看过过程,结论是改成如下代码或者升级 sping boot 到 2.7.0 及以上版本。


FastList<User> findByCity(String city);

Debug


对代码简单分析



  • 查看类名称,方法名称。 有 convert.ConversionFailedException/ConversionUtils.invokeConverter/convert.support.GenericConversionService.convert等等,关键词 convert。我们应该联想到这段代码的功能是把某一个类型 convert 到某一个类型。

  • 再看一眼报错信息,Failed to convert from type [java.util.ArrayList<?>] to type [org.eclipse.collections.api.list.MutableList<?>],无法将 ArrayList 转换成 MutableList

  • 再分析报错的那一行return converter.convert(source, sourceType, targetType),我们会更清晰一点。

    • result 是转换的结果,应该是 MutableList 的一个实例。

    • convert 方法是执行转换的核心逻辑,我们要的核心转换逻辑代码肯定在这里,如果你直接去看的话,它肯定是一个接口,这是面向接口编程。

    • sourceType 源类型,是 ArrayList 类型。

    • targetType 目标类型,是 MutableList 类型。




打断点


在 IDEA 控制台可以直接点击报错 class 定位到源文件,我们先点击 ConversionFailedException ,再点击 ConversionUtils.java:47,发现都是报错的异常,对我们没有帮助。最后我们点击 GenericConversionService.java:192,终于看到一行代码了。


Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);

断点分析


执行过程会停留在断点处,我们可以查看上下文变量类的实例。这里我们以 converter 为例。按照数字步骤点击,如下。


image.png


图中显示是 ConvertertoString 方法的结果。
可能的 converter 如下:


1. java.lang.String -> java.lang.Enum
2. NO_OP
3. java.lang.Boolean -> java.lang.String
// 等等。。。。。

由于是底层方法,被调用的次数很多,在这个断点停留的次数也很多。很多次不是我们想要的 Converter


条件断点


顾名思义 IDEA 会通过我们添加的条件来判断这个断点是否需要被处理。


我们想要的 Converter 是什么呢?回到代码分析阶段,我们想要的 ConvertersourceTypetargetType,通过上面分析 targetType 类型是 MutableList 类型。


下面添加条件断点:


image.png

完整的条件如下:


MutableList.class.isAssignableFrom(targetType.getType());

添加成功的标志如下,会在断点处显示问号。


image.png


单步调试


Debug 模式启动程序,可以看到 IDEA 停留在我们的条件断点上,并且targetType 的类型正是 MutableList


image.png


单步调试代码,来到 org.springframework.core.CollectionFactory#createCollection 方法。部分代码如下:


//省略的其他代码

// 判断集合类型是不是 ArrayList 或者 List,显然这里不是
else if (ArrayList.class == collectionType || List.class == collectionType) {
return new ArrayList<>(capacity);
}
//省略的其他代码

else {
//如果是集合类型的接口 或者 不是集合类型抛出异常
if (collectionType.isInterface() || !Collection.class.isAssignableFrom(collectionType)) {
throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName());
}
try {
//如果是集合类型的类,直接通过反射实例化。
return (Collection<E>) ReflectionUtils.accessibleConstructor(collectionType).newInstance();
}
}

重回代码分析


上面的 collectionTypeMutableList,而 MutableList 是接口,走读代码可以发现最终会执行下面的代码,最终导致抛出异常。


if (collectionType.isInterface() || !Collection.class.isAssignableFrom(collectionType)) {
throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName());
}

所以只需要我们的目标集合不是接口就行了,FastListMutableList 的实现类。 修改代码为如下:


FastList<User> findByCity(String city);

翻看控制台找到了下面的异常信息,这也侧面反映我们之前找的报错位置不是很精确。我们寻找异常时应该选择最原始的异常信息。


Caused by: java.lang.IllegalArgumentException: Unsupported Collection type: org.eclipse.collections.api.list.MutableList
at org.springframework.core.CollectionFactory.createCollection(CollectionFactory.java:205)
at org.springframework.core.convert.support.CollectionToCollectionConverter.convert(CollectionToCollectionConverter.java:81)

继续分析源码可以发现,如果我们定义的类型不是接口,JPA 就会通过反射创建集合,即如下代码:


return (Collection<E>) ReflectionUtils.accessibleConstructor(collectionType).newInstance();

总结


本来通过解决实际问题介绍了 IDEA Debug 功能的使用。还有以下几点需要注意。



  • 查找异常时要定位到最初始的异常,这样往往能迅速处理问题。

  • 本文的问题只有在 sping boot 2.7.0 以下才会出现,高版本已经修复此问题。参见提交 spring data common

  • 使用非 Java 官方集合需要进行转换,有微小的性能损耗,对于常规内存操作来说影响很小,而且高版本中有优化。如果查询数据上千上万条时,应该避免转换,当然也要使用分页避免一次性查询成千上万的数据。


本文源码


作者:郁乎文
来源:juejin.cn/post/7185569129024192568
收起阅读 »

Redis只用来做缓存?来认识一下它其他强大的能力吧。

当今互联网应用中,随着业务的发展,数据量越来越大,查询效率越来越高,对于时序数据的存储、查询和分析需求也越来越强烈,这时候 Redis 就成为了首选的方案之一。 Redis 提供了多种数据结构,如字符串、哈希表、列表、集合、有序集合等,每种数据结构都具备不同的...
继续阅读 »

当今互联网应用中,随着业务的发展,数据量越来越大,查询效率越来越高,对于时序数据的存储、查询和分析需求也越来越强烈,这时候 Redis 就成为了首选的方案之一。


Redis 提供了多种数据结构,如字符串、哈希表、列表、集合、有序集合等,每种数据结构都具备不同的特性,可以满足不同的业务需求。其中,有序集合的 score 可以存储时间戳,非常适合用于存储时序数据,例如监控指标、日志、统计数据、报表等。下面举几个时序数据场景例子:



  1. 监控指标:


假设我们有一个服务,名为 my_service,需要监控它的请求响应时间。我们可以使用 Redis 有序集合来存储数据,每个请求的响应时间作为 value,请求的时间戳作为 score。示例如下:


> ZADD requests:my_service 1613115560 350
(integer) 1
> ZADD requests:my_service 1613115570 450
(integer) 1
> ZADD requests:my_service 1613115580 550
(integer) 1

这些命令向名为 requests:my_service 的有序集合中添加了 3 条数据,分别是 2021 年 2 月 12 日 10:19:20 的请求响应时间为 350ms,10:19:30 的请求响应时间为 450ms,10:19:40 的请求响应时间为 550ms。


接下来,我们来看一下如何使用 Redis 命令查询这些监控指标的数据。下面的命令会返回 requests:my_service 有序集合内所有数据:


> ZRANGE requests:my_service 0 -1 WITHSCORES
1) "350"
2) "1613115560"
3) "450"
4) "1613115570"
5) "550"
6) "1613115580"

命令执行结果表示,数据按照 score 排序,其中 score 是时间戳(单位为秒),value 是请求响应时间(单位为毫秒)。同时,使用 ZRANGEBYSCORE 命令可以获取一段时间范围内的监控数据,例如:


> ZRANGEBYSCORE requests:my_service 1613115570 1613115580 WITHSCORES
1) "450"
2) "1613115570"
3) "550"
4) "1613115580"

这条命令返回了 requests:my_service 有序集合中在时间戳 1613115570 到 1613115580 之间的所有数据。



  1. 日志:


假设我们要存储的日志是一条指定格式的字符串,包含时间戳和日志内容。使用 Redis 列表存储日志数据,每次写入新日志时可以使用 Redis 列表的 rpush 命令将数据写入列表的尾部。示例如下:


> RPUSH logs:my_logs 2021-02-12 10:30:00 INFO message 1
(integer) 1
> RPUSH logs:my_logs 2021-02-12 10:30:01 ERROR message 2
(integer) 2
> RPUSH logs:my_logs 2021-02-12 10:30:02 WARN message 3
(integer) 3

这些命令向名为 logs:my_logs 的列表尾部添加 3 条数据,分别是 2021 年 2 月 12 日 10:30:00 的 INFO 级别消息,10:30:01 的 ERROR 级别消息和 10:30:02 的 WARN 级别消息。


接下来,我们来看一下如何使用 Redis 命令查询这些日志数据。下面的命令会返回 logs:my_logs 列表内所有数据:


> LRANGE logs:my_logs 0 -1
1) "2021-02-12 10:30:00 INFO message 1"
2) "2021-02-12 10:30:01 ERROR message 2"
3) "2021-02-12 10:30:02 WARN message 3"

命令执行结果表示,数据按照插入顺序排序,从列表头部开始遍历。使用 ZRANGEBYSCORE 命令可以获取一段时间范围内的日志数据,例如:


> ZRANGEBYSCORE logs:my_logs 1613115570 1613115580
1) "2021-02-12 10:30:01 ERROR message 2"

这条命令返回了 logs:my_logs 列表中在时间戳 1613115570 到 1613115580 之间的日志数据,但因为日志数据并没有具体的 time stamp 做 score,所以这个例子只是演示这个命令的用法,实际上应该使用有序集合去查询时间区间内的日志数据。



  1. 统计数据:


假设我们要存储的统计数据是一些具体业务相关的计数器,例如每分钟用户访问量。我们可以使用 Redis 有序集合来存储统计数据,key 是计数器名称,score 是时间戳,value 是具体的计数值(例如访问次数)。示例如下:


> ZADD visits 1613167800 100
(integer) 1
> ZADD visits 1613167860 120
(integer) 1
> ZADD visits 1613167920 150
(integer) 1

这些命令向名为 visits 的有序集合中添加了 3 条数据,分别是 2021 年 2 月 12 日 23:30:00 的访问次数为 100,23:31:00 的访问次数为 120,23:32:00 的访问次数为 150。


接下来,我们来看一下如何使用 Redis 命令查询这些统计数据。下面的命令会返回 visits 有序集合内所有数据:


> ZRANGE visits 0 -1 WITHSCORES
1) "100"
2) "1613167800"
3) "120"
4) "1613167860"
5) "150"
6) "1613167920"

命令执行结果表示,数据按照 score 排序,其中 score 是时间戳(单位为秒),value 是访问次数。使用 ZRANGEBYSCORE 命令可以获取一段时间范围内的统计数据,例如:


> ZRANGEBYSCORE visits 1613167860 1613167920 WITHSCORES
1) "120"
2) "1613167860"
3) "150"
4) "1613167920"

这条命令返回了 visits 有序集合中在时间戳 1613167860 到 1613167920 之间的所有数据。


使用 Redis 有序集合中的另一个常见场景是计算 TopN,例如找出访问次数最多的前 10 个计数器,可以使用命令 ZREVRANGE visits 0 9 WITHSCORES,它返回 visits 有序集合中前 10 个元素,按照 value 从大到小排列,并且返回每个元素的 score。


需求实践:


这是一个实时监控系统,主要用于记录和统计服务发生的错误情况,以便在错误数量超过预设阈值时发出警告信息。


系统每秒钟生成随机错误数据,并将它们存储到 Redis 数据库中。每隔 10 秒钟,系统会从 Redis 数据库中聚合最近一分钟内的错误数据,并按照服务名和错误类型进行统计计算。如果某个服务的错误数量超过预设阈值,系统会输出一条警告信息提示用户。


整个系统的目标是帮助用户及时了解每个服务的错误情况,以便及时采取相应的措施,保障服务的稳定性和可靠性。


代码示例:


模拟接口服务异常数据


package com.example.demo.redis;

import redis.clients.jedis.Jedis;
import java.util.*;

public class DataGenerator {
// 定义服务列表
private static final List SERVICES = Arrays.asList("service1", "service2", "service3");
// 定义错误列表
public static final List ERRORS = Arrays.asList("invalid_param", "timeout", "unknown_error");

/**
* 生成数据
*
*
@param total 数据总数
*
@param jedis Redis 客户端连接
*/

public static void generateData(int total, Jedis jedis) {
Random rand = new Random(); // 初始化随机数生成器
long currentTimestamp = System.currentTimeMillis() / 1000; // 获取当前时间戳,精确到秒
long startTimestamp = currentTimestamp - 60; // 计算起始时间戳,为当前时间戳减去 60 秒

for (int i = 0; i < total; i++) { // 循环 total 次,生成 total 条数据
String service = SERVICES.get(rand.nextInt(SERVICES.size())); // 随机选择一个服务
String error = ERRORS.get(rand.nextInt(ERRORS.size())); // 随机选择一个错误
long timestamp = startTimestamp + rand.nextInt(60); // 生成一个随机时间戳,精确到秒,范围为起始时间戳到当前时间戳
int count = 1;
String item = String.format("%s:%s:%d:%d", service, error, timestamp, count);
jedis.zadd("error_data", timestamp, item); // 将错误数据存储到 Redis 数据库中
}
}
}


聚合异常数据,达到阈值告警


package com.example.demo.redis;

import redis.clients.jedis.Jedis;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class DataAggregator {
private static final String REDIS_HOST = "localhost"; // Redis 主机名
private static final int REDIS_PORT = 6379; // Redis 端口号
private static final int THRESHOLD = 100; // 预设阈值,当错误数量超过该阈值时触发警告

public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); // 创建一个只有一个线程的定时任务执行程序
Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT); // 创建 Redis 客户端连接

scheduler.scheduleAtFixedRate(() -> {
// 并发情况下,线程会阻塞
synchronized (jedis) {
DataGenerator.generateData(20, jedis); // 生成随机错误数据,并将其存储到 Redis 数据库中
}
}, 0, 1, TimeUnit.SECONDS); // 定时任务间隔为 1 秒钟

scheduler.scheduleAtFixedRate(() -> { // 定时任务逻辑
synchronized (jedis) {
long currentTimestamp = System.currentTimeMillis() / 1000; // 获取当前时间戳,精确到秒
long startTimestamp = currentTimestamp - 60; // 计算起始时间戳,为当前时间戳减去 60 秒
Set data = jedis.zrangeByScore("error_data", startTimestamp, currentTimestamp); // 使用 zrange 命令获取指定时间范围内的数据

Map> countMap = new HashMap<>(); // 用于记录聚合后的服务和错误数量信息
for (String item : data) { // 遍历所有错误数据
String[] parts = item.split(":"); // 以冒号为分隔符,将错误数据分割为部分
String service = parts[0]; // 获取服务名
String error = parts[1]; // 获取错误类型
long timestamp = Long.parseLong(parts[2]); // 获取时间戳
int count = Integer.parseInt(parts[3]); // 获取错误数量

if (timestamp < startTimestamp) { // 如果时间戳早于起始时间戳,则跳过该数据
continue;
}

Map serviceCountMap = countMap.computeIfAbsent(service, k -> new HashMap<>()); // 获取指定服务的错误数量信息
serviceCountMap.put(error, serviceCountMap.getOrDefault(error, 0) + count); // 更新指定服务和错误类型的错误数量信息
}

List alerts = new ArrayList<>(); // 用于存储警告信息
for (String service : countMap.keySet()) { // 遍历服务名列表
Map serviceCountMap = countMap.get(service); // 获取服务和错误数量信息
int totalErrors = 0;
for (String error : serviceCountMap.keySet()) { // 遍历错误列表
int count = serviceCountMap.get(error); // 获取错误数量
totalErrors += count;
}
if (totalErrors > THRESHOLD) { // 如果错误数量超过预设阈值
alerts.add(service + " has too many errors: " + serviceCountMap.keySet() + ", count: " + totalErrors); // 将该服务名添加到警告信息列表中
}
}
if (!alerts.isEmpty()) { // 如果警告信息列表不为空
System.out.println(String.join("\n", alerts)); // 打印警告信息
}
}
}, 0, 10, TimeUnit.SECONDS); // 定时任务间隔为 10 秒

// 关闭 Redis 连接
jedis.close();
}
}


以上代码可正常运行,有疑问可以交流~~


作者:程序员的思考与落地
来源:juejin.cn/post/7219669309537484837
收起阅读 »

分库分表,真的有必要吗?

分库分表,真的有必要吗? 哈喽,大家好,我是janker。 关于数据库分库分表的面试题已经是烂大街了,面经小册各路神仙的经验分享也是不绝于耳。当然现有的技术解决方案已经是很成熟了。 但是想要使用的得心应手,首先应该搞清楚三个问题? 为什么使用分库分表? 何时...
继续阅读 »

分库分表,真的有必要吗?


哈喽,大家好,我是janker。


关于数据库分库分表的面试题已经是烂大街了,面经小册各路神仙的经验分享也是不绝于耳。当然现有的技术解决方案已经是很成熟了。


但是想要使用的得心应手,首先应该搞清楚三个问题?



  • 为什么使用分库分表?

  • 何时使用分库分表?

  • 如何分库分表?


为什么使用分库分表?


答案很简单:当数据库出现性能瓶颈。顾名思义就是数据库扛不住了。


数据库出现瓶颈,对外表现有以下几个方面?



  1. 高并发场景下,大量请求阻塞,大量请求都需要操作数据库,导致连接数不够了,请求处于阻塞状态。

  2. SQL操作变慢(慢SQL增多)如果数据库中存在一张上亿数据量的表,一条 SQL 没有命中索引会全表扫描,这个查询耗时会非常久。

  3. 随着业务流量变大存储出现问题,单库数据量越来越大,给存储造成巨大压力。


从机器角度,性能瓶颈不外乎就是CPU、磁盘、内存、网络这些,要解决性能瓶颈最简单粗暴的方式就是提升机器性能,但是通过这种方式投入产出比往往不高,也不划算,所以重点还是要从软件层面去解决问题。


数据库相关优化方案


数据库优化方案很多,主要分为两大类:软件层面、硬件层面。


软件层面包括:SQL 调优、表结构优化、读写分离、数据库集群、分库分表等;


硬件层面主要是增加机器性能。


分库分表其实不是数据库优化方案的最终解决办法,一般来说说能用优化SQL、表结构优化、读写分离等手段解决问题,就不要分库分表,因为分库分表会带来更多需要解决的问题,比如说分布式事务,查询难度增大等。


何时使用分库分表?


什么时候我们才会选择分库分表?前面已经说了,除了分库分表以外那些软件手段搞不定的时候,我们才会选择分库分表。


我们心中可能会有这些疑问?



  1. 使用分库分表,我们的评判依据是什么?

  2. 一张表存储了多少数据的时候,才需要考虑分库分表?

  3. 数据增长速度很快,每天产生多少数据,才需要考虑做分库分表?


阿里巴巴开发手册有推荐的思路:单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。


注意:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。


如何分库分表?


当前针对分库分表有很多解决方案。这里分两个方面来展开说说:分库 和 分表。


分库


很多项目前期为了快速迭代,多个应用公用一套数据库,随着业务量增加,系统访问压力增大,系统拆分就势在必行。


为了保证业务平滑,系统架构重构也是分了几个阶段进行。


多应用单数据库


第一个阶段将商城系统单体架构按照功能模块拆分为子服务,比如:Portal 服务、用户服务、订单服务、库存服务等。


image-20230107232802486


多应用单数据库如上图,多个服务共享一个数据库,这样做的目的是底层数据库访问逻辑可以不用动,将影响降到最低。


多应用多数据库


随着业务推广力度加大,数据库终于成为了瓶颈,这个时候多个服务共享一个数据库基本不可行了。我们需要将每个服务相关的表拆出来单独建立一个数据库,这其实就是“分库”了。


单数据库的能够支撑的并发量是有限的,拆成多个库可以使服务间不用竞争,提升服务的性能。


image-20230107233441875


从一个大的数据中分出多个小的数据库,每个服务都对应一个数据库,这就是系统发展到一定阶段必要要做的“分库”操作。


分表


说完了分库,那什么时候才会分表呢?


如果系统处于高速发展阶段,拿商城系统来说,一天下单量可能几十万,那数据库中的订单表增长就特别快,增长到一定阶段数据库查询效率就会出现明显下降。


因此当表数据增长过快,根据阿里巴巴开发规范中超过500w的数据就要考虑分表了,当然这只是一个经验值,具体要不要分表还要看业务考虑未来三年的一个业务增量。


如何分表?


分表有几个维度,一是水平切分和垂直切分,二是单库内分表和多库内分表。


水平拆分和垂直拆分


拿商品表来说,表中分为几类属性:一类是基础属性,例如:商品名称、通用名,商品编码等信息。二类是规格属性:尺寸、型号等信息。三类是拓展属性:描述商品特征的一些属性。我们可以将其拆分为基础信息表、拓展属性表、属性信息表等。这几张表结构不同并且相互独立。但是从这个角度没有解决因为数据量过大而带来的性能问题,因此我们还需要继续做水平拆分。


image-20230108161956743


水平拆分表的方法很多种,比如说1w条数据,我们拆分为两个表,id 为基数的放在user1,id为偶数的放在user2中,这样的拆分方式就是水平拆分。


其他水平拆分的方式也很多,除了上面按照 id 来拆分,还可以按照时间维度拆分,比如订单表,可以按照每日、每月等进行拆分。



  • 每日表:只存储当天你的数据。

  • 每月表:可以起一个定时任务将前一天的数据全部迁移到当月表。

  • 历史表:同样可以用定时任务把时间超过 30 天的数据迁移到 history表。


总结一下水平拆分和垂直拆分的特点:



  • 垂直切分:基于表或者字段划分,表结构不同。

  • 水平拆分:基于数据划分,表结构相同,数据不同。根据表中字段的某一列特性,分而治之。


水平拆分也分两种拆分方式。单库内拆分和多库内拆分


单库内拆分和多库内拆分


拿针对用户表的拆分来举例,之前的单个用户表按照某种规则拆分为多个子表,多个子表存在于同一数据库中。比如下面用户表拆分为用户1表、用户2表。


image-20230108203705147


单库内拆分是在一个数据库中,将一张表拆分为多个子表,一定程度上可以解决单表查询的性能问题,但是也会遇到另外一个问题:但数据库的数据瓶颈。


所以在行业内更多的将子表拆分到多个数据库中,如下图,用户表拆分为4个子表,4个子表分两批存在两个不同的数据库中,根据一定的规则进行路由。


image-20230108204316330


多库拆分用一句话概括:主要是为了减少单张表数据量大小,解决单表数据量过大带来的性能问题。


但是分库分表也带来许多问题。


分库分表带来的问题


既然分库分表方案那么好,那我们是不是在项目初期就应该采用这种方案呢?莫慌,虽然分库分表解决了很多性能问题,但是同时也给系统带来了很多复杂性。下面我展开说说


1. 跨库关联查询


之前单体项目,我们想查询一些数据,无脑join就好了,只要数据模型设计没啥问题,关联查询起来其实还是很简单的。现在不一样了,分库分表后的数据可能不在一个数据库,那我们如何关联查询呢?


下面推荐几种方式去解决这个问题:



  1. 字段冗余:把需要关联的字段放到主表中,避免join操作,但是关联字段更新,也会引发冗余字段的更新;

  2. 数据抽象:通过ETL 等将数据汇总聚合,生成新的表;

  3. 全局表:一般是一些基础表可以在每个数据库都放一份;

  4. 应用层组装:将基础数据查出来,通过应用程序计算组装;

  5. 同特征的数据在一张表:举个例子:同一个用户的数据在同一个库中,比如说我们对订单按照用户id进行分表,订单主表、订单拓展信息表、跟订单有关联查询的表都按照用户id 进行分表,那么同一个用户的订单数据肯定在同一个数据库中,订单维度的关联查询就不存在跨库的问题了。


2. 分布式事务


单数据库我们可以用本地事务搞定,使用多数据库就只能通过分布式事务解决了。


常用的解决方案有:基于可靠消息(MQ)的最终一致性方案、二段式提交(XA)、柔性事务。


当然分布式事务相关开源项目推荐两个:SeataTX-LCN


比较推荐 Seata,阿里出品、大厂加持、如果需要企业级版本支持也是有的。


3. 排序、分表、函数计算问题


使用SQL 时,order bylimit 等关键字需要特殊处理,一般都是采用数据分片的思路:现在每个分片路由上执行函数、然后将每个分片路由的结果汇总再计算,然后得出最终结果。


开源的解决方案当然也有不少,比较推荐shardingsphere,无论是基于client 或者 基于数据库proxy的都有支持。


4. 分布式ID


既然分库分表了,主键id已经不能唯一确定我们的业务数据了,随之而来的就是分布式id,顾名思义就是在多个数据库多张表中唯一确定的ID。


常见的分布式Id 解决方案有:



  1. UUID

  2. 基于全局数据库自增的ID表

  3. 基于Redis缓存生成全局ID

  4. 雪花算法(Snowflake

  5. 百度uid-generator(雪花算法的变种)

  6. 美团Leaf(雪花算法的变种)

  7. 滴滴Tinyid


这些解决方案后面有专门的文章去介绍,这里不过多展开。


5. 多数据源


分库分表之后可能面临从多个数据库中获取数据,一般的解决方案有,基于 client 适配 和 基于 proxy 适配。


比较成熟并且常用的中间件有:



  • shardingsphere(apache顶级项目相当成熟,文档完善)

  • MyCat (社区不太活跃、不推荐)


总结


如果遇到数据库问题,建议不要着急分库分表。原则是:能不分库分表就不要做。先看下能否通过常规优化手段解决问题。


如上所述,引入分库分表虽然可以解决数据库瓶颈问题,但是也给系统带来巨大的复杂性,不是非必须不要使用。设计系统我们一向要本着高可拓展去设计,但是不要过度设计和超前设计。适合当前系统的设计才是最好的。


作者:爪哇干货分享
来源:juejin.cn/post/7186448714779590711
收起阅读 »

这个面试官真烦,问完合并又问拆分。

你好呀,我是歪歪。 这次来盘个小伙伴分享给我的一个面试题,他说面试的过程中面试官的问了一个比较开放的问题: 请谈谈你对于请求合并和分治的看法。 他觉得自己没有答的特别好,主要是没找到合适的角度来答题,跑来问我怎么看。 我能怎么看? 我也不知道面试官想问啥角...
继续阅读 »

你好呀,我是歪歪。


这次来盘个小伙伴分享给我的一个面试题,他说面试的过程中面试官的问了一个比较开放的问题:



请谈谈你对于请求合并和分治的看法。



他觉得自己没有答的特别好,主要是没找到合适的角度来答题,跑来问我怎么看。


我能怎么看?


我也不知道面试官想问啥角度啊。但是这种开放题,只要回答的不太离谱,应该都能混得过去。


比如,如果你回答一个:我认为合并和分治,这二者之间是辩证的关系,得用辩证的眼光看问题,它们是你中有我,我中有你~


那凉了,拿着简历回家吧。



我也想了一下,如果让我来回答这个问题,我就用这篇文章来回答一下。


有广义上的实际应用场景,也有狭义上的源代码体现对应的思想。


让面试官自己挑吧。



铺垫一下


首先回答之前肯定不能干聊,所以我们铺垫一下,先带入一个场景:热点账户。


什么是热点账户呢?


在第三方支付系统或者银行这类交易机构中,每产生一笔转入或者转出的交易,就需要对交易涉及的账户进行记账操作。


记账粗略的来说涉及到两个部分。



  • 交易系统记录这一笔交易的信息。

  • 账户系统需要增加或减少对应的账户余额。


如果对于某个账户操作非常的频繁,那么当我们对账户余额进行操作的时候,肯定就会涉及到并发处理的问题。


并发了怎么办?


我们可以对账户进行加锁处理嘛。但是这样一来,这个账户就涉及到频繁的加锁解锁操作。


虽然这样我们可以保证数据不出问题,但是随之带来的问题是随着并发的提高,账户系统性能下降。


极少数的账户在短时间内出现了极大量的余额更新请求,这类账户就是热点账户,就是性能瓶颈点。


热点账户是业界的一个非常常见的问题。


而且根据热点账户的特性,也可以分为不同的类型。


如果余额的变动是在频繁的增加,比如头部主播带货,只要一喊 321,上链接,那订单就排山倒海的来了,钱就一笔笔的打到账户里面去了。这种账户,就是非常多的人在给这个账户打款,频率非常高,账户余额一直在增加。


这种账户叫做“加余额频繁的热点账户”。


如果余额的变动是在频繁的减少,比如常见的某流量平台广告位曝光,这种属于扣费场景。


商家先充值一笔钱到平台上,然后平台给商家一顿咔咔曝光,接着账户上的钱就像是流水一样哗啦啦啦的就没了。


这种预充值,然后再扣减频繁的账户,这种账户叫做“减余额频繁的热点账户”。


还有一种,是加余额,减余额都很频繁的账户。


你细细的嗦一下,什么账户一边加一遍减呢,怎么感觉像是个二手贩子在左手倒右手呢?


这种账户一般不能细琢磨,琢磨多了,就有点灰色地带了,点到为止。



先说请求合并


针对“加余额频繁的热点账户”我们就可以采取请求合并的思路。


假设有个歪师傅是个正经的带货主播,在直播间穿着女装卖女装,我只要喊“321,上链接”姐妹们就开始冲了。



随着歪师傅生意越来越好,有的姐妹们就反馈下单越来越慢。


后来一分析,哦,原来是更新账户余额那个地方是个性能瓶颈,大量的单子都在这里排着队,等着更新余额。


怎么办呢?


针对这种情况,我们就可以把多笔调整账务余额的请求合并成一笔处理。



当记录进入缓冲流水记录表之后,我就可以告知姐妹下单成功了,虽然钱还没有真的到我的账户中来,但是你放心,有定时任务保证,钱一会就到账。


所以当姐妹们下单之后,我们只是先记录数据,并不去实际动账户。等着定时任务去触发记账,进行多笔合并一笔的操作。


比如下面的这个示意图:



对于歪师傅来说,实际有 6 个姐妹的支付记录,但是只有一次余额调整。


而我拿着这一次余额调整的账户流水,也是可以追溯到这 6 笔交易记录的详情。


这样的好处是吞吐量上来了,用户体验也好了。但是带来的弊端是余额并不是一个准确的值。


假设我们的定时任务是一小时汇总一次,那么歪师傅在后端看到的交易金额可能是一小时之前的数据。


但是歪师傅觉得没关系,总比姐妹们下不了单强。



如果我们把缓冲流水记录表看作是一个队列。那么这个方案抽象出来就是队列加上定时任务。


所以,请求合并的关键点也是队列加上定时任务。


除了我上面的例子外,比如还有 redis 里面的 mget,数据库里面的批量插入,这玩意不就是一个请求合并的真实场景吗?


比如 redis 把多个 get 合并起来,然后调用 mget。多次请求合并成一次请求,节约的是网络传输时间。


还有真实的案例是转账的场景,有的转账渠道是按次收费的,那么作为第三方公司,我们就可以把用户的请求先放到表里记录着,等一小时之后,一起汇总发起,假设这一小时内发生了 10 次转账,那么 10 次收费就变成了 1 次收费,虽然让客户等的稍微久了点,但还是在可以接受的范围内,这操作节约的就是真金白银了。


请求合并,说穿了,就这么一点事儿,一点也不复杂。


那么如果我在请求合并的前面,加上“高并发”这三个字...



首先不论是在请求合并的前面加上多么狂拽炫酷吊炸天的形容词,说的多么的天花乱坠,它也还是一个请求合并。


那么队列和定时任务的这个基础结构肯定是不会变的。


高并发的情况下,就是请求量非常的大嘛,那我们把定时任务的频率调高一点不就行了?


以前 100ms 内就会过来 50 笔请求,我每收到一笔就是立即处理了。


现在我们把请求先放到队列里面缓存着,然后每 100ms 就执行一次定时任务。


100ms 到了之后,就会有定时任务把这 100ms 内的所有请求取走,统一处理。


同时,我们还可以控制队列的长度,比如只要 50ms 队列的长度就达到了 50,这个时候我也进行合并处理。不需要等待到 100ms 之后。


其实写到这里,高并发的请求合并的答案已经出来了。


关键点就三个:



  • 一是需要借助队列加定时任务实现。

  • 二是控制定时任务的执行时间.

  • 三是控制缓冲队列的任务长度。


方案都想到了,把代码写出来岂不是很容易的事情。而且对于这种面试的场景图,一般都是讨论技术方案,而不太会去讨论具体的代码。


当讨论到具体的代码的时候,要么是对你的方案存疑,想具体的探讨一下落地的可行性。要么就是你答对了,他要准备从代码的交易开始衍生另外的面试题了。


总之,大部分情况下,不会在你给了一个面试官觉得错误的方案之后,他还和你讨论代码细节。你们都不在一个频道了,赶紧换题吧,还聊啥啊。


实在要往代码实现上聊,那么大概率他是在等着你说出一个框架:Hystrix。


其实这题,你要是知道 Hystrix,很容易就能给出一个比较完美的回答。


因为 Hystrix 就有请求合并的功能。


通过一个实际的例子,给大家演示一下。


假设我们有一个学生信息查询接口,调用频率非常的高。对于这个接口我们需要做请求合并处理。


做请求合并,我们至少对应着两个接口,一个是接收单个请求的接口,一个处理把单个请求汇总之后的请求接口。


所以我们需要先提供两个 service:



其中根据指定 id 查询的接口,对应的 Controller 是这样的:



服务启动起来后,我们用线程池结合 CountDownLatch 模拟 20 个并发请求:



从控制台可以看到,瞬间接受到了 20 个请求,执行了 20 次查询 sql:



很明显,这个时候我们就可以做请求合并。每收到 10 次请求,合并为一次处理,结合 Hystrix 代码就是这样的,为了代码的简洁性,我采用的是注解方式:



在上面的图片中,有两个方法,一个是 getUserId,直接返回的是null,因为这个方法体不重要,根本就不会执行。


在 @HystrixCollapser 里面可以看到有一个 batchMethod 的属性,其值是 getUserBatchById。


也就是说这个方法对应的批量处理方法就是 getUserBatchById。当我们请求 getUserById 方法的时候,Hystrix 会通过一定的逻辑,帮我们转发到 getUserBatchById 上。


所以我们调用的还是 getUserById 方法:



同样,我们用线程池结合 CountDownLatch 模拟 20 个并发请求,只是变换了请求地址:



调用之后,神奇的事情就出现了,我们看看日志:



同样是接受到了 20 个请求,但是每 10 个一批,只执行了两个sql语句。


从 20 个 sql 到 2 个 sql,这就是请求合并的威力。请求合并的处理速度甚至比单个处理还快,这也是性能的提升。


那假设我们只有 5 个请求过来,不满足 10 个这个条件呢?


别忘了,我们还有定时任务呢。


在 Hystrix 中,定时任务默认是每 10ms 执行一次:


同时我们可以看到,如果不设置 maxRequestsInBatch,那么默认是 Integer.MAX_VALUE。


也就是说,在 Hystrix 中做请求合并,它更加侧重的是时间方面。


功能演示,其实就这么简单,代码量也不多,有兴趣的朋友可以直接搭个 Demo 跑跑看。看看 Hystrix 的源码。


我这里只是给大家指几个关键点吧。


第一个肯定是我们需要找到方法入口。


你想,我们的 getUserById 方法的方法体里面直接是 return null,也就是说这个方法体是什么根本就不重要,因为不会去执行方法体中的代码。它只需要拦截到方法入参,并缓存起来,然后转发到批量方法中去即可。


然后方法体上面有一个 @HystrixCollapser 注解。


那么其对应的实现方式你能想到什么?


肯定是 AOP 了嘛。


所以,我们拿着这个注解的全路径,进行搜索,啪的一下,很快啊,就能找到方法的入口:



com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect#methodsAnnotatedWithHystrixCommand




在入口处打上断点,就可以开始调试了:



第二个我们看看定时任务是在哪儿进行注册的。


这个就很好找了。我们已经知道默认参数是 10ms 了,只需要顺着链路看一下,哪里的代码调用了其对应的 get 方法即可:



同时,我们可以看到,其定时功能是基于 scheduleAtFixedRate 实现的。


第三个我们看看是怎么控制超过指定数量后,就不等待定时任务执行,而是直接发起汇总操作的:



可以看到,在com.netflix.hystrix.collapser.RequestBatch#offer方法中,当 argumentMap 的 size 大于我们指定的 maxBatchSize 的时候返回了 null。


如果,返回为 null ,那么说明已经不能接受请求了,需要立即处理,代码里面的注释也说的很清楚了:



以上就是三个关键的地方,Hystrix 的源码读起来,需要下点功夫,大家自己研究的时候需要做好心理准备。


最后再贴一个官方的请求合并工作流程图:



再说请求分治


还是回到最开始我们提出的热点账户问题中的“减余额频繁的热点账户”。


请求分治和请求合并,就是针对“热点账户”这同一个问题的完全不同方向的两个回答。


分治,它的思想是拆分。


再说拆分之前,我们先聊一个更熟悉的东西:AtomicLong。


AtomicLong,这玩意是可以实现原子性的增减操作,但是当竞争非常大的时候,被操作的“值”就是一个热点数据,所有线程都要去对其进行争抢,导致并发修改时冲突很大。


那么 AtomicLong 是靠什么解决这个冲突的呢?


看一下它的 getAndAdd 方法:



可以看到这里面还是有一个 do-while 的循环:



里面调用了 compareAndSwapLong 方法。


do-while,就是自旋。


compareAndSwapLong,就是 CAS。


所以 AtomicLong 靠的是自旋 CAS 来解决竞争大的时候的这个冲突。


你看这个场景,是不是和我们开篇提到的热点账户有点类似?


热点账户,在并发大的时候我们可以对账户进行加锁操作,让其他请求进行排队。


而它这里用的是 CAS,一种乐观锁的机制。


但是也还是要排队,不够优雅。


什么是优雅的?


LongAdder 是优雅的。


有点小伙伴就有点疑问了:歪师傅,不是要讲热点账户吗,怎么扯到 LongAdder 上了呢?


闭嘴,往下看就行了。



首先,我们先看一下官网上的介绍:



上面的截图一共两段话,是对 LongAdder 的简介,我给大家翻译并解读一下。



首先第一段:当有多线程竞争的情况下,有个叫做变量集合(set of variables)的东西会动态的增加,以减少竞争。


sum 方法返回的是某个时刻的这些变量的总和。


所以,我们知道了它的返回值,不论是 sum 方法还是 longValue 方法,都是那个时刻的,不是一个准确的值。


意思就是你拿到这个值的那一刻,这个值其实已经变了。


这点是非常重要的,为什么会是这样呢?


我们对比一下 AtomicLong 和 LongAdder 的自增方法就可以知道了:



AtomicLong 的自增是有返回值的,就是一个这次调用之后的准确的值,这是一个原子性的操作。


LongAdder 的自增是没有返回值的,你要获取当前值的时候,只能调用 sum 方法。


你想这个操作:先自增,再获取值,这就不是原子操作了。


所以,当多线程并发调用的时候,sum 方法返回的值必定不是一个准确的值。除非你加锁。


该方法上的说明也是这样的:



至于为什么不能返回一个准确的值,这就是和它的设计相关了,这点放在后面去说。



然后第二段:当在多线程的情况下对一个共享数据进行更新(写)操作,比如实现一些统计信息类的需求,LongAdder 的表现比它的老大哥 AtomicLong 表现的更好。在并发不高的时候,两个类都差不多。但是高并发时 LongAdder 的吞吐量明显高一点,它也占用更多的空间。这是一种空间换时间的思想。


这段话其实是接着第一段话在进行描述的。


因为它在多线程并发情况下,没有一个准确的返回值,所以当你需要根据返回值去搞事情的时候,你就要仔细思考思考,这个返回值你是要精准的,还是大概的统计类的数据就行。


比如说,如果你是用来做序号生成器,所以你需要一个准确的返回值,那么还是用 AtomicLong 更加合适。


如果你是用来做计数器,这种写多读少的场景。比如接口访问次数的统计类需求,不需要时时刻刻的返回一个准确的值,那就上 LongAdder 吧。


总之,AtomicLong 是可以保证每次都有准确值,而 LongAdder 是可以保证最终数据是准确的。高并发的场景下 LongAdder 的写性能比 AtomicLong 高。


接下来探讨三个问题:



  • LongAdder 是怎么解决多线程操作热点 value 导致并发修改冲突很大这个问题的?

  • 为什么高并发场景下 LongAdder 的 sum 方法不能返回一个准确的值?

  • 为什么高并发场景下 LongAdder 的写性能比 AtomicLong 高?


先带你上个图片,看不懂没有关系,先有个大概的印象:



接下来我们就去探索源码,源码之下无秘密。


从源码我们可以看到 add 方法是关键:



里面有 cells 、base 这样的变量,所以在解释 add 方法之前,我们先看一下 这几个成员变量。


这几个变量是 Striped64 里面的。


LongAdder 是 Striped64 的子类:



其中的四个变量如下:




  • NCPU:cpu 的个数,用来决定 cells 数组的大小。

  • cells:一个数组,当不为 null 的时候大小是 2 的次幂。里面放的是 cell 对象。

  • base : 基数值,当没有竞争的时候直接把值累加到 base 里面。还有一个作用就是在 cells 初始化时,由于 cells 只能初始化一次,所以其他竞争初始化操作失败线程会把值累加到 base 里面。

  • cellsBusy:当 cells 在扩容或者初始化的时候的锁标识。


之前,文档里面说的 set of variables 就是这里的 cells。



好了,我们再回到 add 方法里面:



cells 没有被初始化过,说明是第一次调用或者竞争不大,导致 CAS 操作每次都是成功的。


casBase 方法就是进行 CAS 操作。


当由于竞争激烈导致 casBase 方法返回了 false 后,进入 if 分支判断。


这个 if 分子判断有 4 个条件,做了 3 种情况的判断




  • 标号为 ① 的地方是再次判断 cells 数组是否为 null 或者 size 为 0 。as 就是 cells 数组。

  • 标号为 ② 的地方是判断当前线程对 cells 数组大小取模后的值,在 cells 数组里面是否能取到 cell 对象。

  • 标号为 ③ 的地方是对取到的 cell 对象进行 CAS 操作是否能成功。


这三个操作的含义为:当 cells 数组里面有东西,并且通过 getProbe() & m算出来的值,在 cells 数组里面能取到东西(cell)时,就再次对取到的 cell 对象进行 CAS 操作。


如果不满足上面的条件,则进入 longAccumulate 函数。


这个方法主要是对 cells 数组进行操作,你想一个数组它可以有三个状态:未初始化、初始化中、已初始化,所以下面就是对这三种状态的分别处理:




  • 标号为 ① 的地方是 cells 已经初始化过了,那么这个里面可以进行在 cell 里面累加的操作,或者扩容的操作。

  • 标号为 ② 的地方是 cells 没有初始化,也还没有被加锁,那就对 cellsBusy 标识进行 CAS 操作,尝试加锁。加锁成功了就可以在这里面进行一些初始化的事情。

  • 标号为 ③ 的地方是 cells 正在进行初始化,这个时候就在 base 基数上进行 CAS 的累加操作。


上面三步是在一个死循环里面的。


所以如果 cells 还没有进行初始化,由于有锁的标志位,所以就算并发非常大的时候一定只有一个线程去做初始化 cells 的操作,然后对 cells 进行初始化或者扩容的时候,其他线程的值就在 base 上进行累加操作。


上面就是 sum 方法的工作过程。


感受到了吗,其实这就是一个分段操作的思想,不知道你有没有想到 ConcurrentHashMap,也不奇怪,毕竟这两个东西都是 Doug Lea 写的。


总的来说,就是当没有冲突的时候 LongAdder 表现的和 AtomicLong 一样。当有冲突的时候,才是 LongAdder 表现的时候,然后我们再回去看这个图,就能明白怎么回事了:



好了,现在我们回到前面提出的三个问题:



  • LongAdder 是怎么解决多线程操作热点 value 导致并发修改冲突很大这个问题的?

  • 为什么高并发场景下 LongAdder 的 sum 方法不能返回一个准确的值?

  • 为什么高并发场景下 LongAdder 的写性能比 AtomicLong 高?


它们其实是一个问题。


因为 LongAdder 把热点 value 拆分了,放到了各个 cell 里面去操作。这样就相当于把冲突分散到了 cell 里面。所以解决了并发修改冲突很大这个问题。


当发生冲突时 sum= base+cells。高并发的情况下当你获取 sum 的时候,cells 极有可能正在被其他的线程改变。一个在高并发场景下实时变化的值,你要它怎么给你个准确值?


当然,你也可以通过加锁操作拿到当前的一个准确值,但是这种场景你还用啥 LongAdder,是 AtomicLong 不香了吗?


为什么高并发场景下 LongAdder 的写性能比 AtomicLong 高?


你发动你的小脑壳想一想,朋友。


AtomicLong 不管有没有冲突,它写的都是一个共享的 value,有冲突的时候它就在自旋。


LongAdder 没有冲突的时候表现的和 AtomicLong 一样,有冲突的时候就把冲突分散到各个 cell 里面了,冲突分散了,写的当然更快了。


我强调一次:有冲突的时候就把冲突分散到各个 cell 里面了,冲突分散了,写的当然更快了。


你注意这句话里面的“各个 cell”。


这是什么?


这个东西本质上就是 sum 值的一部分。


如果用热点账户去套的话,那么“各个 cell”就是热点账户下的影子账户。


热点账户说到底还是一个单点问题,那么对于单点问题,我们用微服务的思想去解决的话是什么方案?


就是拆分。


假设这个热点账户上有 100w,我设立 10 个影子账户,每个账户 10w ,那么是不是我们的流量就分散了?


从一个账户变成了 10 个账户,压力也就进行了分摊。


但是同时带来的问题也很明显。


比如,获取账户余额的时候需要把所有的影子账户进行汇总操作。但是每个影子账户上的余额是时刻在变化的,所以我们不能保证余额是一个实时准确的值。


但是相比于下单的量来说,大部分商户并不关心“账上的实时余额”这个点。


他只关心上日余额是准确的,每日对账都能对上就行了。


这就是分治。


其实我浅显的觉得分布式、高并发都是基于分治,或者拆分思想的。


本文的 LongAdder 就不说了。


微服务化、分库分表、读写分离......这些东西都是在分治,在拆分,把集中的压力分散开来。


这就算是我对于“请求合并和分治”的理解、


好了,到这里本文就算是结束了。


针对"热点账户"这同一个问题,细化了问题方向,定义了加余额频繁和减余额频繁的两种热点账户,然后给出了两个完全不同方向的回答。


这个时候,我就可以把文章开头的那句话拿出来说了:


综上,我认为合并和分治,这二者之间是辩证的关系,得用辩证的眼光看问题,它们是你中有我,我中有你~



作者:why技术
来源:juejin.cn/post/7292955463954563072
收起阅读 »

工作两年,本地git分支达到了惊人的361个,该怎么快速清理呢?

说在前面 不知道大家平时工作的时候会不会需要经常新建git分支来开发新需求呢?在我这边工作的时候,需求都是以issue的形式来进行开发,每个issue新建一个关联的分支来进行开发,这样可以通过issue看到一个需求完整的开发记录,便于后续需求回顾和需求回退。...
继续阅读 »

说在前面



不知道大家平时工作的时候会不会需要经常新建git分支来开发新需求呢?在我这边工作的时候,需求都是以issue的形式来进行开发,每个issue新建一个关联的分支来进行开发,这样可以通过issue看到一个需求完整的开发记录,便于后续需求回顾和需求回退。而我平时本地分支都不怎么清理,这就导致了我这两年来本地分支的数量达到了惊人的361个,所以便开始写了这个可以批量删除分支的命令行工具。



1697987090644.jpg


功能设计


我们希望可以通过命令行命令的方式来进行交互,快速获取本地分支列表及各分支的最后提交时间和合并状态,在控制台选择我们想要删除的分支。


功能实现


1、命令行交互获取相关参数


这里我们使用@jyeontu/j-inquirer模块来完成命令行交互功能,@jyeontu/j-inquirer模块除了支持inquirer模块的所有交互类型,还扩展了文件选择器文件夹选择器多级选择器交互类型,具体介绍可以查看文档:http://www.npmjs.com/package/@jy…


(1)获取操作分支类型


我们的分支分为本地分支和远程分支,这里我们可以选择我们需要操作的分支类型,选择列表为:"本地分支"、"远程分支"、"本地+远程"


(2)获取远程仓库名(remote)


我们可以输入自己git的远程仓库名,默认为origin


(3)获取生产分支名


我们需要判断各分支是否已经合并到生产分支,所以需要输入自己项目的生产分支名,默认为develop


相关代码


const branchListOptions = [
{
type: "list",
message: "请选择要操作的分支来源:",
name: "branchType",
choices: ["本地分支", "远程分支", "本地+远程"],
},
{
type: "input",
message: "请输入远程仓库名(默认为origin):",
name: "gitRemote",
default: "origin",
},
{
type: "input",
message: "请输入生产分支名(默认为develop):",
name: "devBranch",
default: "develop",
},
];
const res = await doInquirer(branchListOptions);

image.png


2、命令行输出进度条


在分支过多的时候,获取分支信息的时间也会较长,所以我们需要在控制台中打印相关进度,避免用户以为控制台卡死了,如下图:


image.png


image.png


3、git操作


(1)获取git本地分支列表


想要获取当前仓库的所有的本地分支,我们可以使用git branch命令来获取:


function getLocalBranchList() {
const command = "git branch";
const currentBranch = getCurrentBranch();
let branch = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
branch = branch
.split("、")
.filter(
(item) => item !== "" && !item.includes("->") && item !== currentBranch
);
return branch;
}

(2)获取远程仓库分支列表


想要获取当前仓库的所有的远程分支,我们可以使用git ls-remote --heads origin命令来获取,git ls-remote --heads origin 命令将显示远程仓库 origin 中所有分支的引用信息。其中,每一行显示一个引用,包括提交哈希值和引用的全名(格式为 refs/heads/<branch_name>)。


示例输出可能如下所示:


Copy Code
<commit_hash> refs/heads/master
<commit_hash> refs/heads/develop
<commit_hash> refs/heads/feature/xyz

其中,<commit_hash> 是每个分支最新提交的哈希值。


function getRemoteList(gitRemote) {
const command = `git ls-remote --heads ${gitRemote}`;
let branchList = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
branchList = branchList
.split("、")
.filter((item) => item.includes("refs/heads/"))
.map((branch) => {
return gitRemote + "/" + branch.split("refs/heads/")[1];
});
return branchList;
}

(3)获取各分支详细信息


我们想要在每个分支后面显示该分支最后提交时间和是否已经合并到生产分支,这两个信息可以作为我们判断该分支是否要删除的一个参考。



  • 获取分支最后提交时间
    git show -s --format=%ci <branchName> 命令用于查看 指定 分支最新提交的提交时间。其中,--format=%ci 用于指定输出格式为提交时间。


在 Git 中,git show 命令用于显示某次提交的详细信息,包括作者、提交时间、修改内容等。通过使用 -s 参数,我们只显示提交摘要信息,而不显示修改内容。


git show -s --format=%ci develop 命令将显示 develop 分支最新提交的提交时间。输出格式为 ISO 8601 标准的时间戳,例如 2023-10-22 16:41:47 +0800


function getBranchLastCommitTime(branchName) {
try {
const command = `git show -s --format=%ci ${branchName}`;
const result = child_process.execSync(command).toString();
const date = result.split(" ");
return date[0] + " " + date[1];
} catch (err) {
return "未获取到时间";
}
}


  • 判断分支是否合并到生产分支
    git branch --contains <branchName> 命令用于查找包含指定分支(<branchName>)的所有分支。


在 Git 中,git branch 命令用于管理分支。通过使用 --contains 参数,我们可以查找包含指定提交或分支的所有分支。


git branch --contains <branchName> 命令将列出包含 <branchName> 的所有分支。输出结果将显示每个分支的名称以及指定分支是否为当前分支。


示例输出可能如下所示:


Copy Code
develop
* feature/xyz
bugfix/123

其中,* 标记表示当前所在的分支,我们只需要判断输出的分支中是否存在生产分支即可:


function isMergedCheck(branch) {
try {
const command = `git branch --contains ${branch}`;
const result = child_process
.execSync(command)
.toString()
.replace(trimReg, "")
.replace(rowReg, "、");
const mergedList = result.split("、");
return mergedList.includes(gitInfoObj.devBranch)
? `已合并到${gitInfoObj.devBranch}`
: "";
} catch (err) {
return "未获取到合并状态";
}
}

(4)删除选中分支


选完分支后我们就该来删除分支了,删除分支的命令大家应该就比较熟悉了吧



  • git branch -D <branchName>


git branch -D <branchName> 命令用于强制删除指定的分支(<branchName>)。该命令会删除本地仓库中的指定分支,无法恢复已删除的分支。



  • git push <remote> :<branchName>


git push <remote> :<branchName> 命令用于删除远程仓库<remote>中的指定分支(<branchName>)。这个命令通过推送一个空分支到远程仓库的 <branchName> 分支来实现删除操作。


async function doDeleteBranch(branchList) {
const deleteBranchList = await getDeleteBranch(branchList);
if (!deleteBranchList) return;
console.log("正在删除分支");
progressBar.run(0);
deleteBranchList.forEach((branch, index) => {
let command = `git branch -D ${branch}`;
if (branch.includes("/")) {
const tmp = branch.split("/");
command = `git push ${tmp[0]} :${tmp[1]}`;
}
child_process.execSync(command);
progressBar.run(Math.floor(((index + 1) / deleteBranchList.length) * 100));
});
console.log("");
console.log("已删除分支:" + deleteBranchList);
}

image.png


1697995985140.png


1697996057503.jpg


可以看到我们的分支瞬间就清爽了很多。


使用


该工具已经发布到 npm 上,可以直接通过命令npm i -g jyeontu进行安装,安装完后在控制台中输入jyeontu git即可进行操作。


源码


该工具的源码也已经开源,有兴趣的同学可以到Gitee上查看:Gitee地址


说在后面



🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。



作者:JYeontu
来源:juejin.cn/post/7292635075304964123
收起阅读 »

服务:简聊微内核结构

1 简介:微内核架构 微内核架构是指内核的一种精简形式,将通常与内核集成在一起的系统服务层被分离出来,变成可以根据需求加入选,达到系统的可扩展性、更好地适应环境要求。 微内核:内核管理着所有的系统资源,在微内核中用户服务和内核服务在不同的地址空间中实现。 该结...
继续阅读 »

1 简介:微内核架构


微内核架构是指内核的一种精简形式,将通常与内核集成在一起的系统服务层被分离出来,变成可以根据需求加入选,达到系统的可扩展性、更好地适应环境要求。


微内核:内核管理着所有的系统资源,在微内核中用户服务和内核服务在不同的地址空间中实现。


该结构是向最初并非设计为支持它的系统添加特定功能的最佳方式。


此体系结构消除了对应用程序可以具有的功能数量的限制。我们可以添加无限的插件(例如Chrome浏览器有数百个插件,称为扩展程序)


2 一个简单例子


微内核架构(也称为插件结构)通常用于实现可做为第三方产品下载的应用程序。此结构在内部业务程序很常见。


实际上,它可以被嵌入到其他模式中,例如分层体系中。典型的微内核架构有两个组件:核心系统和插件模块


	plug-in                  plug-in
core system
plug-in plug-in

漂亮一点的图


new_微内核架构.png


由上图可知,微内核架构也被称为插件架构模式(Plug-inArchitecture Patterm),通常由内核系统和插件组成的原因。


核心系统包括使系统正确运行的最小业务逻辑。可以通过连接插件组件添加更多功能,扩展软件功能。就像为汽车添加涡轮以提高动力。


轮圈37.png


插件组件可以使用开放服务网关计划(OSGi),消息传递,Web服务或对象实例化进行连接。
需要注意的是,插件组件是独立的组件,是为扩展或增强核心系统的功能,不应与其他组件形成依赖。


常见的系统结构使用微内核的如:嵌入式Linux、L4、WinCE。



  • 优缺点说明


微服务在应用程序和硬件的通信中,内核进程和内存管理的极小的服务,而客户端程序和运行在用户空间的服务通过消息的传递来建立通信,它们之间不会有直接的交互。


这样微内核中的执行速度相对就比较慢了,性能偏低这是微内核架构的一个缺点。


微内核系统结构相当清晰,有利于协作开发;微内核有良好的移植性,代码量非常少;微内核有相当好的伸缩性、扩展性。


3 小结


(1)微内核架构难以进行良好的整体化优化。

由于微内核系统的核心态只实现了最基本的
系统操作,这样内核以外的外部程序之间的独立运行使得系统难以进行良好的整体优化。


(2)微内核系统的进程间通信开销也较单一内核系统要大得多。

从整体上看,在当前硬件条件下,微内核在效率上的损失小于其在结构上获得的收益。


(3)通信损失率高。

微内核把系统分为各个小的功能块,从而降低了设计难度,系统的维护与修改也容易,但通信带来的效率损失是一个问题。


作者:楽码
来源:juejin.cn/post/7291468863396708413
收起阅读 »

如何在Java项目中实现漂亮的日志输出

  日志是开发过程中不可或缺的一部分,它可以帮助我们追踪代码的执行过程、排查问题以及监控系统运行状况。然而,大多数开发人员在编写日志时往往只关注于输出必要的信息,而忽略了日志的可读性和美观性。本文将介绍如何在Java项目中实现漂亮的日志输出,提供一些实用的技巧...
继续阅读 »

  日志是开发过程中不可或缺的一部分,它可以帮助我们追踪代码的执行过程、排查问题以及监控系统运行状况。然而,大多数开发人员在编写日志时往往只关注于输出必要的信息,而忽略了日志的可读性和美观性。本文将介绍如何在Java项目中实现漂亮的日志输出,提供一些实用的技巧和建议。



image.png



1. 使用合适的日志框架



  Java有许多优秀的日志框架可供选择,如Log4j、Logback和java.util.logging等。选择一个适合你项目需求的日志框架是实现漂亮日志输出的第一步。这些框架提供了丰富的配置选项,可以帮助你控制日志的格式和输出方式。这里对几个日志框架做一下简单的介绍。


Log4j


  Log4j是一个Java日志处理的框架,用于在Java应用程序中处理日志记录。它提供了一种灵活的方式来记录日志信息,并允许开发者根据需要配置日志输出的格式和目标。


  在Log4j中,主要有三个组件:Logger、Appender和Layout。Logger用于记录日志信息,Appender用于定义日志的输出目标,例如控制台、文件、数据库等,Layout用于定义日志的输出格式。


  以下是一个简单的Log4j代码示例:


import org.apache.log4j.Logger;  

public class MyApp {
// 获取Logger实例
final static Logger logger = Logger.getLogger(MyApp.class);

public static void main(String[] args) {
// 记录不同级别的日志信息
logger.debug("Debugging information");
logger.info("Informational message");
logger.warn("Warning");
logger.error("Error occurred");
logger.fatal("Fatal error occurred");
}
}

  在这个示例中,我们首先导入了Logger类,然后通过Logger.getLogger(MyApp.class)获取了一个Logger实例。在main方法中,我们使用Logger实例记录了不同级别的日志信息,包括Debug、Info、Warn、Error和Fatal。


Logback


  Logback是Log4j的改进版本,是SLF4J(Simple Logging Facade for Java)下的一种日志实现。与Log4j相比,Logback具有更高的性能和更灵活的配置。


  Logback的组件包括Logger、Appender、Encoder、Layout和Filter,其中Logger是最常用的组件。Logger分为rootLogger和nestedLogger,rootLogger是所有Logger的根,nestedLogger则是rootLogger的子级。Logger之间有五个级别,从高到低依次为ERROR、WARN、INFO、DEBUG和TRACE,级别越高,日志信息越重要。


  以下是一个简单的Logback代码示例:


import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;

public class MyClass {
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
public void myMethod() {
logger.debug("Debug message");
logger.info("Info message");
logger.warn("Warn message");
logger.error("Error message");
}
}

logging


  java.util.logging是Java平台的核心日志工具。


  java.util.logging由Logger、Handler、Filter、Formatter等类和接口组成。其中Logger是日志记录器,用于记录日志信息;Handler是处理器,用于处理日志信息;Filter是过滤器,用于过滤不需要记录的日志信息;Formatter是格式化器,用于格式化日志信息。


  这里介绍的日志框架,在项目当中运用的比较多的是Log4j、Logback,从基本的配置上没有太大的差异,大家也可以根据项目需求选择使用。



2. 定义清晰的日志级别



  在Java项目中,定义清晰的日志级别是非常重要的,以便在调试、监控和解决潜在问题时有效地记录和理解系统行为。下面是一些建议,可以帮助你定义清晰的日志级别:



  1. 了解常见的日志级别:Java中常见的日志级别包括DEBUG、INFO、WARN、ERROR和FATAL。每个级别都有特定的含义和用途,首先要了解这些级别的含义。

  2. 根据项目需求确定日志级别:在定义日志级别时,需要考虑项目的需求和目标。例如,对于一个简单的演示应用程序,可能不需要记录过多的调试信息。但对于一个复杂的业务系统,可能需要详细的调试信息来跟踪和解决潜在的问题。根据项目的重要性和规模来确定每个级别的日志信息是否必要。

  3. 默认级别设置:为项目设置一个默认的日志级别。这通常是INFO级别,用于记录系统的常规操作信息。

  4. 根据模块或功能设置日志级别:为每个模块或功能设置不同的日志级别。这有助于在特定部分出现问题时快速定位问题原因。例如,对于数据库模块,可以将其日志级别设置为DEBUG,以便记录详细的数据库操作信息。

  5. 日志级别继承:在一个日志级别下定义的日志信息,应该继承到其所有子级别中。这意味着,如果某个日志信息被设置为WARN级别,那么该信息应该同时出现在WARN、ERROR和FATAL日志中。

  6. 日志信息清晰明了:在记录日志信息时,要确保信息清晰明了,包含必要的细节。例如,对于错误信息,要包含错误类型、发生错误的方法和时间戳等信息。

  7. 日志轮转和清理:及时对日志进行轮转和清理,避免日志文件过大而影响系统性能。可以设置一个合适的大小限制或时间间隔,对旧的日志文件进行归档和清理。

  8. 培训开发人员:为开发人员提供关于如何使用日志系统的培训,确保他们了解如何记录适当的日志信息以及如何利用日志级别进行过滤。

  9. 参考最佳实践:可以参考一些关于日志编写的最佳实践指南,例如Log4j的官方文档,以获取更多关于如何定义清晰日志级别的建议。


  定义清晰的日志级别对于Java项目来说非常重要。通过了解常见的日志级别、根据项目需求确定级别、设置默认级别、按模块或功能划分级别、继承级别、记录清晰明了的日志信息、及时轮转和清理以及培训开发人员等措施,可以帮助你在项目中实现定义清晰、易于理解和使用的日志级别。



3. 格式化日志输出



  下面以Log4j为例,介绍如何格式化日志输出。


1,引入Log4j依赖


  在Maven项目中,可以在pom.xml文件中添加以下依赖:


<dependency>  
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.x.x</version>
</dependency>

2. 配置日志格式


  在log4j2.xml配置文件中,可以使用PatternLayout类来配置日志格式。例如,以下配置将日志输出为每行包含时间戳、日志级别、线程名和消息的格式:


<?xml version="1.0" encoding="UTF-8"?>  
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

  其中,%d表示时间戳,%t表示线程名,%-5level表示日志级别(使用五个字符的宽度),%logger{36}表示最长为36个字符的Logger名称,%msg表示消息。在配置文件中可以根据需要调整格式。


3. 在代码中使用Log4j记录日志


在Java代码中,可以使用以下语句记录日志:


import org.apache.logging.log4j.LogManager;  
import org.apache.logging.log4j.Logger;

public class MyClass {
private static final Logger logger = LogManager.getLogger(MyClass.class);

public static void main(String[] args) {
logger.debug("Debug message");
logger.info("Info message");
logger.warn("Warn message");
logger.error("Error message");
}
}

  在输出结果中,可以看到每条日志信息都符合之前配置的格式。可以使用不同的配置文件来调整日志格式,以满足不同的需求。



4. 日志轮转和切割



  志切割和轮转在Log4j中主要通过两种策略实现:基于大小(Size-based)和基于日期时间(Time-based)。


1. 基于大小的日志切割和轮转


  这种策略是当日志文件达到指定大小时,会进行切割或轮转。例如,你可以设置当日志文件达到100MB时进行轮转。


<RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}.log.gz">  
<PatternLayout>
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingFile>

  在上述配置中,当app.log文件达到100MB时,它会被切割并存储为app-yyyy-MM-dd.log.gz。并且最多保留20个这样的文件。


2. 基于日期时间的日志切割和轮转


  这种策略是当达到指定的日期时间时,进行日志切割或轮转。例如,你可以设置每天凌晨1点进行轮转。


<RollingFile name="File" fileName="logs/app.log" filePattern="logs/app-%d{yyyy-MM-dd}.log.gz">  
<PatternLayout>
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>

  在上述配置中,每天凌晨1点,app.log文件会被切割并存储为app-yyyy-MM-dd.log.gz。并且最多保留30个这样的文件。


  注意:<DefaultRolloverStrategy max="20"/> 或 <TimeBasedTriggeringPolicy interval="1"/> 中的数字可以根据你的实际需要进行调整。



5. 日志过滤器(Filter)的使用



  Log4j中的过滤器(Filter)用于在日志事件发生之前对其进行一些条件判断,以决定是否接受该事件或者更改该事件。这可以让你根据特定的条件过滤日志输出,例如只打印错误级别以上的日志,或者根据线程ID、请求ID等过滤日志。


  在Log4j 2中,你可以通过配置文件(例如log4j2.xml)来为日志事件指定过滤器。以下是一个使用Log4j 2的XML配置文件中的过滤器的示例:


<?xml version="1.0" encoding="UTF-8"?>  
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
<Filters>
<ThresholdFilter level="ERROR"/>
<MarkerFilter marker="FLOW" onMatch="DENY"/>
<MarkerFilter marker="EXCEPTION" onMatch="DENY"/>
</Filters>
</Console>
</Appenders>
<Loggers>
<Root level="all">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

  在这个例子中,我们使用了三个过滤器:


  (1). ThresholdFilter只接受级别为ERROR或更高级别的日志事件。


  (2). 第一个MarkerFilter会拒绝任何带有"FLOW"标记的日志事件。


  (3). 第二个MarkerFilter会拒绝任何带有"EXCEPTION"标记的日志事件。


  另外,你还可以创建自定义的过滤器,只需实现org.apache.logging.log4j.core.filter.Filter接口即可。然后你可以在配置文件中通过指定类全名来使用你的过滤器。


  对于更复杂的过滤需求,你还可以使用Condition元素,它允许你使用Java代码来决定是否接受一个日志事件。不过,请注意,因为这可能会影响性能,所以应谨慎使用。


  下面是实际项目中打印的日志,大家可以根据项目的需求满足日志打印的需求。


image.png



总结



  通过选择合适的日志框架、定义清晰的日志级别、格式化日志输出、添加时间戳和线程信息、使用日志分级以及处理异常和堆栈跟踪,我们可以实现在Java项目中打印漂亮的日志。漂亮的日志输出不仅可以提高代码的可读性,还可以帮助我们更好地理解和跟踪代码的执行过程,从而提高开发效率和系统稳定性。


拓展阅读


# 35岁愿你我皆向阳而生


# 深入解读Docker的Union File System技术


# 说一说注解@Autowired @Resource @Reference使用场景


# 编写Dockerfile和构建自定义镜像的步骤与技巧


# 说一说Spring中的单例模式


# MySQL的EXPLAIN用法


# Spring的Transactional: 处理事务的强大工具


作者:mikezhu
来源:juejin.cn/post/7291675889381031990
收起阅读 »

HashMap扩容机制跟你的工作真的不相关吗?

说来话长,这事还得从我第一份工作说起,那时候纯纯大菜鸡一个,啥也不会,工作中如履薄冰,举步维艰,满屏荒唐码,一把辛酸泪😭 再说句题外话,如果你是中高级程序员,建议您划走离开,否则这篇文章可能会浪费您的宝贵时间☺️ 那年 OK,书归正传,得益于本人工作态度良好...
继续阅读 »

说来话长,这事还得从我第一份工作说起,那时候纯纯大菜鸡一个,啥也不会,工作中如履薄冰,举步维艰,满屏荒唐码,一把辛酸泪😭



再说句题外话,如果你是中高级程序员,建议您划走离开,否则这篇文章可能会浪费您的宝贵时间☺️


那年


OK,书归正传,得益于本人工作态度良好,同事和领导都给予了我很大的帮助,只记得那是18年的平常打工人的一天,我写了如下很多打工人都会写,甚至每天都在写的代码(当时的具体代码已经记不清了,现在大概模拟一下案发场景):


    /**
* 从Order对象中获取id属性并包装成List返回
*
* @param orderList Student列表
* @return idList
*/

public List<Long> getOrderIds(List<Order> orderList) {
List<Long> ids = new ArrayList<>();
for (Order order : orderList) {
ids.add(order.getId());
}
return ids;
}


对没错,用Stream流可以一行代码解决这个问题,但当时受限于我们使用的JDK还是1.6和1.7,你懂得



我的直属领导看了我的代码后首先问我,你知道ArrayList初始化容量是多少吗?他是怎么扩容的?


我:。。。。。
img


这俩问题对现在的程序员来说兼职就是小菜一碟,不值一提,但对当时的我来说,可就有亿点难度了,之前面试之前依稀在那个博客上看别人写过,于是乎我就照着脑袋里模糊不清的知识点模棱两可的回答了这俩问题,emmm,


于是乎我领导就跟我说,既然你知道List容量不够会扩容,扩容会带来性能损耗(这个日后再细说,先说正事)那么你应该这么写,来避免它扩容呢?


    public List<Long> getOrderIds(List<Order> orderList) {
List<Long> ids = new ArrayList<>(orderList.size());
for (Order order : orderList) {
ids.add(order.getId());
}
return ids;
}


千万不要小看这些细节哦



听君一席话,如听一席话,于是我悟了,


从那以后再有类似集合初始化的场景,明确知道容量的场景我都会初始化的时候传入构造参数,避免其扩容,无法知道确切容量的时候也会预估一下容量 尽可能的避免减少扩容次数。


去年


时间来到2022年,去年,我已经不是当年的那个懵懵懂懂愣头青了,坐我旁边的一个哥们(技术比我当年强多了去了),他写了一段初始化HashMap的代码也传入了一个初始容量,代码如下:


    public Map<Long, Order> xxx(List<Order> orderList) {
Map<Long, Order> orderMap = new HashMap<>(orderList.size());
for (Order order : orderList) {
orderMap.put(order.getId(), order);
}
return orderMap;
}

img


敲黑板,重点来了,前面铺垫了那么多,就是为了说这事


历史惊奇在这一天重演,只不过负责问问题的是我


img


Q: 咳咳~HashMap的初始容量是16,放第几个个元素的时候会触发扩容呢(这题简单)


A: 元素个数超过16x0.75=12的时候进行扩容呗,扩容为16x2=32


Q: 既然容量为16,只能存12个元素,超过就会扩容,那么你写的new HashMap<>(orderList.size()) 这个能防止扩容吗?


A: emmm,不能


Q: 那初始化容量应该设置多少呢?


A: ……


Q: 16x0.75=12这个计算公式中, 初始容量变成未知假设为N 需存放的元素个数为20 Nx0.75=20N 是多少?(这大概就是经典的大学数学题吧)


A: 20➗0.75呗, 26.666 四舍五入27个, 设置容量为27,可以存放20个元素并且不触发扩容


img


所以正确的代码应该这么写: new HashMap<>((int) (orderList.size / 0.75 + 1))


别问为啥要+1,问就是因为小数转成int不会四舍五入直接舍弃小数点后的部分


一次轻松的对话就此结束


来看下大佬们是怎么写的


google的guava包 这是一个非常常用的java开发工具包,我从里面真的学到了很多(后续单独开篇文章记录一下)


//入口
HashMap<String, String> map= Maps.newHashMapWithExpectedSize(20);

public static <K extends @Nullable Object, V extends @Nullable Object>
HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
return new HashMap<>(capacity(expectedSize));
}

// 看这里看这里, 还考虑了一些其他的情况,专业!
static int capacity(int expectedSize) {
if (expectedSize < 3) {
checkNonnegative(expectedSize, "expectedSize");
return expectedSize + 1;
}
if (expectedSize < Ints.MAX_POWER_OF_TWO) {
// This is the calculation used in JDK8 to resize when a putAll
// happens; it seems to be the most conservative calculation we
// can make. 0.75 is the default load factor.
return (int) ((float) expectedSize / 0.75F + 1.0F);
}
return Integer.MAX_VALUE; // any large value
}

大佬写的代码就是专业! img


org.apache.curator包 无意之间发现的,实现有点意思


//这里写法一样
HashMap<String, String> map = Maps.newHashMapWithExpectedSize(20);


public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
return new HashMap(capacity(expectedSize));
}

//看这里 看这里 expectedSize + expectedSize / 3
static int capacity(int expectedSize) {
if (expectedSize < 3) {
CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");
return expectedSize + 1;
} else {
return expectedSize < 1073741824 ? expectedSize + expectedSize / 3 : Integer.MAX_VALUE;
}
}

expectedSize + expectedSize / 3 说实话第一次看到这段代码的时候还是有点懵的,wocc这是啥写法,后来用几个数值带入计算了一下,还真是那么回事 👍🏻👍🏻


Hutool工具包


//入口
HashMap<String, String> map = MapUtil.newHashMap(20);

public static <K, V> HashMap<K, V> newHashMap(int size) {
return newHashMap(size, false);
}

//看这里, 平平无奇,什么档次?代码跟我写的一样,😄
public static <K, V> HashMap<K, V> newHashMap(int size, boolean isOrder) {
int initialCapacity = (int)((float)size / 0.75F) + 1;
return (HashMap)(isOrder ? new LinkedHashMap(initialCapacity) : new HashMap(initialCapacity));
}


说实话这个实现相较前者来说就显得不那么细了,居然跟我写的一样。。。


image-20231019160132153

这件事情带来的思考


说起HashMap的知识点,晚上的文章博客简直满天飞,大家现在谁还不能说上几句,但是! 后来在我面试的很多初中级开发时,我问他们准备往Map中存放20个元素,初始化容量设置多少不会触发扩容 时,基本上很少有人能答上来,10个人当中差不多有一个能回答上来?为什么会这样呢? 明明这些人是懂的初始容量16,超过出初始容量的75%会触发扩容,反过来问一下就不会了~😒 这充分说明了,学习要融会贯通举一反三,要细!!!


那段代码现在怎么写


据说JDK都出到21了,最近没怎么关注过~
不过JDK8已经流行很久了,那段代码用JDK8应该这么写:



  • list


public List<Long> getOrderIds(List<Order> orderList) {
List<Long> ids = new ArrayList<>(orderList.size());
for (Order order : orderList) {
ids.add(order.getId());
}
return ids;
}

//一行代码搞定,简洁明了
public List<Long> getOrderIds(List<Order> orderList) {
return orderList.stream().map(Order::getId).collect(Collectors.toList());
}

通过StreamCollectors.toList()来返回一个崭新的List,难道就没人好奇他这个List创建的时候有没有指定容量呢?如过不指定,在上面说到的那些明确知道存放容量的场景里岂不是要白白的扩容耗费性能???


答案是:NO 我们看下来Collectors.toList()的实现


public static <T>
Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}

static class CollectorImpl<T, A, R> implements Collector<T, A, R> {
private final Supplier<A> supplier;
private final BiConsumer<A, T> accumulator;
private final BinaryOperator<A> combiner;
private final Function<A, R> finisher;
private final Set<Characteristics> characteristics;

CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Function<A,R> finisher,
Set<Characteristics> characteristics) {
this.supplier = supplier;
this.accumulator = accumulator;
this.combiner = combiner;
this.finisher = finisher;
this.characteristics = characteristics;
}

CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Set<Characteristics> characteristics) {
this(supplier, accumulator, combiner, castingIdentity(), characteristics);
}

CollectorImplCollectors中的一个内部类,构造函数的第一个参数是Supplier<A> supplier这是一个函数式接口,就是说你得传给我一个实现,告诉我应该如何去创建一个集合,上面是这么传参的ArrayList::new, 这个写法其实就是new ArrayList(), 看到没!他并没有指定集合容量哦~~~


那么如果想提前指定好集合容量应该怎么写呢? 不卖关子了,直接贴代码了,写个B博客,真TM累死个人😌


public List<Long> getOrderIds(List<Order> orderList) {
return orderList.stream().map(Order::getId).collect(Collectors.toCollection(() -> new ArrayList<>(orderList.size())));
}

这就行了,看下Collectors.toCollection()的源码


public static <T, C extends Collection<T>>
Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
return new CollectorImpl<>(collectionFactory, Collection<T>::add,
(r1, r2) -> { r1.addAll(r2); return r1; },
CH_ID);
}

这和Collectors.toList()基本上市一样的,只不过Collectors.toCollection()把如何创建集合的这个步骤抽象起来叫给我们开发者来个性化实现了,是不是又学到了一招~~~(#^.^#)



  • map


public Map<Long, Order> xxx(List<Order> orderList) {
Map<Long, Order> orderMap = new HashMap<>(orderList.size());
for (Order order : orderList) {
orderMap.put(order.getId(), order);
}
return orderMap;
}


//这点破代码用Stream也是分分钟搞定
public Map<Long, Order> xxx(List<Order> orderList) {
return orderList.stream().collect(Collectors.toMap(Order::getId, Function.identity(), (k1,k2) -> k1));
}

和上面的List一样,这玩意初始化Map的时候也没有指定容量


public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

Map的创建也是通过一个函数式接口Supplier<M> mapSupplier定义的,传的参数是 HashMap::new,这也是一个方法引用,写法等于 new hashMap(), 想指定容量怎么办呢? 看代码


    public Map<Long, Order> xxx(List<Order> orderList) {
return orderList.stream()
.collect(Collectors.toMap(Order::getId, Function.identity(), (k1,k2) -> k1,
() -> new HashMap<>((int) (orderList.size() / 0.75 + 1))));
}

这个写法我们同样自己掌控如何创建需要的Map,容量自己定~


写在末尾


没啥要写的了,就这吧,累挺
img


作者:码塞顿开
来源:juejin.cn/post/7291828982558425142
收起阅读 »

01.你为什么需要学习K8S

前言 在"云原生"、"应用上云"火热的这几年,相信大家或多或少都听说过K8S这个可以称得上是容器编排领域事实的存在。 可以看出在2017年之后,K8S热度远超容器编排领域的另外两个工具Mesos和Docker Swarm,并将它们甩开了几十条街,成为了整个容...
继续阅读 »

前言



在"云原生"、"应用上云"火热的这几年,相信大家或多或少都听说过K8S这个可以称得上是容器编排领域事实的存在。


可以看出在2017年之后,K8S热度远超容器编排领域的另外两个工具MesosDocker Swarm,并将它们甩开了几十条街,成为了整个容器编排领域的龙头。



随着现在越来越多的企业把业务系统上云之后,大部分的服务都运行在Kubernetes环境中,可以说Kubernetes已经成为了云、运维和容器行业最炙手可热的工具,这也是为什么需要学习Kubernetes最重要的原因。


目前,AWS、Azure、Google、阿里云、腾讯云等主流公有云提供的是基于Kubernetes的容器服务。Rancher、CoreOS、IBM、Mirantis、Oracle、Red Hat、VMWare等无数厂商也在大力研发和推广基于Kubernetes的PaaS产品。


目前国内容器服务平台做的比较好的有腾讯云容器服务TKE阿里云容器服务ACK,它们都是基于K8S做的二开,有兴趣的读者可以自己了解和尝试使用。


K8S是什么?


K8S是单词Kubernetes的缩写,这个单词在古希腊语中是 [舵手] 的意思,之所以简称其为K8S,是因为'K'字母与'S'字母之间隔着八个单词,为了简便称呼,于是有了K8S这个简称。


K8S起初是Google内部的一个名为Borg的系统,据说Google有超过二十亿的容器运行在Borg上,在积累了十几年的经验之后,Google在2014年重写并开源了该项目,改名为Kubernetes


K8S在基于容器部署的方式上,提供了一个弹性分布式的框架,支持服务发现与负载均衡、存储、自动部署回滚、自动计算与调度、自动扩缩容等等一系列操作,目的是方便开发者不再需要关注服务运行细节,K8S能够自动进行容器与Pod调度、扩缩容、自动重建等等操作,保证服务尽可能健康的运行。


一句话来概括:K8S解放了开发者的双手,能够最大程度的让部署的服务健康运行,同时能够接入很多第三方工具(如服务监控、数据采集等等),满足开发者的定制化需求。


部署演变之路



传统部署时代


在互联网开发早期,开发者会在物理服务器上直接运行应用程序。以一个Go Web程序举例,很典型的一个部署方式是首先在本地编译好对应的二进制文件,之后上传到服务器,然后运行应用。


由于无法限制在物理服务器中运行的应用程序资源使用,因此会导致资源分配问题。例如,如果在同一台物理服务器上运行多个应用程序,则可能会出现一个应用程序占用大部分资源的情况,从而导致其他应用程序的性能下降。


虚拟化部署时代


为了解决上述问题,虚拟化技术被引入了。虚拟化技术允许你在单个物理服务器上运行多个虚拟机(VM)。虚拟化能够使应用程序在不同VM之间被彼此隔离,且能提高一定的安全性,因为一个应用程序的信息不能被另一应用程序随意访问。


虚拟化能够更好地利用物理服务器的资源,并且因为可以轻松地添加或者更新应用程序,而因此可以具有更高的扩缩容性,以及降低硬件成本等等的好处。通过虚拟化,可以将一组物力资源呈现为可丢弃的虚拟机集群。每个VM是一台完整的计算机,在虚拟化硬件之上运行所有的组件,包括自身的操作系统Guest OS


容器部署时代


容器类似于VM,但是具有更轻松的隔离特性,使得容器之间可以共享操作系统Host OS,并且容器不会像VM那样虚拟化硬件,例如打印机等等,只是提供一个服务的运行环境。



通常一台物理机只能运行十几或者数十个VM,但是可以启动成千上万的容器。因此,容器和VM比起来是更加轻量级的,且具有和VM一样的特性:每个容器都具有自己的文件系统、CPU、内存、进程空间等。


我们可以简单理解为:一个VM已经是一台完整的计算机了,而容器只是提供了一个服务能够运行的所有环境。


同时,因为容器与基础架构分离,因此可以跨云和OS发行版本进行移植。


容器部署具有以下优势



  • 敏捷部署:比起VM镜像,提高了容器镜像创建的简便性和效率。

  • DEVOPS:由于镜像的不可变性,可以通过快速简单的回滚,提供可靠并且频繁的容器镜像构建和部署。

  • 开发与运维的隔离:在构建、发布的时候创建应用程序容器镜像,而不是在部署的时候,从而将应用程序和基础架构分离。

  • 松耦合:符合微服务架构思想,应用程序被分解成一个个小服务运行在不同的容器中,可以动态部署和管理。

  • 软件/硬件层面隔离:通过namespace实现操作系统层面的隔离,如隔离不同容器之间的文件系统、进程系统等等;通过cgroup实现硬件层面的隔离,提供物理资源上的隔离,避免某些容器占用过多的物理资源CPU、Memory、IO影响到其他容器中的服务质量。


容器时代之后:Serveless


容器阶段之后,虚拟化仍然还在不断演化和衍生,产生了Serveless这个概念。


Serveless英文直译过来的意思是无服务器,这不代表着它真的不需要服务器,而是说服务器对用户不可见了,服务器的维护、管理、资源分配等操作由平台开发商自行维护。一个Serveless很经典的实现就是云函数,即最近火热的FAAS(Function As A Service),函数即服务。


Serveless并不是一个框架或者工具,它本质上是一种软件架构思想,即:用户无需关注应用服务运行的底层资源,比如CPU、Memory、IO的状况,只需要关注自身的业务开发。


Serveless具有以下特点



  • 无穷弹性计算能力:服务应该做到根据请求数量自动水平扩容实例,并且平台开发商应该提供无限的扩容能力。

  • 无需服务器:不需要申请和运维服务器。

  • 开箱即用:无需做任何适配,用户只需要关注自身业务开发,并且能够做到精确的按量计费。


强大的K8S


想像一个场景,假设我们现在把一个微服务架构的程序部署在成百上千个容器上,这些容器分部在不同的机器上,这个时候管理这些容器是一件非常让人头疼的事情。


让我们想想管理这些容器可能会碰到的问题,例如:



  1. 某个容器发生故障,这个时候我们是不是该启动另一个容器?

  2. 某台机器负载过高,那么我们之后的容器是不是不能部署在这台机器上?

  3. 某个服务请求量突增,我们是不是应该多部署几个运行该服务的容器?

  4. 如果某些容器之间需要相互配合怎么办?比如容器A需要容器B的资源,所以容器A一定要在容器B之后运行。

  5. 运行多个容器时,我怎么做到它们的运行结果是原子性的?即要么全部成功,或者全部失败。亦或者如果某一个容器失败,我能够不断重启这个容器以达到我的预期状态。


以上问题,都可以交给K8S来解决,它提供了一系列的功能来帮助我们轻松管理和编排容器,以达到我们的预期状态,同时因为它本身也是一个分布式高可用的组件,所以无需担心K8S出问题。


K8S官方文档这么描述它的功能:



  • 服务发现和负载均衡 Kubernetes 可以使用 DNS 名称或自己的 IP 地址来暴露容器。 如果进入容器的流量很大, Kubernetes 可以负载均衡并分配网络流量,从而使部署稳定。

  • 存储编排 Kubernetes 允许你自动挂载你选择的存储系统,例如本地存储、公共云提供商等。

  • 自动部署和回滚 你可以使用 Kubernetes 描述已部署容器的所需状态, 它可以以受控的速率将实际状态更改为期望状态。 例如,你可以自动化 Kubernetes 来为你的部署创建新容器, 删除现有容器并将它们的所有资源用于新容器。

  • 自动完成装箱计算 你为 Kubernetes 提供许多节点组成的集群,在这个集群上运行容器化的任务。 你告诉 Kubernetes 每个容器需要多少 CPU 和内存 (RAM)。 Kubernetes 可以将这些容器按实际情况调度到你的节点上,以最佳方式利用你的资源。

  • 自我修复 Kubernetes 将重新启动失败的容器、替换容器、杀死不响应用户定义的运行状况检查的容器, 并且在准备好服务之前不将其通告给客户端。

  • 密钥与配置管理 Kubernetes 允许你存储和管理敏感信息,例如密码、OAuth 令牌和 SSH 密钥。 你可以在不重建容器镜像的情况下部署和更新密钥和应用程序配置,也无需在堆栈配置中暴露密钥


什么人需要学习K8S


运维/运开工程师


随着部署模式的演变,现在企业的应用几乎都以容器的方式在开发、测试、生产环境中运行。掌握基于K8S的容器编排工具的运维、开发能力将成为运维/运开工程师的核心竞争力。


软件开发人员


随着开发模式的演变,基于容器的微服务架构已经成为了开发应用首选的架构,而K8S是运行微服务应用的理想平台,市场会需要一批掌握K8S的软件开发人员。


GO开发人员


GO高级开发基本只有两个方向:高级服务端开发工程师和云原生工程师,其中云原生岗位会比高级服务端开发工程师更多。


这里的云原生主要是做DockerPrometheusKubernetes等云原生工具方向等等开发,这也是因为CNCF基金会的一系列产品基本都是使用Go语言写的,Go开发工程师相比于其他人员拥有天然优势。


总结


到这里,每天十分钟轻松入门K8S的01篇: 《你为什么需要学习K8S就结束了》 ,后续会持续更新相关文章,带大家了解K8S架构、K8S组件、如何搭建K8S集群、各种K8S对象、K8S高级特性、K8S-API等等内容。


欢迎大家点赞、收藏、催更~


作者:安妮的心动录
来源:juejin.cn/post/7291513540025434169
收起阅读 »

过度设计的架构师们,应该拿去祭天

我发现一个非常有趣的现象。 十多年前,那时“美女”这个称谓还是非常稀缺值钱的,被这么称呼的女性同胞占比,也就是不到10%的样子。 后来就愈发不可收拾了,只要是个女的活的,下至5岁上至50岁的,99%都被人称呼过“美女”。 当然,现在互联网行业的架构师,也越来越...
继续阅读 »

我发现一个非常有趣的现象。


十多年前,那时“美女”这个称谓还是非常稀缺值钱的,被这么称呼的女性同胞占比,也就是不到10%的样子。


后来就愈发不可收拾了,只要是个女的活的,下至5岁上至50岁的,99%都被人称呼过“美女”。


当然,现在互联网行业的架构师,也越来越“美女化”了,基本上有个两三年工作经验的,带两三个应届生负责过一两个QPS不过十,用户量不过千的小系统的,把项目用SSM框架给搭建起来的,也都成架构师了。



而这些所谓的“架构师”们,如果仅仅是title上的改动,平时工作中该撸代码就撸代码,该摸鱼看网页就看网页,其实也真的没什么。


最最最最怕的就是,他们觉得自己的身份已经变了,是时候该体现出自己作为系统架构师价值的时候了,那一切就会变得不可收拾了。


这些架构师们体现价值的方式当然是做架构设计。按照他们的话说,系统架构要具备前瞻性、灵活性、复用性、伸缩性、可维护性、可扩展性、低耦合性、高内聚性、可移植性。当然,基本上90%都是过度设计。



下面让我们来细数一下,那些年,我所经历过的过度设计。



名副其实的微服务


不久前我面试过一个中小厂架构师,看他的简历上赫然写着,“主导XX系统从单体服务往微服务架构演进工作”。


然后我问他的问题是:“详细说下微服务拆分这件事情,包括:微服务拆分的原因、时机和拆分后的粒度。”


这个架构师说的第一句话就把我雷到了:“微服务拆分的粒度,我认为越细越好,不然为什么叫微服务呢?而且,现在的一个很小的微服务,随着业务的持续迭代演进,未来都有可能变得非常庞大,我们做架构设计的,必须要具备前瞻性。”


他接着说:“我们的微服务不但按照业务模型进行的拆分,而且我还按照controller层、service层和dao层也做了拆分,这样可以提升代码复用性,你想用我哪层的代码,就可以调用我哪层的API。”


最终,一个单体服务就被他拆分成了这样。



我问他:“微服务的‘三个火枪手原则’了解吗?”


他摇了摇头,说不清楚。


我心里感慨到,今年阿里云和腾讯云业绩能不能达标,全看这类架构师的了,他们是真费机器啊。


3个库和300张表


去年,跟一个三方公司临时组建了一个项目组,共同开发孵化A项目。


项目联调期间,我跟三方公司的小A说:“我刚调用了你们项目的XX接口,新增了20条交易数据,你看看你们接口的业务处理正常吗?数据库里面有这20条数据吗?”


小A说:“好的,稍等,我看看。”


20分钟过去了,我问小A看得怎么样了。


小A说:“业务处理是正常的,数据我正在一条条找,20条已经找到17条了,我在找剩下的3条。”


我听得有些懵逼,问小A:”你直接从你们订单表里,不能一下子看到20分钟前写入的20条数据吗?为什么还需要一条条找啊?“


小A说:”我们的架构师老张,按照每天三百万订单的数据增量,做了一个五年架构规划,已经分好了3个库和300张表。我现在正在根据他的路由规则,一条条地找这些数据。“



满城尽是大中台


呵呵,忽如一夜春风来,满城尽是大中台。


2015年福厂正式提出了“大中台、小前台”的中台战略,通过将原本分散到各个业务的支持部门,比如技术部门、数据部门集中到一起,进行快速的服务迭代,以期更高效地支撑前线,大幅降低支持部门的重复投资建设。



三年后,各个大小互联网公司纷纷跟进,争相建设自己家的中台,也就在这时,某独角兽公司的架构师老范过来找我取经。


我跟老范说:“你们的两个主业务是机票和酒店,业务差别太大了,且创新孵化业务并不多,并不适合中台策略。”


老范说:“不,中台这个我们一定要搞,因为既是研发团队的政治任务,也是我个人的技术追求。”


半年后,我问老范搞得怎么样了,老范说:“唉,讨论了半年哪些职责属于大中台,哪些职责属于小前端,现在还没讨论明白呢。”


无处不在的消息队列


福厂收购了某公司,在收购后的一次技术交流中,我听到对方公司的首席架构师说:“MQ是个好东西,能异步处理,能消峰,能解耦,还是应该在项目中多用用的。”



后来发现,大首席架构师的下级执行力真强,MQ真的在他们的项目中无处不在:



  • 发短信验证码的场景用MQ,且其生产者和消费者是同一个服务,就为了用MQ异步调用短信服务的SDK;

  • 打业务日志的场景用MQ,且其生产者和消费者是同一个服务,就是为了用MQ异步打一行日志;

  • TPS个位数的约课场景用MQ,且其生产者和消费者是同一个服务,其美名曰进行消峰;

  • 各服务间的通信基本上80%都用了MQ,而不是RPC,其美名曰系统解耦;


牛逼Class!


遍地开花的多级缓存


对,对,还是上次的那个首席架构师,他除了爱用消息队列外,还特别喜欢用缓存,而且是Guava Cache + Redis的多级缓存。



据同事说,这种多级缓存策略在这位首席架构师的熏陶下,已经遍布了OA系统、公司官网、消息中心、结算系统、供应链系统、CRM系统。


首席架构师说:“缓存不仅能提升性能,还能帮助数据库抗压,提升系统可用性,绝对是个好东西,应该多用一用。”


然后,公司的系统就经常发生多种缓存的数据与数据库的数据一致性问题。


首席架构师又说:“任何架构都是有利有弊的,但只要利大于弊就好,不要太在意。”


设计模式的流毒


记得我刚上班不久,组内有一个架构师同事,写的代码巨复杂,各种技巧、设计模式、高级语法满天飞,还沾沾自喜的给我们炫耀。



一次Code Review的时候,我嘴欠问他这里咋这么设计,他就鄙视的说:“你连这个都不知道,这是设计模式中的建造者模式啊。”



当时觉得他好牛逼,而我好low。


以后,每次进行Code Review,只要看到其他同事代码里有几个if else,架构师同事就质问道:“为什么不用策略模式优化if else?”


当然,还有其他的质问,类似于:这块为什么不用抽象工厂模式?这块为什么不用代理模式?这块为什么不用观察者模式?


后来我们就给他起了个外号,叫“设模”(se mo)。


多多益善的复杂关系


前面说的那些架构师们,他们过度设计所带来的后果是浪费服务器和研发资源,但架构师老邓不一样,他的过度设计是浪费表。


之前见过某在线教育公司设计的表结构,基本上所有表之间的外键关系都是按照多对多方式设计的,也就是加一个中间的关系映射表。



有的我是可以理解的,比如:



  • 一个学生会出现在多个不同的班级里,而一个班级里也会有不同的学生;

  • 一个学生可以学习多门课程,而每门课程又会对多个学生进行学习;

  • 一个学生可以上多个老师的课,而一个老师又可以教多个学生;


但是,但是。



  • 一个学生可以有多个考试成绩,难道一个考试成绩还能属于多个学生吗?

  • 一个学生有多个课程的课时余额,难道一个课时余额还能属于多个学生吗?


老邓说:“万一以后业务变化了呢?一切皆有可能啊。”


数据库的可移植性


还在上大学的时候,在CSDN上看某著名架构师在极力强调数据库的可移植性。



我记得当时的原话大概是:



  • Hibernate的HQL可以帮我们保证不同数据库之间的移植性,比如:MySQL中的limit和Oracle中的rownum。

  • 为什么不能写存储过程?一个重要的原因就是业务逻辑放到数据库里会导致数据库移植成本变大。

  • 程序内尽量采用标准SQL语法,因为我们要考虑将来的移植风险。


当时听了,觉得这个大架构师简直就是YYDS。然后我工作了这么多年,也没遇到过一次数据库移植。


无间道版的数据校验


我厂某团队的架构师老李素以严谨著称,其经常放在嘴边的一句话就是:“工程师不仅仅是一项有创造性的职业,也是一门严谨审慎的职业。”


这话说的确实没毛病,我也看过他们团队的工程代码,程序的边界处理、异常处理和容错处理做得都特别好,入参校验也是特别细致入微。


就像老李所说的那样:“All input is evil。”


不,等等,入参校验没问题,但怎么从数据库里读出来的数据,为什么还要再校验一遍?难道不是在写入的时候校验吗?


老李面无表情地说:“如果数据库中的数据,并没有经过应用程序处理,而是不知道被谁直接改库了呢?”


卧槽,这是泥马数据校验无间道版吗?



疯魔成活的配置化


还是上面的那个架构师老李,他要求团队代码中带数字的地方,全部走配置中心,这样可以不发布代码就直接进行修改。



然后,我就看到了这样的现象:



  • 如果某个HashMap的size大于0,则进行xxxx,这个0写到了配置中心里。

  • 如果用户性别等于1(男性),则进行男装推荐,这个1写到了配置中心里。

  • 如果商品状态等于2(已下线),则进行xxxx,这个2写到了配置中心里。


配置中心啊,你的责任真的好重大。


总结


遇到这种类型的架构师,真的特别想把他们祭天了,因为我是Kiss原则的忠实拥趸。



Keep it simple,stupid,即:保持简单、愚蠢。


保持简单就能让系统运行更好,更容易维护扩展,越是资深的人,越明白这个道理。


作者:库森学长
来源:juejin.cn/post/7287144182967107638
收起阅读 »

看了我项目中购物车、订单、支付一整套设计,同事也开始悄悄模仿了...

在我的mall电商实战项目中,有着从商品加入购物车到订单支付成功的一整套功能,这套功能的设计与实现对于有购物需求的网站来说,应该是一套通用设计了。今天给大家介绍下这套功能设计,涵盖购物车、生成确认单、生成订单、取消订单以及支付成功回调等内容,希望对大家有所帮助...
继续阅读 »

在我的mall电商实战项目中,有着从商品加入购物车到订单支付成功的一整套功能,这套功能的设计与实现对于有购物需求的网站来说,应该是一套通用设计了。今天给大家介绍下这套功能设计,涵盖购物车、生成确认单、生成订单、取消订单以及支付成功回调等内容,希望对大家有所帮助!



mall项目简介


这里还是简单介绍下mall项目吧,mall项目是一套基于 SpringBoot + Vue + uni-app 的电商系统(Github标星60K),采用Docker容器化部署。包括前台商城项目和后台管理系统,能支持完整的订单流程!涵盖商品、订单、购物车、权限、优惠券、会员、支付等功能,功能很强大!



后台管理系统演示



前台商城项目演示



功能设计



这里介绍下从商品加入购物车到订单支付成功的整个流程,涵盖流程的示意图和效果图。



流程示意图


以下是从商品加入购物车到订单支付成功的流程图。



流程效果图


以下是从商品加入购物车到订单支付成功的效果图,可以对照上面的流程示意图查看。



数据库设计


为了支持以上购物流程,整个订单模块的数据库设计如下。



设计要点



接下来介绍下整个购物流程中的一些设计要点,涵盖加入购物车、生成确认单、生成订单、取消订单以及支付成功回调等内容。



加入购物车


功能逻辑


用户将商品加入购物车后,可以在购物车中查看到商品。购物车的主要功能就是存储用户选择的商品信息及计算购物车中商品的优惠。



购物车优惠计算流程



相关注意点



  • 购物车中商品优惠金额不会在购物车中体现,要在生成确认单时才会体现;

  • 由于商品优惠都是以商品为单位来设计的,并不是以sku为单位设计的,所以必须以商品为单位来计算商品优惠;

  • 代码实现逻辑可以参考mall项目中OmsPromotionServiceImpl类的calcCartPromotion方法。


生成确认单


功能逻辑


用户在购物车页面点击去结算后进入生成确认单页面。确认单主要用于用户确认下单的商品信息、优惠信息、价格信息,以及选择收货地址、选择优惠券和使用积分。



生成确认单流程



相关注意点



  • 总金额的计算:购物车中所有商品的总价;

  • 活动优惠的计算:购物车中所有商品的优惠金额累加;

  • 应付金额的计算:应付金额=总金额-活动优惠;

  • 代码实现逻辑可以参考mall项目中OmsPortalOrderServiceImpl类的generateConfirmOrder方法。


生成订单


功能逻辑


用户在生成确认单页面点击提交订单后生成订单,可以从订单详情页查看。生成订单操作主要对购物车中信息进行处理,综合下单用户的信息来生成订单。



下单流程



相关注意点




  • 库存的锁定:库存从获取购物车优惠信息时就已经从pms_sku_stock表中查询出来了,lock_stock字段表示锁定库存的数量,会员看到的商品数量为真实库存减去锁定库存;




  • 优惠券分解金额的处理:对全场通用、指定分类、指定商品的优惠券分别进行分解金额的计算:



    • 全场通用:购物车中所有下单商品进行均摊;

    • 指定分类:购物车中对应分类的商品进行均摊;

    • 指定商品:购物车中包含的指定商品进行均摊。




  • 订单中每个商品的实际支付金额计算:原价-促销优惠-优惠券抵扣-积分抵扣,促销优惠就是购物车计算优惠流程中计算出来的优惠金额;




  • 订单号的生成:使用Redis来生成,生成规则:8位日期+2位平台号码+2位支付方式+6位以上自增id;




  • 优惠券使用完成后需要修改优惠券的使用状态;




  • 代码实现逻辑可以参考mall项目中OmsPortalOrderServiceImpl类的generateOrder方法。




取消订单


功能逻辑


订单生成之后还需开启一个延时任务来取消超时的订单,用户也可以在订单未支付的情况下直接取消订单。



订单取消流程



相关注意点



  • 代码实现逻辑可以参考mall项目中OmsPortalOrderServiceImpl类的cancelOrder方法。


支付成功回调


功能逻辑


前台用户订单支付完成后,第三方支付平台需要回调支付成功接口。



支付成功回调流程



相关注意点



  • 代码实现逻辑可以参考mall项目中OmsPortalOrderServiceImpl类的paySuccess方法。


总结


今天给大家介绍了mall项目中整套购物流程的功能设计,其实对于很多网站来说都需要这么一套功能,说它是通用功能也不为过。从本文中大家可以看到,mall项目的整套购物流程设计的还是比较严谨的,考虑到了方方面面,如果你对mall项目整套购物流程实现感兴趣的话可以学习下mall项目的代码。


项目源码地址


github.com/macrozheng/…


作者:MacroZheng
来源:juejin.cn/post/7290931758787313725
收起阅读 »

2023年震撼!Java地位摇摇欲坠?Java在TIOBE排行榜滑坡至历史最低!

一、Java掉到历史最低 从2023年6月开始Java掉到历史最低排到第4位 2023年10月tiobe编程语言排行榜,Java仍然还是排到了第4位,C# 和 Java 之间的差距从未如此之小。 top 10 编程语言1988年~2023年历史排名 引用...
继续阅读 »

一、Java掉到历史最低


从2023年6月开始Java掉到历史最低排到第4位



2023年10月tiobe编程语言排行榜,Java仍然还是排到了第4位,C# 和 Java 之间的差距从未如此之小。



top 10 编程语言1988年~2023年历史排名



引用tiobe官网上TIOBE Software 首席执行官的话:


10 月头条:C# 越来越接近 Java


C# 和 Java 之间的差距从未如此之小。目前,差距仅为 1.2%,如果保持这种趋势,C# 将在大约 2 个月的时间内超越 Java。在所有编程语言中,Java 的跌幅最大,为 -3.92%,C# 的涨幅最大,为 +3.29%(每年)。这两种语言一直在相似的领域中使用,因此二十多年来一直是竞争对手。Java 受欢迎程度下降的主要原因是 Oracle 在 Java 8 之后决定引入付费许可模式。微软在 C# 上采取了相反的做法。过去,C#只能作为商业工具Visual Studio的一部分。如今,C# 是免费且开源的,受到许多开发人员的欢迎。Java 的衰落还有其他原因。首先,Java 语言的定义在过去几年中没有发生太大变化,而其完全兼容的直接竞争对手 Kotlin 更易于使用且免费。——TIOBE Software 首席执行官 Paul Jansen


二、编程语言排行榜


编程语言排行榜是一种用来衡量编程语言的流行度或受欢迎程度的指标,它通常会根据一些数据或标准来对编程语言进行排序和评价。不同的编程语言排行榜可能会有不同的数据来源、计算方法和评估标准,因此它们的结果也可能会有所差异。


目前,最知名和权威的编程语言排行榜之一是 TIOBE 编程社区指数,它由成立于 2000 年 10 月位于荷兰埃因霍温的 TIOBE Software BV 公司创建和维护。TIOBE 编程社区指数通过对网络搜索引擎中涉及编程语言的查询结果数量进行计算,来衡量各种编程语言的受欢迎程度。TIOBE 编程社区指数每个月都会更新一次,并且每年还会评选出一门年度编程语言,表示该门语言在当年的排名中上升幅度最大。除了 TIOBE 编程社区指数之外,还有一些其他的编程语言排行榜,以下是列举的一些编程语言排行榜。


1、TIOBE编程语言排行榜


TIOBE是一家荷兰的编程软件质量评估公司,每月发布一份编程语言排行榜。它使用搜索引擎查询结果、开发者社区活跃度和其他指标来评估编程语言的受欢迎程度。



2023年10月TIOBE编程语言排行榜



2、Stack Overflow开发者调查


Stack Overflow每年进行一次开发者调查,其中包括有关最受欢迎编程语言的信息。Stack Overflow 开发者调查是最权威的编程语言排行榜之一,该调查可以反映全球开发者对编程语言的喜好和使用情况。在选择编程语言时,可以参考该调查的结果,但也需要根据自己的实际需求和开发环境进行综合考虑。



连续三年最受欢迎编程语言排名,可以明显的看出Java的占比在逐年的降低



3、GitHub编程语言趋势榜


GitHub提供了一个编程语言趋势页面,显示了开发者在GitHub上使用的编程语言趋势。虽然这不是正式的排行榜,但反映了实际的开发趋势。


GitHub在趋势榜比较前的基本者是Python或Go的项目



GitHub官网已经去掉了top的排名榜只保留了趋势榜,由一些GitHub的爱好者和贡献者创建和维护的www.github-zh.com的GitHub中文社区网站,是非官方github网站,它旨在为中文用户提供GitHub的相关资讯、教程、交流和协作平台,还可以查到Github项目排行榜。



三、展望Java


可以看到各种编程语言排行榜的数据,虽然会存在片面的情况,但也大体能表现出Java的地位在下降,遥想当年Java是排行榜霸榜老大哥。



虽然Java明显下降,或许正如TIOBE首席执行官说的“Java 受欢迎程度下降的主要原因是 Oracle 在 Java 8 之后决定引入付费许可模式。微软在 C# 上采取了相反的做法。”在这个开放的世界里真正的开源而不是利用开源来测试付费项目才能真正的让大家推崇。


Java的许可模式变化导致用户流失。自从Java 8之后,甲骨文公司决定对Java的商业使用收取费用,这使得一些企业和开发者转向其他免费或开源的语言,如C#、Python等 。


Java的竞争对手不断发展和创新,提供了更多的选择和优势。例如,C#在.NET平台上不断完善和扩展,支持跨平台、混合开发、WebAssembly等技术 ;Python在数据科学、人工智能、Web开发等领域有着广泛的应用和生态 ;Kotlin作为Android官方推荐的语言,兼容Java,并提供了更多的语法糖和功能 。




Java虽然在编程语言排行榜上有所下降,但并不意味着Java就没有前途和价值。Java仍然是一门成熟、稳定、高效、跨平台的语言,拥有庞大的用户群和丰富的生态系统。Oracle作为Mysql、Java等重量级项目的拥有者,也在不断地改进和创新Java,让Java能够适应时代的变化和需求。包括Java 17的免费、Kafka/Spring Boot新版本最低的Java版本为17、Java 21引入协程等,都是Oracle在努力让Java保持竞争力和活力的例证 。



未来在不断的变化,说不定马斯克的美女机器人就真的造出来了。。。


当然,我们也不能忽视其他编程语言的发展和优势,我们应该保持开放和学习的心态,了解不同语言的特点和适用场景,选择最合适的语言来解决问题。编程语言只是工具,重要的是我们能够用它们创造出有价值的产品和服务。


作者:玄明Hanko
来源:juejin.cn/post/7290849115721285667
收起阅读 »

可别小看了一边写代码嘴里一边叨咕的同事,人家可能用的是小黄鸭调试法

什么,鸭子还能调试代码?什么神奇的鸭子啊。 当然不是了,是鸭子帮你调试,那好像也有点儿厉害。 初听感觉是傻子,再听感觉是玄学。 什么是小黄鸭调试法 当然不是鸭子调试代码了,也不是鸭子帮你调试,其实还是靠你自己的。 小黄鸭调试法(Rubber Duck Deb...
继续阅读 »

什么,鸭子还能调试代码?什么神奇的鸭子啊。


当然不是了,是鸭子帮你调试,那好像也有点儿厉害。


初听感觉是傻子,再听感觉是玄学。



什么是小黄鸭调试法


当然不是鸭子调试代码了,也不是鸭子帮你调试,其实还是靠你自己的。


小黄鸭调试法(Rubber Duck Debugging)是一种常用于解决编程问题的技巧,不是代码技术层面的技巧。


大致的调试过程是这样的:



  1. 首先你写好了代码,或者有些逻辑一直写不出来,然后很有自信或者不自信;

  2. 然后你找到一只鸭子,玩具鸭子,或者任意一个电脑旁边的物件;

  3. 最后,把你的代码的逻辑尽量详细的讲个上一步找到的对象,比如一只玩具鸭子;

  4. 通过讲解的过程,你很有可能发现代码上的漏洞,有时候还能发现隐藏的漏洞;



你还可以拉过旁边的人,对着他讲,前提是保证别人不会打你。


这个过程更像是一种review的过程,而且是那种非常具体的review,只不过是自己 review 自己的代码或逻辑。


它的核心是通过将问题或逻辑用语言描述出来,在这个过程中找到解决问题的线索。


虽然这个方法听起来可能有点奇怪,但它在实际中确实能够帮助很多人解决问题。解释问题的过程可能会强迫你慢下来,更仔细地思考,从而找到之前忽略的问题点。


另外,在进行这一些列操作的过程中,尽量保证周围没有人,不然别人可能觉得你是个傻子。


当然了,这个操作你可以在心里默默进行,也是一样的效果。


各位平时工作中有没有遇见过有人使用小黄鸭调试法呢?我看到这个概念的时候想了一下,好像还真碰到过。之前有同事在那儿写代码,一边写嘴里一边叨咕,也不知道在说啥,还开玩笑说这是不是你们这个星座的特质(某个星座)。


现在想想,人家当时用的是不是小黄鸭调试法呀,只恨当初孤陋寡闻,没有问清楚啊。


内在原理


小黄鸭调试法的内在原理其实是涉及到认知心理学中的一些概念的,并不真的是玄学和沙雕行为。


认知外部化


这是小黄鸭调试法的核心。当你将问题从内心中的思考状态转移到外部表达时,你会更加仔细地思考问题。解释问题需要你将问题的细节和步骤以清晰的语言描述出来,这个过程可以帮助你整理思路,更好地理解问题。


问题表达


描述问题的过程可以迫使你更具体地考虑问题。将问题分解为不同的部分,逐步地解释代码的执行流程,有助于你更好地理解代码中可能的缺陷或错误。


观察问题:


当你通过语言表达问题时,可能会注意到之前忽略的细节。这可能是因为你在描述问题时需要更仔细地审查代码和逻辑,从而让你注意到潜在的问题点。


听觉和口头处理


讲述问题的过程涉及到将问题从书面表达转化为口头表达。听觉和口头处理可以帮助你以不同的方式来理解问题,可能会在你的大脑中触发新的洞察力。


认知切换:


与代码一起工作时,你可能会一直陷入相同的思维模式中,难以看到问题。通过将问题从代码中抽离出来,并通过描述来关注它,你会进行认知切换,从而能够以不同的角度审视问题。


总结起来其实很简单,如果一个知识点你理解了,你一定能给别人讲出来,或者写出来,而且别人能够理解。如果你在讲的时候发现有模棱两可的地方,那说明你还没有百分百理解。


就像我们平时写技术文章一样,有时候碰到一些细节写半天也写不清楚,那就是还没有完全理解。


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

推荐一款“自学编程”的宝藏网站!详解版~(在线编程练习,项目实战,免费Gpt等)

🌟云端源想学习平台,一站式编程服务网站🌟云端源想官网传送门⭐📚精品课程:由项目实战为导向的视频课程,知识点讲解配套编程练习,让初学者有方向有目标。🎯📈课程阶段:每门课程都分多个阶段进行,由浅入深,很适合零基础和有基础的金友们自由选择阶段进行练习学习。🌈🎯章节实...
继续阅读 »

🌟云端源想学习平台,一站式编程服务网站🌟


云端源想官网传送门


📚精品课程:由项目实战为导向的视频课程,知识点讲解配套编程练习,让初学者有方向有目标。🎯


📈课程阶段:每门课程都分多个阶段进行,由浅入深,很适合零基础和有基础的金友们自由选择阶段进行练习学习。🌈


🎯章节实战:每一章课程都配有完整的项目实战,帮助初学者巩固所学的理论知识,在实战中运用知识点,不断积累项目经验。🔥


💼项目实战:企业级项目实战和小型项目实战结合,帮助初学者积累实战经验,为就业打下坚实的基础,成为实战型人才。🏆


729cc0d1da3852fe1d8a0eb81a6b357b.png


🧩配套练习:根据课程小节设置配套选择练习或编程练习,帮助学习者边学边练,让学习事半功倍。💪


💻在线编程:支持多种编程语言,创建Web前端、Java后端、PHP等多种项目,开发项目、编程练习、数据存储、虚拟机等都可在线使用,并可免费扩展内存,为你创造更加简洁的编程环境。🖥


🤝协作编程:可邀请站内的好友、大佬快速进入你的项目,协助完成当前项目。与他人一起讨论交流,快速解决问题。👥


📂项目导入导出:可导入自己在在线编辑过的项目再次编辑,完成项目后也可以一键导出项目,降低试错成本。🔗


🤖AI协助编程:AI智能代码协助完成编程项目,随时提问,一键复制代码即可使用。💡

ddcdaae2cab0bac546d4d195018866f0.png


🔧插件工具:在使用在线编程时,可在插件工具广场使用常用的插件,安装后即可使用,帮助你提高编程效率,带来更多便捷。🛠


📞一对一咨询:编程过程中,遇到问题随时提问,网站1V1服务(在职程序员接线,不是客服),实时解决你在项目中遇到的问题。📬


🛠工具广场:提供一些好用的在线智能工具,让你能够快速找到各种实用工具和应用。覆盖了多个领域,包括智能AI问答等。🔍


910fa68dda93dd2594530d0e5af268c7.pngd419dacb74e778686c40897862075ac8.png

收起阅读 »

谈谈成长

背景 距离毕业到已经三年多了, 距离实习到现在已经三年半了, 在主管的建议下, 8月17号在公司的研发部门做了 一场关于成长的分享, 从工作方式到技术能力提升对自己三年的成长进行了一个复盘, 幸运的是分享得到的反 馈非常好, 老板看了将录屏在整个研发部进行公布...
继续阅读 »

背景


距离毕业到已经三年多了, 距离实习到现在已经三年半了, 在主管的建议下, 8月17号在公司的研发部门做了
一场关于成长的分享, 从工作方式到技术能力提升对自己三年的成长进行了一个复盘, 幸运的是分享得到的反
馈非常好, 老板看了将录屏在整个研发部进行公布并且推荐大家观看, 这是部门内分享活动以来第一次公示与推荐录
屏的情况, 我自己也感觉非常感慨, 毕业以来, 一直朝着“一年入门, 三年高工、五年资深、七年架构”的目
标前进, 有时候回过头来会发现这一路还是挺有意思的, 借着这样一个平台, 以文字的形式描述出来, 期望
能够给前行路上的各位有一定的帮助


下面的描述中, 为了不暴露个人信息, 对公司名称的描述统一替换为 XX, 涉及到人名的我都会进行打码


01.png


一、为什么做这个分享(why)


1.1、原因一


主管说从实习加入XX三年半以来, 看到我成长了很多, 推荐我可以做一下关于个人成长这一块的分享


1.2、原因二


我个人仔细的想了一下, 三年半前加入XX, 当时公司才400多人, 没有一个正式的java团队(公司主c#), 三年半的时间, 公司发展到了750多人, 我经历了java团队从0到1的完整过程(目前已经有3个java团队, 加起来有接近30个java开发), 并且在这三年半的时间, 公司的后端业务项目开始从.net往java迁移, 有大量的重构项目和新的大型项目, 在此期间, 经历了可能有10个以上从0到1的大型项目的落地, 并且这些项目中我个人承担了绝大部分的核心功能开发, 在这些项目的锻炼下, 不管是技术还是工作方式方面, 都有学习到非常多的知识, 借着这样的机会复盘一下, 继续完善自己, 我主观认为这样的经历可能会有一定的参考价值, 期望能够通过这样的一个分享, 能够给一些可能遇到瓶颈或者跟我遇到一样问题的同学一些启发


二、工作方式上的成长及建议


2.1、需求方案的决策


Pre: 遇到问题 / 需求我应该怎么做, 还没有独当一面的能力


Now: 遇到问题 / 需求我思考可以怎么做, 并从自己的角度出发提出方案, 标识每个方案的优缺点, 询问应该怎么做(即使提出的方案都不是最优解, 但是有自己的思考, 并且在遇到更合适的方案时能有一个对比学习)


截图是两个例子, 在我实现功能的时候, 有不同的方案, 在把握不准哪种方案比较合适的情况下, 向主管进行询问, 对可行的方案的优缺点进行分析, 在主管多年的经验下确定最终的实施方案


02.png


03.png


2.2、需求功能的实现和上线


Pre: 需求处理完, 简单的自测或者压根就不自测, 测试流程走完以后发上线就不管了(在工作过程中其实有遇到许多同事也是这样), 在刚来实习以及刚毕业那段时间, 一个功能编码完以后测试反馈了许多的bug问题, 改了一个旧的出现一个新的, 之前有听过一个段子, 测试听到开发说的最多的是什么(我没改代码 / 网络问题 / 你再试试), 这个段子就在我身上出现了, 当时改一个功能, 连续几次都没改好(因为改完后没有充分验证就提测了), 见下图(2021年的聊天记录)


Now: 需求处理完, 充分的自测, 提测以后多次跟进测试同学的测试情况, 上线后跟进线上是否正常使用, 找产品进行验收流程


Suggest: owner意识、需求从哪里开始就从哪里结束, 回调产生事件的人, 形成闭环


owner意识是主管在小组中提出来的一个工作方式, 是说一个开发, 在多人协作的场景下, 不能只关注自己那一块的功能, 需要对整个流程有充分的了解, 以一个owner的角色参与到项目开发中, 跟进其他端的对接, 把控整个项目的进度, 要做到这个其实是很难的, 需要花费更多的精力, 但是一旦做到了, 对整个项目就有一种非常清晰的感觉, 能够更好的完成多人协作项目的落地, 需求从哪里开始就从哪里结束, 每次一个需求处理完以后, 我会严格按照 自测 -> 提测 -> 联调 -> 督促产品验收 -> 灰度环境发布及验收 -> 正式环境发布及验收 -> 同步所有有关开发自己的功能的发布情况


在这样一个闭环的链路中, 功能的落地和稳定有了非常明显的提高


04.png


2.3、句句有回应, 事事有着落


Pre: 忙的时候忘记回消息, 事情多的时候忘记一些临时分配的事情, 遇到的坑重复跳


Now: todoList、QQ置顶, 多检查(防止多次返工), 防止重复的问题重复出现(错别字)


todoList:
我是通过一个txt文件来进行一个记录的(当然有更合适的, 只是用习惯了), 会记录手上的需求有哪些, 每一个的进度是怎么样的, 上线流程是怎么样的, 我给自己定的上线流程中分为以下几步, 并且在上线的过程中严格的按照下面的流程进行操作, 这样极大的提高了上线的稳定性(特别是一些大版本的发布, 有这样的严格流程下, 稳定性有了非常明显的提高)


1、是否有旧数据需要处理


2、是否有sql脚本需要执行


3、是否依赖于其他人的功能


4、是否有单独服务器灰度的需求, 有些业务需要单独一台服务器来进行灰度, 然后通过nginx将部分流量转发过来验收


5、开始灰度, 灰度后产品验收, 全部负载发布完毕后回调需求开始的人, 形成闭环


QQ置顶: 我们公司是采用QQ作为沟通工具的(可能是因为很早之前就是用这个, 即使现在有企业微信, 但是沟通这一块大家都不太倾向于切到企业微信), 当在外面或者遇到其他比较紧急事情时有人找或者发消息, 可能看完就忘记回了, 于是我养成一个习惯, 有人发消息没时间回的, 第一时间置顶, 等有空后再来跟进, 跟进完后取消置顶


防止重复的问题重复出现: 我们需要写周报, 刚开始的时候有出现错别字、格式不正确、周报字数过多的情况(主管规定周报不能超过250个字), 后面为了避免这种情况, 我每次写完都会阅读两遍内容, 防止之前产生的问题重复发生, 每个人都有粗心的时候, 也不能保证不出现错误, 但是我们可以通过一些方式方法来尽量的避免错误的重复发生


2.4、学习别人优秀的地方


人无完人, 比如我有时候说话会比较直接(好吧, 我是直男....), 那时主管就让我在沟通这一块多跟斜对面的同事学习一下, 后面我就观察他的沟通方式, 然后自己从模仿开始, 慢慢的也学会了怎么进行有效的沟通、委婉的拒绝等, 学习别人优秀的地方也是成长中比较重要的一环


2.5、批量接口的重要性


Pre: 对需求的时候遇到一个问题就问一次, 效率低, 刚来实习的时候, 不熟悉业务, 当时接触到一块比较复杂的涉及到购物车优惠计算的业务, 当时跟对方讨论的时候, 遇到一个不懂的就直接去问, 多次打断他的工作, 效率非常低


Now: 对整个需求进行梳理, 整理所有的疑问一次性请教, 效率提高了很多, 也不会容易打断别人的工作(反到现在我经常被别人打断=,=看到了曾经的我....)


2.6、做的事太简单, 没有挑战性


今年带了一个实习生, 刚开始给他分配的工作都是比较简单的, 主要是修bug、小需求, 但是即使是这样简单的功能, 他做的也是磕磕碰碰, 比如代码规范不达标、空指针异常判断不全面、业务涉及到的点想的不全面等等许多瑕疵, 多次需要我来进行一个兜底, 后面他跟HR反馈说太简单没有挑战性, 于是我就分配了一个稍微大的一点需求给他, 结果做的惨不忍睹, 后面他实习结束时这个需求才做到一半, 我转给其他同事帮忙接手....结果那个同事看到代码后对我吐槽了许久....


当时收到反馈后, 我及时的找他进行沟通, 我的观点是他做的每一个简单的需求, 需求的完整稳定上线都是为了给主管建立一个做事靠谱的印象, 如果做事不靠谱, 经常需要他人来进行兜底, 那么谁也不敢把复杂重要的任务交给你做, 公司很多业务都涉及到商家的钱, 一旦这种重要的业务出问题, 那么会给公司造成巨大损失, 其实仔细想想, 自己实习那会也差不多, 想要做很厉害的项目, 用很流行的技术, 但是如果自己给人的感觉是不靠谱的, 那么主管自然就不敢把这些项目交给我了, 明白了这一点后, 当时我毕业一年给主管的目标是一个需求功能下来, 不管大小, 测试反馈的bug不能超过3个, 一个月产生的线上事故不能超过1次, 我努力做到了这一点, 线上事故几乎没发生过, 随之而来的是小组中绝大部分核心的业务、基础组件的开发都由我来处理, 以及一波大的涨薪


2.7、内卷


维基百科: 原是一个社会学概念,指一种文化模式发展到一定水平后,无法突破自身,只能在内部继续发展、复杂化的过程。大约从2018年开始,“内卷”一词在中国大陆变得广为人知,并引申表示付出大量努力却得不到等价的回报,必须在竞争中超过他人的社会文化,包含了恶性竞争、逐底竞争等更为负面的含义。


我们公司965几乎不加班, 所以我们6点会有比较充足的时间, 我一般去楼下吃完饭以后就会回到公司学习, 三年半的时间阅读了大量的源码书籍、学习了许多知识, 个人认为, 深入学习技术, 提高自己其实不属于内卷, 无意义的加班, 内耗等才是内卷, 其他小组也有人说我比较卷, 但是我们小组(包括主管)都是知道我晚上下班后是在公司学习的, 也很少会加班, 借着分享的机会我也跟大家澄清了这样的情况, 并且我们小组的氛围并没有因为我下班后的学习而导致整个小组都晚下班的情况(公司7点左右就基本空了.....)


三、技术能力上的成长及建议


3.1、如何学习


一、我学习一门未接触过的技术时, 会先看视频学习, 建立基本认识, 并且能够从讲师身上学到一些经验, 即先学会简单的使用
二、在了解了基本使用, 并且用起来的情况下, 我会查找跟该技术有关的权威书籍, 对权威书籍的学习是为了建立完整的知识体系


这个学习方式是我从大学以来就保持的, 而我认为这也是对我来说是最合适的学习方式


3.2、打地基-基础知识的重要性


基础知识对一个程序员来说是非常重要的, 个人认为基础越扎实的同学往往在学习技术的时候会吸收的更快, 并且也能够走的更远


一、数据结构, 我学习数据结构的时候, 会手写每个数据结构, 即使是最难的红黑树, 我也手写出来了(当时在大学的时候花了一个下午就为了写一个新增节点和删除节点的方法), 对于数据结构的学习, 我推荐: 恋上数据结构 这套视频, 讲的非常好, 大家如果有兴趣的话可以各显神通的去找找=,=


二、计算机网络, 计算机网络我是通过看视频加书籍的方式来学习的, 视频推荐: 韩立刚, B站就能搜到, 讲的通俗易懂, 我推荐了几个朋友看, 都反馈非常棒, 书籍推荐 计算机网络 第六版(考研408专用)


三、操作系统, 对操作系统的学习, 能够让我们在了解JVM、以及一些底层知识的时候(比如CAS、volatile、synchronize等原理)能够更加的顺利, 他们都是依赖于操作系统相关的知识来的, 视频我推荐: 哈工大的计算机操作系统, B站能搜到, 书籍推荐计算机操作系统(考研408专用)


四、汇编语言, 如果有看过深入理解Java虚拟机这本书, 那么里面就有出现跟汇编相关的话术, 如果对汇编有所了解, 能够亲身的体验到寄存器操作、中断的原理等, 这些在学习操作系统等知识的时候必然会遇到的话术, 视频我推荐推荐: 小甲鱼, B站就能搜到, 书籍我推荐(汇编语言(第3版) 王爽)


五、设计模式, 刚开始写代码的时候, 会一个方法写很多逻辑, 就像流水账一样, 一直写下去, 没有考虑复用等情况, 通过学习设计模式, 我们可以写出更加优雅的代码, 模板方法、单例、工厂等模式的使用能够使得我们的代码阅读性更高、扩展性更强, 学会了设计模式的情况下, 再去看自己之前写的代码就会发现还能写的更好! 并且有了这个知识的基础上, 我们去看一些框架源码的时候会更加顺利, 框架源码用到设计模式的时候命名都是通俗易懂的, 看到名字就知道用了什么模式, 就像程序员之前互相沟通一样, 这个我没有看视频, 我看的是 HeadFirst设计模式 这本书籍, 通过一些生动形象的例子, 把设计模式讲活了...


3.3、创造核心竞争力-不停留在只会用的地步


java开发往往离不开spring的生态系统, 框架开发出来就是给人更加方便开发功能用的, 如果仅仅会用, 那么在遇到一些问题的时候会无从下手, 三年半的时候, 我阅读了spring、springmvc、mybatis、springboot、springcloud等框架的源码, 通过书籍加视频的方式深入的了解了这些框架的原理, 看这些框架源码的时候, 不纠结于一些边线知识, 只管主线流程, 了解主线流程后, 我发现后续遇到问题时, 我能非常自信的跟进源码去排查问题, 在第四章节中我会整理每一个框架我都是通过哪些书籍来深入学习的


3.4、学以致用-尝试输出(github / 博客 / 分享)


学习一个知识, 如果仅仅看了一遍书 / 看了一遍视频, 那么可能过几天就会忘记了, 一般我是通过看视频 -> 记笔记 -> 看书 -> 对书中的知识点进行整理笔记, 笔记采用类似于给他人讲解的方式来记录 -> 将笔记记录在github 或者 以博客的形式分享出来, 在这样的链路下, 我每一步都能更加深刻的学习到知识点, 有时候看书看懂了不代表真懂了, 真正用笔记来描述的时候会发现是磕磕碰碰的, 与此同时, 将这些磕磕碰碰的知识去再次学习, 那么对整个知识点就会有更加全新的认识, 大家也可以看到, 我的掘金的博客是从2020年就开始写了, 都是我个人的口头描述转为文字描述


3.5、有枪不用和无枪可用


在掌握了工作中需要的知识点的情况下, 我们需要去学习流行的技术, 防止自己落伍, 技术的迭代更新是非常快的, 学习这些技术, 往往会给自己带来意想不到的结果


一年前公司我深入的去研究了eureka、zuul等springcloud组件的原理, 后面幸运的是, 公司有一个私有化部署的项目, 主管的计划是用微服务来搭建, 这个项目需要考虑到客户的资源,
有些客户可能预算比较高, 我们就可以提供一套完整的微服务来运行, 有些客户预算比较低, 那么可能最多就跑3-4个java项目, 于是主管的要求是我们的微服务功能, 需要能够满足上述的情况,
能够非常方便的将一个或者多个服务合并成一个服务, 并且自由搭配


正是因为我有对这些组件的深入了解, 我从源码层次提供了一套实现方案, 并且是最简单的实现方案, 主要的原理就是控制bean的加载(打包的时候一起打包, 但是不加载到内存)以及内部
rpc调用时的扩展(利用回环地址来尽可能的忽略http请求的花销), 如果我没有对这一块有所掌握, 那么我可能就失去了这样一个非常好的锻炼机会了


四、从成长的曲线来看侧重点


05.png


作者:zhongshenglong
来源:juejin.cn/post/7277489569958936588
收起阅读 »

茶百道全链路可观测实战

作者:山猎 茶百道是四川成都的本土茶饮连锁品牌,创立于 2008 年 。经过 15 年的发展,茶百道已成为餐饮标杆品牌,全国门店超 7000 家,遍布全国 31 个省市,实现中国大陆所有省份及各线级城市的全覆盖。2021 年 3 月 31 日,在成渝餐·饮峰会...
继续阅读 »

作者:山猎


茶百道是四川成都的本土茶饮连锁品牌,创立于 2008 年 。经过 15 年的发展,茶百道已成为餐饮标杆品牌,全国门店超 7000 家,遍布全国 31 个省市,实现中国大陆所有省份及各线级城市的全覆盖。2021 年 3 月 31 日,在成渝餐·饮峰会中,茶百道斩获“2021 成渝餐·饮标杆品牌奖”。2021 年 8 月,入选艾媒金榜(iiMedia Ranking)最新发布《2021 年上半年中国新式茶饮品牌排行 Top15》。2023 年 6 月 9 日,新茶饮品牌“茶百道”获得新一轮融资,由兰馨亚洲领投,多家知名投资机构跟投,估值飙升至 180 亿元。


今年 4 月,茶百道在成都总部举行了品牌升级发布会,宣布门店数突破 7000 家。根据中国连锁经营协会的数据,截至 2020 年、2021 年以及 2022 年 12 月 31 日,茶百道门店数量分别为 2,240 间、5,070 间以及 6,532 间,疫情并没有拖慢其扩张步伐。


随着业务规模的急速扩展,茶百道全面加速推进数字化转型战略。 但由于茶百道部分早期业务系统由外部 SaaS 服务商提供,无法满足线上业务高速增长所带来的大规模、高并发、弹性扩展、敏捷性、可观测等要求。为了满足线上线下门店客户需求与业务增长需要,针对店务、POS、用户交易、平台对接、门店管理、餐饮制作等核心链路服务,茶百道选择全面自研与阿里云云原生能力相结合,推动容器化、微服务化、可观测能力全面升级。


云原生化的业务价值


茶饮行业面临着市场竞争的压力和内部运营效率的提升需求。为了应对这些挑战,阿里云与茶百道一起完成云原生上云的转型,开启数字化的新征程。


采用容器和微服务技术实现了应用的轻量化和高可移植性。让企业可以更灵活地部署、扩展应用,快速响应市场需求,使得企业能够实现应用的高可用性和弹性扩展能力,无论面对突发的高峰访问量还是系统故障,都能保持业务的稳定运行。


引入了持续交付和持续集成的开发方式,帮助企业实现了快速迭代和部署。通过自动化的流程,企业能够更快地推出新功能和产品,与市场保持同步,抢占先机。


云原生的上云转型不仅带来了更高的安全性、可用性和可伸缩性,也提升了企业的创新能力和竞争力。


云原生带来的可观测挑战


茶百道作为业务高速发展的新兴餐饮品牌,每天都有海量的在线订单,这背后是与互联网技术的紧密结合,借助极高的数字化建设支撑茶百道庞大的销售量。因此,对于业务系统的连续性与可用性有着非常严苛的要求,以确保交易链路核心服务的稳定运行。特别是在每日高峰订餐时段、营销活动、突发热点事件期间,为了让用户有顺畅的使用体验,整个微服务系统的每个环节都需要保证在高并发大流量下的服务质量。


完善的全链路可观测平台以及 APM  ( Application Performance Management )工具,是保障业务连续性与可用性的前提。在可观测技术体系建设上,茶百道技术团队经历过比较多探索。全面实现容器化之前,茶百道在部分微服务系统上接入了开源 APM 工具,并进行超过一年时间的验证,但最终没有能够推广到整个微服务架构中,主要有这几个方面的原因:




  • 指标数据准确度与采样率之间的平衡难以取舍


    适当的采样策略是解决链路追踪工具成本与性能的重要手段,如果 APM 工具固定使用 100% 链路全采集,会带来大量重复链路信息被保存。在茶百道的庞大微服务系统规模下,100% 链路采集会造成可观测平台存储成本超出预期,而且在业务高峰期还会对微服务应用本身的性能带来一定影响。但开源工具在设定采样策略的情况下,又会影响指标数据准确度,使错误率、P99 响应时间等重要可观测指标失去观测与告警价值。




  • 缺少高阶告警能力


    开源工具在告警方面实现比较简单,用户需要自行分别搭建告警处理及告警分派平台,才能实现告警信息发送到 IM 群等基本功能。由于茶百道微服务化后的服务模块众多、依赖复杂。经常因为某个组件的异常或不可用导致整条链路产生大量冗余告警,形成告警风暴。造成的结果就是运维团队疲于应付五花八门且数量庞大的告警信息,非常容易遗漏真正用于故障排查的重要消息。




  • 故障排查手段单一


    开源 APM 工具主要基于 Trace 链路信息帮助用户实现故障定位,对于简单的微服务系统性能问题,用户能够快速找到性能瓶颈点或故障源。但实际生产环境中的很多疑难杂症,根本没有办法通过简单的链路分析去解决,比如 N+1 问题,内存 OOM,CPU 占用率过高,线程池打满等。这样就对技术团队提出了极高要求,团队需要深入了解底层技术细节,并具备丰富 SRE 经验的工程师,才能快速准确的定位故障根源。




接入阿里云应用实时监控服务 ARMS


在茶百道系统架构全面云原生化的过程中,茶百道技术团队与阿里云的工程师深入探讨了全链路可观测更好的落地方式。


ARMS 应用监控作为阿里云云原生可观测产品家族的重要成员,提供线程剖析、智能洞察、CPU & 内存诊断、告警集成等开源 APM 产品不具备的能力。在阿里云的建议下,茶百道技术团队尝试着将一个业务模块接入 ARMS 应用监控。


由于 ARMS 提供了容器服务 ACK 环境下的应用自动接入,只需要对每个应用的 YAML 文件增加 2 行代码就自动注入探针,完成整个接入流程。经过一段时间试用,ARMS 应用监控提供的实战价值被茶百道的工程师不断挖掘出来。茶百道同时使用了阿里云性能测试产品 PTS,来实现日常态和大促态的容量规划。因为ARMS和 PTS 的引入,茶百道日常运维与稳定性保障体系也发生了众多升级。


围绕 ARMS 告警平台构建应急响应体系


由于之前基于开源产品搭建告警平台时,经常遇到告警风暴的问题,茶百道对于告警规则的配置是非常谨慎的,尽可能将告警目标收敛到最严重的业务故障上,这样虽然可以避免告警风暴对 SRE 团队的频繁骚扰,但也会让很多有价值的信息被忽略,比如接口响应时间的突增等。


其实对于告警风暴问题,业界是有一整套标准解法的,其中涉及到去重、压缩、降噪、静默等关键技术,只是这些技术与可观测产品集成上存在一定复杂度,很多开源产品并没有在这个领域提供完善方案。


这些告警领域的关键技术,在 ARMS 告警平台上都有完整功能。以事件压缩举例,ARMS 提供基于标签压缩和基于时间压缩两种压缩方式。满足条件的多条事件会被自动压缩成为一条告警进行通知(如下图所示)。


图片
图: 基于标签压缩


图片
图:基于时间压缩


配合 ARMS 告警平台所提供的多种技术手段,可以非常有效的解决告警风暴的问题,因此茶百道技术团队开始重视告警的使用,逐步丰富更多的告警规则,覆盖应用接口、主机指标、JVM 参数、数据库访问等不同层面。


通过企业微信群进行对接,使告警通知实现 ISTM 流程的互动,当值班人员收到告警通知后,可以直接通过 IM 工具进行告警关闭、事件升级等能力,快速实现告警处理。(如下图所示)


图片
图:监控告警事件的智能化收敛与通告


灵活开放的告警事件处置策略满足了不同时效、场景的需求。茶百道在此基础上参考阿里巴巴安全生产最佳实践,开始构建企业级应急响应体系。将业务视角的应急场景作为事件应急处置的核心模型,通过不同告警级别,识别与流转对应的故障处理过程。这些都是茶百道在全面云原生化后摸索出的经验,并显著提升生产环境服务质量。


引入采样策略


从链路信息中提取指标数据,是所有 APM 工具的必备功能。不同于开源产品简单粗暴的指标提取方式,ARMS 应用监控使用端侧预聚合能力,捕捉每一次真实请求,先聚合,后采样,再上报,提供精准的指标监控。确保在采样策略开启的情况下,指标数据依然与真实情况保持一致。


图片
图:ARMS 端侧预聚合能力


为了降低 APM 工具带来的应用性能损耗,茶百道对大部分应用采取 10% 采样率,对于 TPS 非常高的应用则采取自适应采样策略,进一步降低高峰期应用性能损耗。通过实测,在业务高峰期,ARMS 应用监控造成的应用性能损耗比开源产品低 30% 以上且指标数据准确性可信赖, 比如接口级别的平均响应时间、错误数等指标都可以满足生产级业务需求。


图片
图:接口级别指标数据


异步链路自动埋点*


在 Java 领域存在异步线程池技术,以及众多开源异步框架,比如 RxJava、Reactor Netty、Vert.x 等。相较于同步链路,异步链路的自动埋点与上下文透传的技术难度更大。开源产品对主流异步框架的覆盖度不全,在特定场景下存在埋点失败问题,一旦出现这样的问题,APM 工具最重要的链路分析能力就难以发挥作用。


在这种情况下,需要开发者自行通过 SDK 手工埋点,以保证异步链路的上下文透传。这就会造成巨大的工作量且难以在团队内部大面积、快速推广。


ARMS 对主流的异步框架都实现了支持,无需任何业务代码上的侵入就能够异步链路上下文透传,即使对一些异步框架的特定版本没有及时支持,只要用户侧提出需求,ARMS 团队就能在新版本的探针中补齐。使用 ARMS 应用监控之后,茶百道技术团队直接将此前异步框架手工埋点代码进行了清理,大幅度减少维护工作量。


图片


图:异步调用的链路上下文


更高阶应用诊断技术的运用


在埋点覆盖度足够高的情况下,传统 APM 工具和链路跟踪工具能够帮助用户快速确定链路的哪一个环节(也就是Span)存在性能瓶颈,但需要更进一步排查问题根源时,就无法提供更有效的帮助了。


举一个例子,当系统 CPU 占用率显著提升时,是否因某个业务方法疯狂的消耗 CPU 资源所导致?这个问题对于大多数的 APM 产品而言,都是难以办法解决的。因为单从链路视图无法知晓每个环节的资源消耗情况。茶百道的工程师在使用开源工具时,曾多次遇到类似问题,当时只能凭借经验去猜测,再去测试环境反复对比来彻底解决,虽然也试过一些 Profiling 工具,但使用门槛比较高,效果不是很好。


ARMS 应用监控提供了 CPU & 内存诊断能力,可以有效发现 Java 程序中因为 CPU、内存和 I/O 导致的瓶颈问题,并按照方法名称、类名称、行号进行细分统计,最终协助开发者优化程序、降低延迟、增加吞吐、节约成本。CPU & 内存诊断可以在需要排查特定问题时临时开启,并通过火焰图帮助用户直接找到问题根源。在一次生产环境某应用 CPU 飙升场景中,茶百道的工程师通过 CPU & 内存诊断一步定位到问题是由一个特定业务算法所导致。


图片
图:通过火焰图分析 CPU 时间


此外,对于线上的业务问题,还可以通过 ARMS 提供的 Arthas 诊断能力在线排查。Arthas 作为诊断 Java 领域线上问题诊断利器,利用字节码增强技术,可以在不重启 JVM 进程的情况下,查看程序运行情况。


虽然 Arthas 使用有一定门槛,需要投入比较多精力进行学习,但茶百道的工程师非常喜欢使用这个工具。针对“到底符合哪种特殊的数据导致某业务异常”此类问题,没有比 Arthas 更方便的排查工具了。


图片


阶段性成果


经过 2 个月时间的调研与对比,茶百道决定全面从开源可观测平台转向 ARMS,从开源压测平台转向 PTS,并在团队内部进行推广。**随着使用的不断深入,ARMS 所提供的智能洞察、线程池分析等高阶可观测能力也逐步被茶百道的技术团队应用于日常运维中,线上问题排查效率相比之前也有了数倍提升。


在可观测产品本身的使用成本上,虽然表面上 ARMS 相比开源产品有所提高,但这是建立在开源方案数据单写,以及存在单点故障的情况下。其实茶百道的技术团队也非常清楚,之前的开源方案是存在高可用性隐患的,某个组件的故障会导致整个可观测方案不可用。只是大家对于开源方案提供的可观测能力并没有重度使用,所以才没有足够重视。所以综合来看,ARMS 整体成本并不会高于开源方案。


利用 ARMS 能力,茶百道实现了可观测指标采样率百分百覆盖,链路全采集,监控数据准确率大幅提供,能够快速实现业务故障的自动发现,有效的配合敏态业务发展。


故障发生后,监控系统需要第一时间通知相关人员,做初步定位,ARMS 告警告警能力实现了 ChatOps 能力,基于 IM 工具,快速触达相关人员,并且提供初步定位能力,是故障的响应能力大幅提升。


故障的快速恢复,对于控制业务影响至关重要,ARMS 利用全链路 Trace 能力,快速定位具体应用、接口、方法、慢sql等,是故障快速恢复的关键助手。茶百道技术团队负责人表示: “在与开源方案成本持平的前提下,ARMS 丰富且全面的全栈观测与告警能力,使茶百道快速建立运维观测与响应能力,故障恢复效率提升 50% 以上,故障恢复耗时****缩短 50%,真正做到用可观测为业务迅猛发展保驾护航。”


故障的预防收敛,在稳定性体系建设中是投入产出比极高的,PTS 利用全国流量施压的能力,和秒级监控能力,验证站点容量并定位性能瓶颈。茶百道在业务上线前,充分对单应用和全链路做压测,累计压测 800 余次,在上线前做到了性能问题的收敛,避免演进为线上故障。


下阶段目标


在可观测领域,Prometheus + Grafana 是指标数据存储、计算、查询、展示的事实标准,ARMS 产品家族提供托管加强的 Prometheus 和 Grafana 服务。ARMS 应用监控生成的指标数据也会自动保存到托管版 Prometheus 中,并预置数张 Grafana 大盘。茶百道的工程师们正在基于 Prometheus 和 Grafana,将应用层指标、关键业务指标、云服务指标进行结合,开发多维度可观测大盘。


在不久的将来,茶百道就会建立覆盖业务层、用户体验层、应用服务层、基础设置层、云服务层的统一可观测技术体系,为千万级用户同时在线的大规模微服务系统实现稳定性保障。


作者:阿里云云原生
来源:juejin.cn/post/7289767547329970231
收起阅读 »

领导说我工作 3 年了只会 CRUD

在老东家工作 3 年了,公司的业务和技术栈相对熟练得差不多了。 领导觉得我能够委以重任,便把一个新项目交给我负责,另外指派一名同事协助我。 项目的重点在于数据的交互比较多,以及每天大量的数据同步和批量操作,不能出错。 队友建议以短、平、快为主,能够使用已有现成...
继续阅读 »

在老东家工作 3 年了,公司的业务和技术栈相对熟练得差不多了。


领导觉得我能够委以重任,便把一个新项目交给我负责,另外指派一名同事协助我。


项目的重点在于数据的交互比较多,以及每天大量的数据同步和批量操作,不能出错。


队友建议以短、平、快为主,能够使用已有现成的技术就用现成的技术。直接面向过程开发是人们最为舒适,是人为本能的习惯。由于他有这一种能够处理好的决心,便把数据批量操作这块委托于他。


查看了以往公司现成一些写法,一部分是直接面向 SQL 写法批量插入,面对增量同步则先查出,存在的更新,不存在的插入。一部分是通过 Kafka 和后台任务原子操作。


理论上这么操作结果也能成,但是看到修改记录,我就知道面临的需求变了很多变化很快,导致大量的更改。私底下询问负责人也了解出了太多问题,原本一劳永逸赶紧写完结果反而投入了更多的精力和时间。


出于预防心理,也对那位同事进行了提醒并且加以思考再下手。


不到一个月,我们就把项目上线了,并且没有出现数据上的错误,得到了领导的表扬。


我们也提前收场,做一些小的优化,其余时间在摸鱼。


一段时间之后,麻烦便接踵而至,其一就是开始数据量暴增,那位同事在做增量同步时进行了锁表操作,批量操作需要一些时间,在前台读取时出现响应超时。


其二就是增量同步要调整,以主库或第三方来源库为主,出现数据更新和删除的需要同步操作。


同事目前的主力放在了新项目上,把一些零散的时间用来调整需求和 bug,结果越处理,bug 出现的越多,不是数量过多卡死就是变量不对导致数据处理不对。


于是到了某一时刻终于爆发,领导找到我俩,被痛批一顿,工作这么久就只会 CRUD 操作,来的实习生都会干的活,还养你们干什么。


当然,要复盘的话当然有迹可循。我想碰见这种情况还真不少,首次开发项目时一鼓作气,以“短、平、快” 战术面向过程开发,短时间内上线。


但是,一个软件的生命周期可不止步于上线,还要过程运维以及面对变化。


导致在二次开发的时候就脱节了,要么当时写法不符合现有业务,要么改动太多动不动就割到了大动脉大出血,要么人跑了...


所以我们会采用面向对象,抽象化编程,就是用来保稳定,预留一部分来应付变化,避免牵一发而动全身。


挨完骂,也要开始收拾烂摊子。


于是我打算重新组装一个通用的方法,打算一劳永逸。


首先我们定义一个接口通用思维 IDbAsyncBulk。由于源码已经发布到了github,所以一些注释写成了英文,大致也能看出蹩脚英文的注释。


public interface IDbAsyncBulk
    {
        /// <summary>
        /// default init.
        /// use reflect to auto init all type, to lower case database fileds,and  default basic type.
        /// if ignore some fileds,please use DbBulk,Ignore property to remarkable fileds.
        /// if other operating,need user-defined to init operate.
        /// </summary>
        /// <typeparam name="T">Corresponding type</typeparam>
        Task InitDefaultMappings<T>();

        /// <summary>
        /// batch operating
        /// </summary>
        /// <typeparam name="T">will operate object entity type.</typeparam>
        /// <param name="connection_string">database connecting string.</param>
        /// <param name="targetTable">target table name. </param>
        /// <param name="list">will operate data list.</param>
        Task CopyToServer<T>(string connection_string, string targetTable, List<T> list);

        /// <summary>
        /// batch operating
        /// </summary>
        /// <typeparam name="T">will operate object entity type.</typeparam>
        /// <param name="connection">database connecting string.need to check database connecting is openning.
        /// if nothing other follow-up operate, shouldn't cover this connecting.</param>
        /// <param name="targetTable">target table name.</param>
        /// <param name="list">will operate data list.</param>
        Task CopyToServer<T>(DbConnection connection, string targetTable, List<T> list);

        /// <summary>
        /// renew as it exists,insert as it not exists.
        /// follow up : 
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection_string">connecting string</param>
        /// <param name="keys">mapping orignal table and target table fileds,need primary key and data only,if not will throw error.</param>
        /// <param name="targetTable">target table name.</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="insertmapping">need to insert column,if is null,just use Mapping fileds,in order to avoid auto-create column</param>
        /// <param name="updatemapping">need to modify column,if is null,just use Mapping fileds</param>
        Task MergeToServer<T>(string connection_string, List<string> keys, string targetTable, List<T> list, string tempTable = null, List<string> insertmapping = null, List<string> updatemapping = null);

        /// <summary>
        /// renew as it exists,insert as it not exists.
        /// follow up : 
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection">database connecting string.need to check database connecting is openning.</param>
        /// <param name="keys">mapping orignal table and target table fileds,need primary key and data only,if not will throw error.</param>
        /// <param name="targetTable">target table name.</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="insertmapping">need to insert column,if is null,just use Mapping fileds,in order to avoid auto-create column</param>
        /// <param name="updatemapping">need to modify column,if is null,just use Mapping fileds</param>
        Task MergeToServer<T>(DbConnection connection, List<string> keys, string targetTable, List<T> list, string tempTable = null, List<string> insertmapping = null, List<string> updatemapping = null);

        /// <summary>
        ///  batch update operating。
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection_string">connecting string</param>
        /// <param name="where_name">matching 'where' compare fileds.</param>
        /// <param name="update_name">need to update fileds.</param>
        /// <param name="targetTable">target table name</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        Task UpdateToServer<T>(string connection_string, List<string> where_name, List<string> update_name, string targetTable, List<T> list, string tempTable = null);

        /// <summary>
        ///  batch update operating。
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection_string">connecting string</param>
        /// <param name="where_name">matching 'where' compare fileds.</param>
        /// <param name="update_name">need to update fileds.</param>
        /// <param name="targetTable">target table name</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="createtemp"> create temporary table or not </param>
        Task UpdateToServer<T>(DbConnection connection, List<string> where_name, List<string> update_name, string targetTable, List<T> list, string tempTable = nullbool createtemp = true);

        /// <summary>
        /// renew as it exists,insert as it not exists.original table not exist and  target table exist will remove.
        /// 1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// 4.will remove data that temporary data not exist and target table exist.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection_string">connecting string</param>
        /// <param name="keys">mapping orignal table and target table fileds,need primary key and data only,if not will throw error.</param>
        /// <param name="targetTable">target table name</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="insertmapping">need to insert column,if is null,just use Mapping fileds,in order to avoid auto-create column</param>
        /// <param name="updatemapping">need to modify column,if is null,just use Mapping fileds</param>
        Task MergeAndDeleteToServer<T>(string connection_string, List<string> keys, string targetTable, List<T> list, string tempTable = null, List<string> insertmapping = null, List<string> updatemapping = null);

        /// <summary>
        /// renew as it exists,insert as it not exists.original table not exist and  target table exist will remove.
        ///  1.create temporary table
        /// 2.put data into temporary table.
        /// 3.merge data to target table.
        /// 4.will remove data that temporary data not exist and target table exist.
        /// </summary>
        /// <typeparam name="T">data type</typeparam>
        /// <param name="connection">database connecting string.need to check database connecting is openning.</param>
        /// <param name="keys">mapping orignal table and target table fileds,need primary key and data only,if not will throw error.</param>
        /// <param name="targetTable">target table name</param>
        /// <param name="list">will operate data list.</param>
        /// <param name="tempTable">put data into temporary table,default name as 'target table name + # or _temp'</param>
        /// <param name="insertmapping">need to insert column,if is null,just use Mapping fileds,in order to avoid auto-create column</param>
        /// <param name="updatemapping">need to modify column,if is null,just use Mapping fileds</param>
        Task MergeAndDeleteToServer<T>(DbConnection connection, List<string> keys, string targetTable, List<T> list, string tempTable = null, List<string> insertmapping = null, List<string> updatemapping = null);

        /// <summary>
        /// create temporary table
        /// </summary>
        /// <param name="tempTable">create temporary table name</param>
        /// <param name="targetTable">rarget table name</param>
        /// <param name="connection">database connecting</param>
        Task CreateTempTable(string tempTable, string targetTable, DbConnection connection);
    }

解释几个方法的作用:



InitDefaultMappings:初始化映射,将目标表的字段映射到实体,在批量操作时候会根据反射进行一一匹配表字段;


CopyToServer:批量新增,在符合数据表结构时批量复制到目标表,采用官方 SqlBulkCopy 类结合实体简化操作。


MergeToServer:增量同步,需指定唯一键,存在即更新,不存在则插入。支持指定更新字段,指定插入字段。


UpdateToServer:批量更新,需指定 where 条件,以及更新的字段。


MergeAndDeleteToServer:增量同步,以数据源和目标表进行匹配,目标表存在的则更新,不存在的则插入,目标表存在,数据源不存在则目标表移除。


CreateTempTable:创建临时表。



增加实体属性标记,用来标记列名是否忽略同步数据,以及消除数据库别名,大小写的差异。


 /// <summary>
    /// 数据库批量操作标记,用于标记对象属性。
    /// </summary>
    public class DbBulkAttribute : Attribute
    {
        /// <summary>
        /// 是否忽略。忽略则其余属性不需要设置,不忽略则必须设置Type。
        /// </summary>
        public bool Ignore { getset; }

        /// <summary>
        /// 列名,不设置则默认为实体字段名小写
        /// </summary>
        public string ColumnName { getset; }

    }

实现类,目前仅支持 SqlServer 数据库,正在更新 MySql 和 PGSql 中。然后需要定义BatchSize(default 10000)、BulkCopyTimeout (default 300)、ColumnMappings,分别是每批次大小,允许超时时间和映射的字段。


/// <summary>
    /// sql server batch
    /// </summary>
    public class SqlServerAsyncBulk : IDbAsyncBulk
    {
        /// <summary>
        /// log recoding
        /// </summary>
        private ILogger _log;
        /// <summary>
        ///batch insert size(handle a batch every time )。default 10000。
        /// </summary>
        public int BatchSize { getset; }
        /// <summary>
        /// overtime,default 300
        /// </summary>
        public int BulkCopyTimeout { getset; }
        /// <summary>
        /// columns mapping
        /// </summary>
        public Dictionary<stringstring> ColumnMappings { getset; }
        /// <summary>
        /// structure function
        /// </summary>
        /// <param name="log"></param>
        public SqlServerAsyncBulk(ILogger<SqlServerAsyncBulk> log)
        {
            _log = log;
            BatchSize = 10000;
            BulkCopyTimeout = 300;
        }
        
        //...to do

使用上也非常的简便,直接在服务里注册单例模式,使用的时候直接依赖注入。


 //if you use SqlServer database, config SqlServerAsyncBulk service.
services.AddSingleton<IDbAsyncBulk, SqlServerAsyncBulk>();

public class BatchOperate
{
  private readonly IDbAsyncBulk _bulk;
  public BatchOperate(IDbAsyncBulk bulk)
  {
    _bulk = bulk;
  }
}

以 user_base 表举两个实例,目前测试几十万数据也才零点几秒。


 public async Task CopyToServerTest()
        {
            var connectStr = @"Data Source=KF009\SQLEXPRESS;Initial Catalog=MockData;User ID=xxx;Password=xxx";
            await _bulk.InitDefaultMappings<UserBaseModel>();
            var mock_list = new List<UserBaseModel>();
            for (var i = 0; i < 1000; i++) {
                mock_list.Add(new UserBaseModel
                {
                    age = i,
                    birthday = DateTime.Now.AddMonths(-i).Date,
                    education = "本科",
                    email = "xiaoyu@163.com",
                    name = $"小榆{i}",
                    nation = "
",
                    nationality="
中国"
                });
            }
            await _bulk.CopyToServer(connectStr, "
user_base", mock_list);
        }

public async Task MergeToServerTest()
        {
            var connectStr = @"Data Source=KF009\SQLEXPRESS;Initial Catalog=MockData;User ID=sa;Password=root";
            await _bulk.InitDefaultMappings<UserBaseModel>();
            var mock_list = new List<UserBaseModel>();
            for (var i = 0; i < 1000; i++)
            {
                mock_list.Add(new UserBaseModel
                {
                    age = i,
                    birthday = DateTime.Now.AddMonths(-i).Date,
                    education = "本科",
                    email = "mock@163.com",
                    name = $"小榆{i}",
                    nation = "汉",
                    nationality = "中国"
                });
            }
            var insertMapping = new List<string> { "birthday""education""age""email""name""nation""nationality" };
            var updateMapping = new List<string> { "birthday""education""age""email"};
            await _bulk.MergeToServer(connectStr,new List<string> {"id"}, "user_base", mock_list,null, insertMapping, updateMapping);
        

到这里,也已经完成了批量数据操作啦,不用再面对大量的sql操作啦。面向 sql 开发一时确实爽,但是面临变化或者别人接手的时候,是很痛苦的。


具体实现细节内容过多,篇幅有限暂时不全部展示,有兴趣或者尝试的伙伴可以进 github 进行参考。



github👉:github.com/sangxiaoyu/… 💖



作者:桑小榆呀
来源:juejin.cn/post/7290361767141376057
收起阅读 »

为网站配置SSL

HTTPS (全称:Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。HTTPS 在HTTP 的基础下加...
继续阅读 »

HTTPS (全称:Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。HTTPS 在HTTP 的基础下加入SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。 HTTPS 存在不同于 HTTP 的默认端口及一个加密/身份验证层(在 HTTP与 TCP 之间),这个系统提供了身份验证与加密通讯方法。



现状

















证书申请




除了向公有云申请证书, 也可使用 自签名openssl生成的证书,但方便起见还是使用云厂商提供的证书.








一般免费版,只有一年有效期.到期需要重新申请&更换














Nginx配置




将证书文件上传至/usr/local/openresty/nginx/conf/cert目录下.




博客项目当前的conf配置如下:


server {
    listen      80;
    server_name dashen.tech www.dashen.tech;
    access_log  /var/log/blog.access.log main;
    error_log  /var/log/blog.error.log;

  location / {
        root        /home/ubuntu/cuishuang.github.io;
        index       index.html;
        expires     1d;
        add_header  Cache-Control public;
        access_log  off;
    }
}


新增启用https的配置:
server {
      listen        443 ssl;                                                 
      server_name    dashen.tech www.dashen.tech;  #域名                         
      ssl_certificate      /usr/local/openresty/nginx/conf/cert/shuang_blog.pem;  #证书路径     
      ssl_certificate_key  /usr/local/openresty/nginx/conf/cert/shuang_blog.key;  #key路径             
      ssl_session_cache    shared:SSL:1m;   #s储存SSL会话的缓存类型和大小                       
      ssl_session_timeout  5m; #会话过期时间 

      access_log  /var/log/blog.access.log main;
      error_log  /var/log/blog.error.log;

location / {
        root        /home/ubuntu/cuishuang.github.io;
        index       index.html;
        expires     1d;
        add_header  Cache-Control public;
        access_log  off;
    }                                                     
  }



删掉之前的conf. 重启nginx,访问https://www.dashen.tech[1],已能正常访问.




再访问之前的网址https://dashen.tech[2],则







配置将http访问自动跳转到https




再增加一段配置:


server {
    listen      80;
    server_name dashen.tech www.dashen.tech;
    access_log  /var/log/blog.access.log main;
    error_log  /var/log/blog.error.log;

    return      301 https://$server_name$request_uri; #这是nginx最新支持的写法

  location / {
        root        /home/ubuntu/cuishuang.github.io;
        index       index.html;
        expires     1d;
        add_header  Cache-Control public;
        access_log  off;
    }
}

参考: Nginx强制跳转Https[3]


再次重启nginx,这时请求https://dashen.tech[4]就可以跳转到https://www.dashen.tech[5]




但因为网站下有部分资源使用了http,所以浏览器依然没有变为安全锁,


可参考Hexo启用https加密连接[6],


也可右键查看哪些请求使用了http,将其修改为https即可~





参考资料


[1]

https://www.dashen.tech: https://www.dashen.tech

[2]

https://dashen.tech: https://dashen.tech

[3]

Nginx强制跳转Https: https://www.jianshu.com/p/116fc2d08165

[4]

https://dashen.tech: https://dashen.tech

[5]

https://www.dashen.tech: https://www.dashen.tech

[6]

Hexo启用https加密连接: https://note.youdao.com/web/#/file/recent/note/WEBe69d252eb353dd5ee0210d053ec0cc3a/



作者:fliter
来源:mdnice.com/writing/3257fabc35eb44a7a9be93bd809ffeca
收起阅读 »

lstio在微服务框架中的使用

在云原生时代,微服务架构已经成为企业构建灵活、可扩展和高可用系统的首选方案。但是,微服务也带来了一系列新的挑战,包括服务发现、负载均衡、安全、监控等。Istio是一款开源的服务网格,它通过提供丰富的特性帮助开发者轻松应对这些挑战。在本文中,我们将探索Istio...
继续阅读 »

在云原生时代,微服务架构已经成为企业构建灵活、可扩展和高可用系统的首选方案。但是,微服务也带来了一系列新的挑战,包括服务发现、负载均衡、安全、监控等。Istio是一款开源的服务网格,它通过提供丰富的特性帮助开发者轻松应对这些挑战。在本文中,我们将探索Istio在微服务框架中的使用原理,并深入分析与Spring Cloud的集成案例。


Istio的使用原理


Istio通过将智能代理(Envoy)注入到每个微服务的Pod中,从而实现微服务之间的网络通信的拦截和管理。Envoy代理负责处理服务与服务之间的交互,这样就能在不修改微服务业务代码的情况下,实现流量管理、安全、监控等功能。


源码结构


Istio的源码由Go语言编写,主要包括以下组件:



  1. Envoy Proxy:由C++编写,负责流量的代理和管理。

  2. Pilot:提供服务发现和流量管理功能。

  3. Mixer:负责策略控制和遥测数据收集。

  4. Citadel:提供服务间通信的安全认证和授权功能。


实例:Istio与Spring Cloud集成


以下是一个简单的示例,展示如何在Spring Cloud微服务中使用Istio。


在Istio和Spring Cloud的集成场景中,你需要在Kubernetes集群中部署Spring Cloud应用,并且确保Istio的Envoy代理被注入到应用的Pod中。以下是一个详细步骤和配置说明。


1. 安装Istio


首先确保你已经在Kubernetes集群中安装了Istio并启用了自动sidecar注入。如果还没有安装,可以按照Istio的官方文档进行安装和配置。


2. 准备Spring Cloud应用的Docker镜像


确保你的Spring Cloud应用已经被打包为Docker镜像,并推送到Docker镜像仓库中。例如:


docker build -t myrepo/springcloud-service:v1 .
docker push myrepo/springcloud-service:v1

3. 创建Kubernetes Deployment配置文件**


创建一个YAML配置文件,用于部署Spring Cloud应用。注意,我们在Pod的metadata.annotations中添加了sidecar.istio.io/inject: "true",用于启用Istio sidecar自动注入。


例如,创建一个名为springcloud-service-deployment.yaml的文件,内容如下:


apiVersion: apps/v1
kind: Deployment
metadata:
name: springcloud-service
spec:
replicas: 3
selector:
matchLabels:
app: springcloud-service
template:
metadata:
labels:
app: springcloud-service
annotations:
sidecar.istio.io/inject: "true" # 启用Istio sidecar自动注入
spec:
containers:
- name: springcloud-service
image: myrepo/springcloud-service:v1 # 使用你的Spring Cloud应用镜像
ports:
- containerPort: 8080 # 应用的端口号

4. 部署Spring Cloud应用到Kubernetes集群**


使用kubectl命令行工具部署应用:


kubectl apply -f springcloud-service-deployment.yaml

这会在Kubernetes集群中创建一个新的Deployment,运行你的Spring Cloud应用,并且每个Pod中都会注入Istio的Envoy代理。


5. 检查部署状态**


使用以下命令查看Pod的状态,确保所有Pod都已经正常运行,并且Istio的Envoy代理也被正确注入。


kubectl get pods

你应该能看到类似如下的输出,其中2/2表示每个Pod中有两个容器(你的应用和Istio Envoy代理)都已经正常运行。


NAME                                       READY   STATUS    RESTARTS   AGE
springcloud-service-5c79df6f59-9fclp 2/2 Running 0 5m
springcloud-service-5c79df6f59-g6qlc 2/2 Running 0 5m
springcloud-service-5c79df6f59-wvndz 2/2 Running 0 5m

至此,你的Spring Cloud应用已经被成功部署在Istio服务网格中,可以利用Istio提供的各种特性来管理和监控应用的运行。


配置Istio资源


当你的 Spring Cloud 应用已经成功部署在 Istio 服务网格中后,你需要配置 Istio 资源以管理服务间的流量、安全、策略和遥测等功能。以下我们将具体说明如何配置 Istio 资源,主要涉及 VirtualService 和 DestinationRule。


1. VirtualService**


VirtualService 定义了访问一个服务的路由规则。你可以控制根据不同的请求属性(例如 URL、请求头等)将流量路由到不同的服务或服务的不同版本。


创建 VirtualService 配置文件


以下是一个 virtual-service.yaml 的示例,该文件定义了一个简单的路由规则,将所有发送到 springcloud-service 服务的流量路由到标有 "v1" 标签的 Pod。


apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: springcloud-service
spec:
hosts:
- springcloud-service
http:
- route:
- destination:
host: springcloud-service
subset: v1

这里 hosts 定义了这个 VirtualService 的作用域,即它将控制哪些服务的流量。http 定义了 HTTP 流量的路由规则,destination 定义了匹配的流量将被路由到哪里。


2. DestinationRule


DestinationRule 定义了 Pod 的子集和对这些子集的流量的策略。通常和 VirtualService 一起使用,用于细粒度控制流量。


创建 DestinationRule 配置文件


以下是一个 destination-rule.yaml 的示例,定义了 springcloud-service 服务的两个子集:v1 和 v2。


apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: springcloud-service
spec:
host: springcloud-service
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2

在这里,我们定义了两个子集 v1 和 v2,分别匹配标签为 version=v1version=v2 的 Pod。这样你就可以在 VirtualService 中使用这些子集来控制流量。


应用 Istio 配置


将 VirtualService 和 DestinationRule 的配置文件应用到 Kubernetes 集群中:


kubectl apply -f virtual-service.yaml
kubectl apply -f destination-rule.yaml

通过配置 Istio 的 VirtualService 和 DestinationRule,您可以轻松控制和管理在 Istio 服务网格中运行的 Spring Cloud 应用的流量。你可以实现各种高级的流量管理功能,如金丝雀发布、蓝绿部署、流量镜像、故障注入等,而无需更改应用的代码。


代码示例


下面是一个基于Spring Cloud的简单微服务应用代码示例。


@RestController
public class HelloWorldController {

@RequestMapping("/hello")
public String hello() {
return "Hello, World!";
}

}

在Istio环境中,你不需要更改Spring Cloud应用的代码。Envoy代理会自动处理服务间的通信,你只需要使用Istio的配置文件定义流量路由规则、策略等。


总结


Istio与Spring Cloud的结合为开发者提供了一种强大的方式来部署、管理和扩展微服务应用。Istio的流量管理、安全认证和遥测数据收集功能使得开发者能够更加关注业务逻辑的开发,而不是底层的网络通信和安全问题。希望这个更详细的指南能帮助你更好地理解Istio与Spring Cloud的集成。


作者:一只爱撸猫的程序猿
来源:juejin.cn/post/7290485584069001231
收起阅读 »

从拼夕夕砍一刀链接漫谈微信落地页防封

写在前面 最近v2ex上一个话题火了,大概内容是有一个 好奇 摸鱼的程序员,在分析了拼夕夕发出的砍一刀短链接后,惊呼不可能。 是什么让一个见多识广的程序员如此惊讶呢?问题就出在拼夕夕发出的短链接上,经过测试发现,在微信内打开的短链,会出现二维码的页面,而在p...
继续阅读 »

写在前面


最近v2ex上一个话题火了,大概内容是有一个 好奇 摸鱼的程序员,在分析了拼夕夕发出的砍一刀短链接后,惊呼不可能。


image.png
是什么让一个见多识广的程序员如此惊讶呢?问题就出在拼夕夕发出的短链接上,经过测试发现,在微信内打开的短链,会出现二维码的页面,而在pc端浏览器打开时,则出现的另外一套界面。是什么导致了这样的情况呢?

微信落地页防封


谈到拼多多的短链分享,就不得不提一个很关键的名词微信落地页防封 ,说到微信落地页防封,那就需要知道,在什么情况下,会触发微信的域名拦截机制,一般来说,触发域名拦截有以下几个原因




  • 域名是新购入的老域名,在微信内之前有过违规记录,上过黑名单。




  • 网站流量太大,微信内同一域名被大量分享,比如分享赚类的平台某拼。




  • 诱导分享传播,即便是合法营销活动,也会触发拦截。




  • 网站内容违规,这个不必多说。




  • 被同行恶意举报。




为了让域名活的久一些,微信落地页防封这样的技术就应运而生,主要通过以下几点,来逃避微信的域名拦截机制



  • 大站域名【美团、京东...】

  • 不同主体各自备案域名【鸡蛋不放在一个篮子内】

  • 多级跳转+前置防火墙【通过前置防火墙中转页识别是否是机器扫描】

  • 随机Ip【cdn分发】

  • 图床 + 短链

  • 短链 + 自定义跳转 【稍后详细分析一下这种方式】


拼夕夕的防封技术猜测


经过测试,拼夕夕的防封应该采用的是图床+短链+自定义跳转的方式,接下来就听我一一道来



  • 图床
    图床是oss对象存储的昵称,通常是用来存放图片的,如果是用在防封里,那他其实是将一个html页面上传进了图床内,至于是怎么上传进去的。很简单啊,你只需要有一个阿里云,京东云,腾讯云的账号,购买了oss对象存储服务,设置公共读私有写,就可以访问了,这些不重要,你只需要知道图床所存储的是html就可以了。


我通过chrome的控制台抓取了通过短链转换而来地址,然后抓到了如下请求



  • 短链重定向


image.png
注意看第一个请求,第一个请求就是短链的自定义跳转,短链自定义跳转我们下一节详细去说,通过301重定向,将我们重定向到了图床的地址



  • 图床ua、地域、等判断
    图床内的html包含了对ua、地域、设备类型等的判断,不同的环境所打开的内容是不同的,通过对环境的判断,展示不同的内容去屏蔽微信的扫描,拼夕夕就是通过这样的方式来实现落地页防封的
    下面是我从落地页中拿到的一个函数,虽然我们很难完全还原这个函数,但是通过里面没被混淆的常量比如ke.HUAWEIke.OPPO等不难看出来,这是一个判断当前手机品牌的函数,针对不同的品牌下的浏览器,会做一些特殊的处理。


 const t = e(u().mark(function t (e) {
let r, n, o, i, c, s
return u().wrap(function (t) {
for (; ;) {
switch (t.prev = t.next) {
case 0:
if (r = e.brand,
n = e.payload,
o = a()(n, 'data', {}),
i = a()(n, 'isThirdBrowser'),
c = a()(n, 'data.fastAppDomains', ''),
s = Te(o),
Pe(o),
!i) {
t.next = 8
break
}
return t.abrupt('return')
case 8:
if (r !== ke.HUAWEI) {
t.next = 11
break
}
return t.next = 11,
R(c, {
cTime: s,
data: o
}).catch(fn)
case 11:
if (r !== ke.OPPO) {
t.next = 27
break
}
if (!j(A.OppoLeftScreen, o)) {
t.next = 17
break
}
return t.next = 15,
R(c, {
cTime: s,
data: o
}).catch(fn)
case 15:
case 20:
t.next = 27
break
case 17:
return t.prev = 17,
t.next = 20,
sn(c, {
cTime: s,
data: o
})
case 22:
if (t.prev = 22,
t.t0 = t.catch(17),
!j(A.banBrowserV2, o) && !j(A.oppoQAppPriority, o)) {
t.next = 27
break
}
return t.next = 27,
R(c, {
cTime: s,
data: o
}).catch(fn)
case 27:
if (r !== ke.VIVO) {
t.next = 30
break
}
return t.next = 30,
sn(c, {
cTime: s,
data: o
}).catch(fn)
case 30:
case 'end':
return t.stop()
}
}
}
, t, null, [[17, 22]])
}
))

再注意看接下来的一段代码片段,很明显针对上面获取到的手机品牌,会生成不同的图片,注意看下面混淆过的c函数,x.brandType, brand有品牌的意思,也就是上面函数获取到的手机品牌


o = new Promise((function(t) {
var r, o = document.createElement("img"), i = k(n), c = (f(r = {}, x.brandType, 1),
f(r, E.funcParams, i),
r), u = a()(e.split(","), "0");
o.onload = function(e) {
var r = a()(e, "path[0]") || a()(e, "target")
, n = gn(r);
t({
brand: n,
img: r
})
}
,
o.onerror = function() {
t({
brand: ke.OTHERS
})
}
;
var s = S(u).href;
o.src = m(c, s)
}
)),

得益于落地页开发者优秀的代码命名习惯,通过下面的片段,isWeChatPlatform,isIOSWeChatPlatform这两个字符串让我们知道落地页里面还有针对微信的一些判断,会判断是安卓还是ios微信


n = a()(r, "data", {}),
i = a()(r, "isWeChatPlatform"),
c = a()(r, "isIOSWeChatPlatform"),
f = a()(r, "data.mqCodeKey", ""),
l = a()(r, "data.websiteDomain", "").replace(/\/$/, ""),
p = a()(r, "data.fastAppDomains", ""),
d = v("image_url"),
h = v(f) || location.href,
!d) {
t.next = 15;
break
}

还有落地页内针对UA的判断的实现


((w = t.document),
(x = w ? w.title : ''),
(_ = navigator.userAgent.toLowerCase()),
(S = navigator.platform.toLowerCase()),
(O = !(!S.match('mac') && !S.match('win'))),
(A = _.indexOf('wxdebugger') != -1),
(E = _.indexOf('micromessenger') != -1),
(I = _.indexOf('android') != -1),
(T = _.indexOf('iphone') != -1 || _.indexOf('ipad') != -1),
(P = function () {
const t = _.match(/micromessenger\/(\d+\.\d+\.\d+)/) || _.match(/micromessenger\/(\d+\.\d+)/)
return t ? t[1] : ''

通过上面的代码片段,我们得以窥见拼夕夕落地页的逻辑设计,落地页内,至少实现了下面的能力



  • 针对手机品牌的处理

  • 针对安卓与ios系统的处理

  • 针对是否微信的处理


这些代码进一步的验证了我们的猜想,拼夕夕的确是通过oss内的html动态创建元素来规避微信拦截的!下面是短链智能跳转的一个例子,可以帮助大家更好的理解短链推广的内在逻辑


短链与智能跳转


我们以某平台的功能为例,演示如何通过短链实现自定义的跳转



  • 创建短链接


image.png



  • 配置智能跳转


image.png



  • 智能跳转的规则


可以看到,本身规则就支持按平台,按访问环境,按地域去进行智能跳转了,这也是为什么谷歌会想要将UA的信息进行加密或减少所提供的的信息。


image.png



  • 按地域的实现
    服务器可以看到当前访问的ip,通过ip去反向推断地域

  • 操作系统、访问环境 是通过判断UA来实现


console.log(navigator.userAgent)
// ua内会包含设备的关键信息,如果是微信浏览器内打开的,会携带微信浏览器特有的ua信息
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'

结语



技术本身都是为了解决现实存在的问题,技术没有好坏黑白,但是作为一个技术人,我们能做的就是做任何事情的时候,要坚守心中的底线。君子不立危墙之下,尽量少游走在黑白间的灰色地带。



作者:AprilKroc
来源:juejin.cn/post/7156548454502629384
收起阅读 »