注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

前一阵闹得沸沸扬扬的IP归属地,到底是怎么实现的?

大家好,我是王老师,一直在准备写这篇稿子,但是事情太多一直耽误了,导致一直拖一直拖,结果就从最近变成了前一阵子。这下好了,不会有人说我蹭热度了。 大家都知道,前一阵子抖音和微博开始陆续上了IP归属地的功能,引起了众多热议。有大批在国外的老铁们开始"原形毕露...
继续阅读 »

大家好,我是王老师,一直在准备写这篇稿子,但是事情太多一直耽误了,导致一直拖一直拖,结果就从最近变成了前一阵子。这下好了,不会有人说我蹭热度了。


image.png


image.png


大家都知道,前一阵子抖音和微博开始陆续上了IP归属地的功能,引起了众多热议。有大批在国外的老铁们开始"原形毕露",被定位到国内来,那么IP归属到底是怎么实现的呢?那么网红们的归属地到底对不对呢?这篇文章帮大家揭晓。


一.第一步:如何拿到用户的真实IP


大家都知道,我们一般想访问公网,一般必须具备上网环境,那么我们开通宽带之后,运营商会给我们分配一个IP地址。一般IP地址我们都是自动分配的。所以我们不知道本机地址是什么?想知道自己的ip公网地址,可以通过百度搜索IP查看自己的ip位置
image.png


那么问题来了。百度是怎么知道我的公网IP的?


一般情况,用户访问我们的服务网络拓扑如下:


image.png


用户通过域名或者IP访问门户,然后请求到后端服务。这样的话后端服务就可以通过request.getRemoteAddr();方法获取用户的ip。


SpringBoot获取IP如下:


@RestController
public class IpController {

  @RequestMapping("/getIp")
  public String hello(HttpServletRequest request) {
      String ip = request.getRemoteAddr();
      System.out.println(ip);
      return ip;
  }
}

将服务部署到服务端,然后请求该接口,即可获取IP信息,如下图:


image.png


但是为什么我们获取的IP和百度搜出来的不一样呢?


1.1内网IP和外网IP


打开电脑CMD,输出ipconfig命令,查看本机的IP地址,发现我们本机地址和程序获取的地址是一样的。


image.png


其实,网络也是分内网IP和公网IP的。内网也成局域网。对于像公司,学校这种一般内部建立自己的局域网,对内部的信息进行传输时,都是通过内网相互通讯,建立局域网内网通讯节省了公网IP资源,并且通信效率也有很大的提升。当然非局域网内的设备则无法向内网的设备发送信息。


但是机器想要访问互联网的资源时,则需要机器拥有外网带宽,也就是我们所说的分配公网IP,负责也是无法访问互联网资源的。


image.png


因此,我们把服务部署在同一局域网内,客户端使用内网进行通信,因此获取的就是内网IP地址。但访问百度是需要使用公网访问,因此百度搜出来的IP就是公网IP地址。


1.2.为什么有时候获取到的客户端IP有问题?


当我们兴致勃勃的把IP获取的功能搞上去之后,发现获取的IP都是同一个?这是为什么呢?不可能只是一个用户在访问呀?查询IP信息之后发现,原来是我们部署的一台负载均衡的IP地址。


image.png


那么后端服务获取的地址都是负载均衡如nginx的地址。那么怎么透过负载均衡获取真实的地址呢?


透明的代理服务器在将客户端的访问请求转发到下一环节的服务器时,会在HTTP的请求头中添加一条X-Forwarded-For记录,用于记录客户端的IP,格式为X-Forwarded-For:客户端IP。如果客户端和服务器之间有多个代理服务器,则X-Forwarded-For记录使用以下格式记录客户端IP和依次经过的代理服务器IP:X-Forwarded-For:客户端IP, 代理服务器1的IP, 代理服务器2的IP, 代理服务器3的IP, ……


因此,常见的Web应用服务器可以通过解析X-Forwarded-For记录获取客户端真实IP。


public static String getIp(HttpServletRequest request) {
  String ip = request.getHeader("x-forwarded-for");

  if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
      ip = request.getRemoteAddr();
  } else if (ip.length() > 15) {
      //多次反向代理后会有多个ip值,第一个ip才是真实ip
      String[] ips = ip.split(",");
      for (int index = 0; index < ips.length; index++) {
          String strIp = ips[index];
          ip = strIp;
          break;
      }
  }
  return ip;
}

第二步:如何解析IP


IP来了,我们怎么解析呢:


IP的解析一般都要借助第三方软件使用了,第三方一般也分为离线库和在线库



  • 离线库支持的有如:IPIP,使用离线库的好处是解析效率高,性能好,问题就是IP库要经常更新。如果大家需要我私信我可以提供给大家比较新版本的ip库。

  • 在线库则各大云厂商接口能力都有支持。在线版本的好处是更新即时,问题就是接口查询性能和使用TPS有要求。


以下演示借助IP库离线IP解析方式:


借助IP库就可以帮我们实现ip地址的解析。


public static void main(String[] args) {
  IpAddrInfo IpAddrInfo = IPAddr.getInstance().putLocInfo("114.103.71.226");
  System.out.println(JSONObject.toJSONString(IpAddrInfo));
}

public IpAddrInfo putLocInfo(String ip) {
  IpAddrInfo info = new IpAddrInfo();
  if (StringUtils.isNotBlank(ip)) {
      try {
          DistrictInfo addrInfo = db.findInfo(ip, "CN");
          info.setCity(addrInfo.getCityName());
          info.setCountry(addrInfo.getCountryName());
          info.setCountryCode(addrInfo.getChinaAdminCode());
          info.setIsp(addrInfo.getIsp());
          info.setLat(addrInfo.getLatitude());
          info.setLon(addrInfo.getLongitude());
          info.setProvince(addrInfo.getRegionName());
          info.setTimeZone(addrInfo.getTimeZone());
          System.out.println(addrInfo.toString());
      } catch (IPFormatException e) {
          e.printStackTrace();
      } catch (InvalidDatabaseException e) {
          e.printStackTrace();
      }
  }
  return info;
}

image.png


其实IP的定位解析其实就是一个巨大的位置库,同时IP数量也是有限制的,因此同一个Ip也可能会分配到不同的区域,因此影响IP解析位置准确率的有几个方面
1、位置库不精准,导致解析偏差大或者地区字段确实
2、离线库更新不及时
并且海外的一般有专门的离线库去支持,使用同一套离线库并不一定支持海外IP的解析,所以本次受影响最大的海外网红门被解析到中国各个地区,被大家认为造假,当然也包括真的有造假。不过上线了这个功能也是有好处的,至少网络不是法外之地,大家也要有序的健康的冲浪,拒绝网络暴力。


好了,今天就到这里,我是王老狮,一个有想法有内涵的工程狮,关注我,学习更多技术知识。


收起阅读 »

三分钟,趁同事上厕所的时间,我覆盖了公司的正式环境数据

大家好啊,又跟大家见面了,最近有个需求就是批量修改公司的数据报表,正式环境!! 而且要执行update!! update it_xtgnyhcebg I set taskStatus = XXX 而且是没有加where条件的,相当于全表更新,这可马虎不得,我...
继续阅读 »

大家好啊,又跟大家见面了,最近有个需求就是批量修改公司的数据报表,正式环境!!
而且要执行update!!


update it_xtgnyhcebg I set taskStatus = XXX

而且是没有加where条件的,相当于全表更新,这可马虎不得,我们在任何操作正式数据库之前一定一定要对数据库备份!!不要问我怎么知道的,因为我就因为有一次把测试环境的数据覆盖到正式环境去了。。。


在这里插入图片描述


别到时候就后悔莫及,那是没有用的!


在这里插入图片描述
在这里插入图片描述


由于这个需求是需要在跨库操作的,所以我们在查询数据的时候需要带上库的名称,例如这样


SELECT
*
FROM
BPM00001.ACT_HI_PROCINST P
LEFT JOIN BPM00001.ACT_HI_VARINST V ON V.PROC_INST_ID_ = P.ID_
AND V.NAME_ = '__RESULE'


这样如果我们在任何一个库里面,只要在一个mysql服务里面都可以访问到这个数据
查出这个表之后
在这里插入图片描述
我们需要根据这里的内容显示出不同的东西
就例如说是“APPROVAL”我就显示“已通过”
这就类似与java中的Switch,其实sql也能实现这样的效果
如下:
在这里插入图片描述
这就是sql的case语句的使用
有了这些数据之后我们就可以更新数据表了,回到我们之前讨论过的,这是及其危险的操作
我们先把要set的值给拿出来
在这里插入图片描述


在这里插入图片描述
但是我们怎么知道这个里面的主键呢?
你如果直接这么加,肯定是不行的
在这里插入图片描述
所以我们需要在sql后面加入这样的一条语句
在这里插入图片描述
注意,这个语句一定要写在set语句的里面,这样sql就能依据里面判断的条件进行一一赋值
最后,将这个sql语句执行到生产库中


拓展:


作为查询语句的key绝对不能重复,否则会失败(找bug找了半天的人的善意提醒)
例如上面的语句中P.BUSINESS_KEY_必须要保证是唯一的!!


在这里插入图片描述
成功执行!!!
怎么样,这些sql

作者:掉头发的王富贵
来源:juejin.cn/post/7244563144671723576
的小妙招你学会了吗?

收起阅读 »

十年码农内功:经历篇

分享工作中重要的经历,可以当小说来看 一、伪内存泄漏排查 1.1 背景 我们原来有一个刚用 C++ 写的业务服务,迟迟不敢上线,原因是内存泄漏问题一直解决不了。现象是,服务上线后,内存一直慢慢每隔几秒上涨4/8KB,直到服务下线。 我刚入职不久,领导让我来查...
继续阅读 »

分享工作中重要的经历,可以当小说来看



一、伪内存泄漏排查


1.1 背景


我们原来有一个刚用 C++ 写的业务服务,迟迟不敢上线,原因是内存泄漏问题一直解决不了。现象是,服务上线后,内存一直慢慢每隔几秒上涨4/8KB,直到服务下线。


我刚入职不久,领导让我来查这个问题,非常具有挑战性,也是领导对我的考察!


1.2 分析


心路历程1:工具分析


使用 Valgrind 跑 N 遍服务,结果中都没有发现内存泄漏,但是有很多没有被释放的内存和很多疑似内存泄漏。实在没有发现线索。


心路历程2:逐个模块排查


工具分析失败,那就挨个模块翻看代码,并且逐个模块写demo验证该模块是否有泄漏(挂 Valgrind),很遗憾,最后还是没有找到内存泄漏。


心路历程3:不抛弃不放弃


这个时候两周快过去了,领导说:“找不到内存泄漏那就先去干别的任务吧”,感觉到一丝凉意,我说:“再给我点时间,快找到了”。这样顶着巨大压力加班加点的跑Valgrind,拿多次数据结果进行对比,第一份跑 10 分钟,第二份跑 20 分钟,看看有哪些差异或异常,寻找蛛丝马迹。遗憾的是还是没有发现内存泄漏在哪。


功夫不负有心人,看了 N 份结果后,对一个队列产生了疑问,它为啥这么大,队列长度 1000 万,直觉告诉我,它不正常。


去代码中找这个队列,确实在初始化的时候设置了 1000 万长度,这个长度太大了。


1.3 定位


进队列需要把虚拟地址映射到物理地址,物理内存就会增加,但是出队列物理内存不会立刻回收,而是保留给程序一段时间(当系统内存紧张时会主动回收),目的是让程序再次使用之前的虚拟地址更快,不需要再次申请物理内存映射了,直接使用刚才要释放的物理内存即可。


当服务启动时,程序在这 1000 万队列上一直不停的进/出队列,有点像貔貅,光吃不拉,物理内存自然会一直涨,直到貔貅跑到了队尾,物理内存才会达到顶峰,开始处在一个平衡点。


图1 中,红色代表程序占用的物理内存,绿色为虚拟内存。



图1


然而每次上线还没等 到达平衡点前就下线了,担心服务内存一直涨,担心出事故就停服务了。解决办法就是把队列长度调小,最后调到了 2 万,再上线,貔貅很快跑到了队尾,达到了平衡点,内存就不再增涨。


其实,本来就没有内存泄漏,这就是伪内存泄漏。


二、周期性事故处理


2.1 背景


我们有一个业务,2019 年到 2020 年间发生四次(1025、0322、0511 和 0629)大流量事故,事故时网络流量理论峰值 3000 Gbps,导致网络运营商封禁入口 IP,造成几百万元经济损失,均没有找到具体原因,一开始怀疑是服务器受到网络攻击。


后来随着事故发生次数增加,发现事故发生时间具有规律性,越发感觉不像是被攻击,而是业务服务本身的流量瞬间增多导致。服务指标都是事故造成的结果,很难倒推出事故原因。


2.2 猜想(大胆假设)


2.2.1 发现事故大概每50天发生一次


清晰记得 2020 年 7 月 15 日那天巡检服务时,我把 snmp.xxx.InErrors 指标拉到一年的跨度,如图2 发现多个尖刺的间距似乎相等,然后我就看了下各个尖刺时间节点,记录下来,并且具体计算出各个尖刺间的间隔记录在下面表格中。着实吓了一跳,大概是 50 天一个周期。并且预测了 8月18日 可能还有一次事故。



图2 服务指标


事故时间相隔天数
2019.09.05-
2019.10.2550天
2019.12.1450天
2020.02.0149天
2020.03.2250天
2020.05.1150天
2020.06.2949天
2020.08.18预计

2.2.2 联想50天与uint溢出有关


7 月 15 日下班的路上,我在想 3600(一个小时的秒数),86400(一天的秒数),50 天,5 x 8 等于 40,感觉好像和 42 亿有关系,那就是 uint(2^32),就往上面靠,怎么才能等于 42 亿,86400 x 50 x 1000 是 40 多亿,这不巧了嘛!拿出手机算了三个数:


2^32                  = 4294967296 
3600 * 24 * 49 * 1000 = 4233600000
3600 * 24 * 50 * 1000 = 4320000000

好巧,2^32 在后面的两个结果之间,4294967296 就是 49 天 16 小时多些,验证了大概每 50 天发生一次事故的猜想。



图3 联想过程


2.3 定位(小心求证)


2.3.1 翻看代码中与时间相关的函数


果然找到一个函数有问题,下面的代码,在 64 位系统上没有问题,但是在 32 位系统上会发生溢出截断,导致返回的时间是跳变的,不连续。图4 是该函数随时间输出的折线图,理想情况下是一条向上的蓝色直线,但是在 32 位系统上,结果却是跳变的红线。


uint64_t now_ms() {
struct timeval t;
gettimeofday(&t, NULL);
return t.tv_sec * 1000 + t.tv_usec / 1000;
}


图4 函数输出


这里解释一下,问题出在了 t.tv_sec * 1000,在 32 位系统上会发生溢出,高于 32 位的部分被截断,数据丢失。不幸的是我们的客户端有一部分是 32 位系统的。


2.3.2 找到出问题的逻辑


继续追踪使用上面函数的逻辑,发现一处问题,客户端和服务端的长链接需要发Ping保活,下一次发Ping时间等于上一次发Ping时间加上 30 秒,代码如下:


next_ping = now_ms() + 30000;

客户端主循环会不断判断当前时间是否大于 next_ping,当大于时发 Ping 保活,代码如下:


if (now_ms() > next_ping) {
send_ping();
next_ping = now_ms() + 30000;
}

那怎么就出现大流量打到服务器呢?举个例子,如图3,假如当前时间是 6月29日 20:14:00(20:14:26 时 now_ms 函数返回 0),now_ms 函数的返回值超级大。


那么 next_ping 等于 now_ms() 加上 30000(30s),结果会发生 uint64 溢出,反而变的很小,这就导致在接下来的 26 秒内,now_ms函数返回值一直大于 next_ping,就会不停发 Ping 包,产生了大量流量到服务端。


2.3.3 客户端实际验证


找到一个有问题的客户端设备,把它本地时间拨回 6月29日 20:13:00,让其自然跨过 20:14:26,发现客户端本地 log 中有大量发送 Ping 包日志,8 秒内发送 2 万多个包。证实事故原因就是这个函数造成的。解决办法是对 now_ms 函数做如下修改:


uint64_t now_ms() {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return uint64_t(t.tv_sec) * 1000 + t.tv_nsec / 1000 / 1000;
}

2.3.4 精准预测后面事故时间点


因为客户端发版周期比较长,需要做好下次事故预案,及时处理事故,所以预测了后面多次事故。


时间戳(ms)16进制北京时间备注
15719580303360x16E000000002019/10/25 07:00:30历史事故时间
15762529976320x16F000000002019/12/14 00:03:17不确定
15805479649280x170000000002020/02/01 17:06:04不确定
15848429322240x171000000002020/03/22 10:08:52历史事故时间
15891378995200x172000000002020/05/11 03:11:39历史事故时间
15934328668160x173000000002020/06/29 20:14:26历史事故时间
15977278341120x174000000002020/08/18 13:17:14精准预测事故发生
16020228014080x175000000002020/10/07 06:20:01精准预测事故发生
16063177687040x176000000002020/11/25 23:22:48精准预测事故发生

2.4 总结


该事故的难点在于大部分服务端的指标都是事故导致的结果,并且大流量还没有到业务服务,就被网络运营商封禁了 IP;并且事故周期跨度大,50 天发生一次,发现规律比较困难。


发现规律是第一步,重点是能把 50 天和 uint32 的最大值联系起来,这一步是解决该问题的灵魂。



  • 大胆假设:客户端和服务端的代码中与时间相关的函数有问题;

  • 小心求证:找到有问题的函数,别写代码验证,最后通过复现定位问题;


经过不屑努力从没有头绪到逐渐缩小排查范围,最后定位和解决问题。

作者:科英
来源:juejin.cn/post/7252159509837119546

收起阅读 »

关于Java已死,看看国外开发者怎么说的

博主在浏览 medium 社区时,发现了一篇点赞量 1.5k 的文章,名称叫《Java is Dead — 5 Misconceptions of developers that still think Java is relevant today!》直译过来...
继续阅读 »


博主在浏览 medium 社区时,发现了一篇点赞量 1.5k 的文章,名称叫《Java is Dead — 5 Misconceptions of developers that still think Java is relevant today!》直译过来就是《Java 已死 — 开发人员对 Java 在现代编程语言中的5个误解》。这篇文章可以说是标题党得典范,热度全靠标题蹭 😂。当然本文重点在于文章评论区。作者因为标题党惨着评论区大佬们怒怼,不敢回复。


原文地址:medium.com/@sidh.thoma… Thomas



推荐博主开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。


github 地址:github.com/wayn111/way…



下面是文章内容:



人们仍然认为 Java 与当今时代相关,这是一种常见的误解。事实上 Java 是一种正在消亡的编程语言。 Java 一直是世界上使用最广泛、最流行的编程语言之一,但它很快就会面临消亡的危险。如今 Java 拥有庞大而活跃的开发者社区,并且仍然用于广泛的应用程序,包括 Web 开发、移动应用程序开发和企业级软件开发,但 Java 能在未来 10 年生存吗?让我们看看开发者对 Java 有哪些误解:



误解 1:Java 拥有庞大且活跃的开发者社区。世界各地有数百万 Java 开发人员,该语言在开发人员共享知识和资源的在线论坛和社区中占有重要地位。



虽然情况仍然如此,但开发人员转向其他平台和编程语言的速度很能说明问题,我个人也看到开发人员惊慌失措地跳槽。主要问题是 Java 作为一种编程语言还没有现代化,因此它仍然很冗长,通过一个步履蹒跚但极其笨重的类型系统结合了静态和动态类型之间最糟糕的两个世界,并且要求在具有以下功能的 VM 上运行宏观启动时间(对于长时间运行的服务器来说不是问题,但对于命令行应用程序来说是痛苦的)。虽然它现在表现得相当不错,但它仍然无法与 C 或 C++ 竞争,并且只要有一点爱,C#、Go、Rust 和 Python 就可以或将会在该领域超越它。对于现实世界的生产服务器,它往往需要大量的 JVM 调整,而且很难做到正确。



误解 2:Java 的应用范围很广。 Java 不仅仅是一种 Web 开发语言,还用于开发移动应用程序、游戏和企业级软件。这种多功能性使其成为许多不同类型项目的有价值的语言。



Java 不再是移动应用程序开发(尤其是 Android)首选的编程语言。 Kotlin 现在统治着 Android,大多数 Android 开发者很久以前就已经跳槽了。就连谷歌也因为几年前与甲骨文的惨败而放弃了 Java 作为 Android 的事实上的语言。 Java 作为一种 Web 开发语言也早已失去了它的受欢迎程度。就企业开发而言,Java 在大型企业中仍然适用,因为它可靠且稳定。尽管许多初创公司并未将 Java 作为企业软件的首选,但他们正在使用其他替代方案。



误解 3:Java 是基础语言。许多较新的编程语言都是基于 Java 的原理和概念构建的,并且旨在以某种方式与其兼容。这意味着即使 Java 的受欢迎程度下降,它的原理和概念仍然具有相关性。



虽然 Java 确实是许多人开始编程之旅的基础语言,但事实是 Java 仍然非常陈旧且不灵活。最重要的是,与其他现代编程语言相比,它仍然很冗长,这意味着它需要大量代码来完成某些任务。这会使编写简洁、优雅的代码变得更加困难,并且可能需要更多的精力来维护大型代码库。此外,Java 是静态类型的这一事实意味着它可能比动态类型语言更严格且灵活性较差,这可能会让一些开发人员感到沮丧。



误解 4:Java 得到各大公司的大力支持。 Oracle 是维护和支持 Java 的公司,对该语言有着坚定的承诺,并持续投资于其开发和改进。此外,包括 Google 和 Amazon 在内的许多大公司都在其产品和服务中使用 Java。



Oracle 的 Java 市场份额正在快速被竞争对手夺走。见下图:



尽管下图显示甲骨文仍然拥有最大的市场份额,但其份额已减少了一半以上。 2020 年,甲骨文占据了“大约 75% 的 Java 市场”,而现在的份额还不到 35%。


根据 New Relic 的数据,排名第二的是亚马逊,自 2021 年 11 月发布 Java 17 以来,其份额急剧上升,当时其份额几乎与 Eclipse Adoptium 相同。



误解 5:Java 在学校和大学中广泛教授。 Java 是一种流行的编程概念教学语言,经常用于学校和大学的计算机科学课程。这意味着有源源不断的新开发人员正在学习 Java 并熟悉其功能。



这种情况正在发生很大的变化。渴望成为软件开发人员的年轻大学生正在迅速转向其他编程语言。由于对这些其他编程语言的普遍需求,这越来越多地促使学院和大学寻找替代方案。


我知道这是一个有争议的话题。虽然我也认为 Java 是一种彻底改变了软件编写方式的语言,并为其他编程语言树立了可以效仿的基准。但不幸的是,该语言的所有权掌握在公司手中,在没有留下太多财务收益的情况下,该公司没有动力继续改进它。



OK,文章内容就这么多,下面是本文重点!



评论区



喜闻乐见评论区来了 😎,看看国外开发者怎么反驳这篇文章得,本文选取评论点赞量较高得5条评论放在下文。



评论一


来自Migliorabile



作者不知道什么是编程语言、它为什么存在以及它在哪里使用。

仅因为许多程序员都在应用程序中最简单的部分工作,就认为 Java 与 Python 等效,这是完全错误的。

假设自因为使用自行车的人比驾驶采矿机的人多,我就认为自行车比卡特彼勒采矿机更好,这是不对得。



评论二


来自Khalid Hamid



哈哈哈,我想说他甚至可能不是一个程序员,可能会做一些 JavaScript 的事情,即使如此,将 JavaScript 和 TypeScript 归类为两种语言也是没有意义的。

在安卓开发中,他不明白 Kotlin 是什么,虽然它确实有效。



评论三


来自Dan Decker



每次看到这样的文章我都会直接去看评论。(喜闻乐见评论区🤔)



评论四


来自Max Dancona



对于成熟,我有一些话要说。我过去三份工作中有两份是在一些公司开始使用一种性感的新语言(即 ruby 和 python),然后付钱给像我这样的人用 Java 重写他们的应用程序。



评论五


来自Marco Kneubühler



作者似乎不明白编程语言的风格是出于不同的目的而存在的,语言之间进行比较没有意义, 比如拿 sql 或 html/css 与 java 来比?语言是一个丰富的生态系统,我们需要为特定目的选择正确的语言。因此需要多语言开发人员而不是教条主义。



总结


博主这里说下自己得看法,虽然作者对于自己得观点进行了5个误解的阐述,但是博主是并不认同得。



  • 文章的标题就是一个误导性的问题,暗示了 Java 已经不行。事实上 Java 仍然是一门非常流行和强大的编程语言,它在很多领域都有广泛的应用和优势,如移动应用、Web 应用、可穿戴设备、大数据、云计算等。Java 也有不断地更新和改进,引入了很多新的特性和功能,以适应不断变化的技术需求。

  • Java 也有庞大的社区和丰富的资源,为开发者提供了很多支持和帮助。根据 GitHub Octoverse Report 2022,Java 是第三大最受欢迎的语言,仅次于 JavaScript、Python。根据 JetBrains State of Developer Ecosystem 2022,Java 是过去12个月内使用占有率排名第五的语言,占据了 48% 的份额。根据 StackOverflow Developer Survey 2022,最常用的编程语言排行榜中 Java 是排名第六的语言,占据了 33.27% 的份额。这些数据都表明 Java 并没有死亡或不在流行,而是仍然保持着其重要的地位。


GitHub Octoverse Report 2022


JetBrains State of Developer Ecosystem 2022


StackOverflow Developer Survey 2022



  • 文中说 Java 是一门过时和冗长的语言,它没有跟上时代的变化,而其他语言如 Python、JavaScript 和 Kotlin 等都更加简洁和现代化。这个观点忽略了 Java 的设计哲学和目标。Java 是一门成熟、稳定、跨平台、高性能、易维护、易扩展的编程语言,它注重可读性、健壮性和兼容性。Java 的语法可能相对复杂,但它也提供了很多强大的特性和功能,如泛型、注解、枚举、lambda 表达式、流 API、模块化系统等。

  • Java 也没有停止创新和改进,它在近几年引入了很多新的特性和功能,如 Record 类、密封类、模式匹配、文本块、虚拟线程、外部函数和内存API等。其他语言可能在某些方面比 Java 更加简洁或现代化,但它们也有自己的局限和缺点,比如运行速度慢、类型系统弱、错误处理困难等。不同的语言适合不同的场景和需求,并不是说一种语言就可以完全取代另一种语言。


总之,我觉得 Java 在未来会被替代的可能性很小,但也不能掉以轻心,在后端开发领域,Go 已经在逐步蚕食 Java 得份额,今年非常火得 ai 模型领域相关,大部分代码也是基于 Python 编写。Java 需要在保持优势领域地位后持续地创新和改进。



关注公众号【waynblog】每周分享技术干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!


作者:waynaqua
来源:juejin.cn/post/7252127579195736119

收起阅读 »

十年码农内功:分布式

分布式协议一口气学完 一、Raft 协议 1.1 基础概念 Raft 算法是通过一切以领导者为准的方式,实现一系列数据的共识和各节点日志的一致。Raft是强领导模型,集群中只能有一个领导者。 成员身份:领导者(Leader)、跟随者(Follower)和候选...
继续阅读 »

分布式协议一口气学完



一、Raft 协议


1.1 基础概念


Raft 算法是通过一切以领导者为准的方式,实现一系列数据的共识和各节点日志的一致。Raft是强领导模型,集群中只能有一个领导者。


成员身份:领导者(Leader)、跟随者(Follower)和候选者(Candidate)。



图1 成员身份



  • 领导者:集群中霸道总裁,一切以我为准,处理写请求、管理日志复制和不断地发送心跳信息。

  • 跟随者:普通成员,处理领导者发来的消息,发现领导者心跳超时,推荐自己成为候选人。

  • 候选人:先给自己投一票,然后请求其他集群节点投票给自己,得票多者成为新的领导者。

  • 任期编号:每任领导者都有任期编号。当领导者心跳超时,跟随者会变成候选人,任期编号 +1,然后发起投票。任期编号小的服从编号大的。

  • 心跳超时:每个跟随者节点都设置了随机心跳超时时间,目的是避免跟随者们同时成为候选人,同时发起投票。

  • 选举超时:每轮选举的结束时间,随机选举超时时间可以避免多个候选人同时结束选举未果,然后同时发起下一轮选举


1.2 领导选举


1.2.1 选举规则



  • 领导者周期性地向跟随者发送心跳消息,告诉跟随者我是领导者,阻止跟随者发变成候选人发起新选举;

  • 如果跟随者的随机心跳超时了,那么认为没有领导者了,推荐自己为候选人,发起新的选举;

  • 在一轮选举中,赢得一半以上选票的候选人,即成为新的领导者;

  • 在一轮选举中,每个节点对每个任期编号的选举只能投出一票,先来先服务原则;

  • 日志完整性高(也就是最后一条日志项对应的任期编号值更大,索引号更大)的跟随者A拒绝给完整性低的候选人B投票,即使B的任期编号大;


1.2.2 选举动画



图2 初始选举



图3 领导者宕机/断网



图4 第一轮选举未果,发起第二轮选举


1.3 日志复制


日志项(Log Entry):是一种数据格式,它主要包含索引值(Log index)、任期编号(Term)和 指令(Command)。



  • 索引值:它是用来标识日志项的,是一个连续的、单调递增的整数值。

  • 任期编号:创建这条日志项的领导者的任期编号。

  • 指令:一条由客户端请求指定的、服务需要执行的指令。



图5 日志信息


1.3.1 日志复制动画



图6 简单日志复制



图7 复杂日志复制


1.3.2 日志恢复


每次选举出来的Leader一定包含在多数节点上最新的已经提交的日志,新的Leader将会覆盖其他节点上不一致的数据。


虽然新Leader一定包括上一个Term的Leader已提交(Committed)日志,但是可能也包含上一个Term的Leader的未提交(Uncommitted)日志。


这部分未提交日志需要转变为Committed,相对比较麻烦,需要考虑Leader多次切换且未完成日志恢复,需要保证最终提案是一致的、确定的,不然就会产生所谓的幽灵复现问题。


为了将上一个Term未提交的日志转为已提交,Raft算法要求Leader当选后立即追加一条Noop的特殊内部日志,并立即同步到其它节点,实现前面未提交日志全部隐式提交。


这样保证客户端不会读到未提交数据,因为只有Noop被大多数节点同意并提交了之后(这样可以连带往期日志一起同步),服务才会对外正常工作;


Noop日志本身是一个分界线,Noop之前的日志被提交,之后的日志将会被丢弃。Noop日志仅包含任期编号和日志索引值,没有指令。


日志“幽灵复现”的场景



图8


第一步,A是领导者,在本地记录4和5日志,并没有提交,然后挂了。



图9


第二步,由于B的日志索引值比C的大,B成为了领导者,仅把日志3同步给了C,然后挂了。



图10


第三步,A恢复了,并且成为了领导者,然后把未提交的日志4和5同步给了B和C(C在A成为了领导者之后、同步日志之前恢复了),然后ABC都提交了日志4和5,就这样原本客户端写失败的日志4和5复活了,进而客户端会读到其认为未提交的日志(实际上集群日志已提交)。


Noop解决日志复现


第一步,同上面一样。


第二步,由于B的日志索引值比C的大,B成为了领导者,这次不仅把日志3同步给了C,还记录了一个Noop日志,并且同步给了C。



图11


第三步,当A恢复了,想成为领导者,发现自己的日志任期编号和日志索引值都不是最大的,即使B挂了也还有C,A也就成为不了领导者,乖乖使用B的日志覆盖自己的日志。


1.4 成员变更


集群成员变更最大的风险是可能同时出现 2 个领导者。比如在成员变更时,节点 A、B 和 C 之间发生了分区错误,节点 A、B 组成旧集群(ABC)中的“大多数”。


而节点 C 和新节点 D、E 组成了新集群(ABCDE)的“大多数”,它们可能会选举出新的领导者(比如节点 C)。结果出现了同时存在 2 个领导者的情况。违反了Raft协议中领导者唯一性原则。



图12 集群(ABC)同时增加节点D和E


最初解决办法是联合共识(Joint Consensus),但实现起来难,后来 Raft 的作者就提出了一种改进后的方法,单节点变更(single-server changes)。


在正常情况下,旧集群的“大多数”和新集群的“大多数”都会有一个重叠的节点。



图13 集群(ABCD)增加新节点E



图14 集群(ABCDE)删除节点A



图15 集群(ABC)增加新节点D



图16 集群(ABCD)删除节点A


需要注意的是,在分区错误、节点故障等情况下,如果并发执行单节点变更,那么就可能出现一次单节点变更尚未完成,新的单节点变更又在执行,导致集群出现 2 个领导者的情况。


二、Gossip 协议


Gossip协议,顾名思义,就像流言蜚语一样,利用一种随机、带有传染性的方式,将信息传播到整个网络中,并在一定时间内,使得系统内的所有节点数据一致。Gossip其实是一种去中心化思路的分布式协议,解决信息在集群中的传播和最终一致性。


2.1 原理


Gossip协议的消息传播主要有两种:反熵(Anti-Entropy)和谣言传播(Rumor-Mongering)。


2.1.1 反熵:节点相对固定,节点数量不多,以固定概率传播所有的数据


每个节点周期性地随机选择其他节点,通过互相交换各自的所有数据来消除两者之间的差异,实现数据的最终一致性。反熵非常可靠,但每次节点两两交换各自的所有数据会带来非常大的通信负担,因此不会频繁使用。通过引入校验和等机制,可以降低需要对比的数据量和传播消息量。


反熵 使用“simple epidemics”方式,其包含两种状态:susceptible和infective,这种模型也称为SI model。处于infective状态的节点代表其有数据更新,并且会将这个数据分享给其他节点;处于susceptible状态的节点代表其并没有收到来自其他节点的更新。



图17 反熵


2.2.2 谣言传播:节点动态变化,节点数量较多,仅传播新到达的数据


当一个节点有了新信息后,这个节点变成活跃状态,并周期性地向其他节点传播新信息。直到所有的节点都知道该新信息。由于节点之间只传播新信息,所以大大减少了通信负担。


谣言传播 使用“complex epidemics”方法,比反熵 多了一种状态:removed,这种模型也称为SIR model。处于removed状态的节点说明其已经接收到来自其他节点的更新,但是其并不会将这个更新分享给其他节点。因为谣言消息会在某个时间标记为removed,然后不会再被传播给其他节点,所以谣言传播有极小概率使得所有节点数据不一致。



图18 谣言传播


一般来说,为了在通信代价和可靠性之间取得折中,需要将这两种方法结合使用。


2.2 通信方式


节点间的交互主要有三种方式:推、拉和推/拉



图19 节点状态


2.2.1 推送模式(push)


节点A随机选择联系节点B,并向其发送自己的信息,节点B在收到信息后比较/更新自己的数据。



图20 推方式


2.2.2 拉取模式(pull)


节点A随机选择联系节点B,从对方获取信息,节点A在收到信息后比较/更新自己的数据。



图21 拉方式


2.2.3 推/拉模式(push/pull)


节点A向选择的节点B发送信息,同时从对方获取信息,节点A和节点B在收到信息后各自比较/更新自己的数据。



图22 推/拉方式


2.3 优缺点



  • 优点

    • 可扩展性(Scalable): Gossip协议是可扩展的,一般需要 O(logN) 轮就可以将信息传播到所有的节点,其中N代表节点的个数。每个节点仅发送固定数量的消息,并且与网络中节点数目无关。在数据传送时,节点并不会等待消息的Ack,所以消息传送失败也没有关系,因为可以通过其他节点将消息传递给之前传送失败的节点。允许节点的任意增加和减少,新增节点的数据最终会与其他节点一致。

    • 容错(Fault-tolerance): 网络中任何节点的重启或者宕机都不会影响 Gossip 消息的传播,具有天然的分布式系统容错特性。

    • 健壮性(Robust): Gossip协议是去中心化的协议,集群中的所有节点都是对等的,没有特殊的节点,所以任何节点出现问题都不会阻止其他节点继续发送消息。任何节点都可以随时加入或离开,而不会影响系统的整体服务质量。

    • 最终一致性(Convergent consistency): Gossip协议实现信息指数级的快速传播,因此在有新信息需要传播时,消息可以快速地发送到全局节点,在有限的时间内能够做到所有节点都拥有最新的数据。



  • 缺点

    • 消息延迟:节点随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网,不可避免的造成消息延迟。

    • 消息冗余:节点定期随机选择周围节点发送消息,而收到消息的节点也会重复该步骤,不可避免地引起同一节点消息多次接收,增加消息处理压力。




三、参考


作者:科英
来源:juejin.cn/post/7251501954156855352

收起阅读 »

全局唯一ID生成

在Java中,可以使用多种方法生成唯一的ID。下面我将介绍几种常用的方法: UUID(Universally Unique Identifier):UUID是一种128位的唯一标识符。它可以通过java.util.UUID类来生成,使用UUID.rando...
继续阅读 »

在Java中,可以使用多种方法生成唯一的ID。下面我将介绍几种常用的方法:




  1. UUID(Universally Unique Identifier):UUID是一种128位的唯一标识符。它可以通过java.util.UUID类来生成,使用UUID.randomUUID()方法返回一个新的UUID。UUID的生成是基于时间戳和计算机MAC地址等信息,因此几乎可以保证全局唯一性。


    import java.util.UUID;

    public class UniqueIdExample {
    public static void main(String[] args) {
    UUID uuid = UUID.randomUUID();
    String id = uuid.toString();
    System.out.println(id);
    }
    }



  2. 时间戳:可以使用当前时间戳作为唯一ID。使用System.currentTimeMillis()方法可以获取当前时间的毫秒数作为ID值。需要注意的是,时间戳只是在同一台机器上保持唯一性,在分布式系统中可能存在重复的风险。


    public class UniqueIdExample {
    public static void main(String[] args) {
    long timestamp = System.currentTimeMillis();
    String id = String.valueOf(timestamp);
    System.out.println(id);
    }
    }



  3. Snowflake算法:Snowflake是Twitter开源的一种分布式ID生成算法,可以生成带有时间戳、机器ID和序列号的唯一ID。可以使用第三方库(如Twitter的Snowflake)来生成Snowflake ID。Snowflake ID的生成是基于时间序列、数据中心ID和机器ID等参数的。


    import com.twitter.snowflake.SnowflakeIdGenerator;

    public class UniqueIdExample {
    public static void main(String[] args) {
    SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator();
    long id = idGenerator.nextId();
    System.out.println(id);
    }
    }



以上是一些常用的生成唯一ID的方法,每种方法都有自己的特点和适用场景。选择合适的方法要根据具体需求、性能要求

作者:Lemonade22
来源:juejin.cn/post/7250037058684583995
以及系统架构来决定。

收起阅读 »

Web的攻击技术: 别让我看到你网站的缺陷,不然你看我打不打你🍗🍗🍗

简单的 HTTP 协议本身并不存在安全性问题,因此协议本身几乎不会成为攻击的对象。应用 HTTP 协议的服务器和客户端,已经运行在服务器上的 Web 应用等资源才是攻击目标。 在客户端即可篡改请求 在 Web 应用中,从浏览器那接收到的 HTTP 请求的全部内...
继续阅读 »

简单的 HTTP 协议本身并不存在安全性问题,因此协议本身几乎不会成为攻击的对象。应用 HTTP 协议的服务器和客户端,已经运行在服务器上的 Web 应用等资源才是攻击目标。


在客户端即可篡改请求


Web 应用中,从浏览器那接收到的 HTTP 请求的全部内容,都可以在客户端自由地变更、篡改。所以 Web 应用可能会接收到与预期数据不相同的内容。


HTTP 请求报文内加载攻击代码,就能立发起对 Web 应用的攻击,通过 URL 查询字段或表单、HTTP 首部、Cookie 等途径吧攻击代码传入,若这时 Web 应用存在安全漏洞,那内部信息就会遭到窃取,或被攻击者拿到管理权限。
20230701072440


针对 Web 应用的攻击模式


Web 应用的攻击模式有以下两种:



  • 主动攻击;

  • 被动攻击;


以服务器为目标的主动攻击


主动攻击是指攻击者通过直接访问 Web 应用,把攻击代码传入的模式,由于该模式是直接针对服务器上的资源进行攻击,因此攻击者能够访问到那些资源。


主动攻击模式里面具有代表性的攻击是 SQL 注入攻击和 OS 命令注入攻击。


20230701072818


以服务器为目标的被动攻击


被动攻击是指利用圈套策略执行攻击代码的攻击模式,在被动攻击过程中,攻击者不直接对目标 Web 应用访问发起攻击。


被动攻击通常的攻击模式如下所示:



  1. 攻击者诱使用户触发已设置好的陷阱,而陷阱会启动发送已嵌入攻击代码的 HTTP 请求;

  2. 当用户不知不觉中招之后,用户的浏览器或邮件客户端会触发这个陷阱;

  3. 中招后的用户浏览器会把含有攻击代码的 HTTP 请求发送给作为攻击目标的 Web 应用,运行攻击代码;

  4. 执行完攻击代码,存在安全漏洞的 Web 应用会成为攻击者的跳板,可能导致用户所持的 Cookie 等个人信息被窃取,登录状态中的用户权限遭恶意滥用等后果。


被动攻击模式中具有代表性的攻击是跨站脚本攻击和跨站点请求伪造。
20230701074804


利用被动攻击,可发起对原本从互联网上无法直接访问的企业内网等网络的攻击。只要用户踏入攻击者预先设好的陷阱,在用户能够访问到的网络范围内,即使是企业内网也同样会受到攻击。


20230701075024


因输出值转移不完全引发的安全漏洞


实施 Web 应用的安全对策可大致分为以下两部分:



  • 客户端的验证;

  • Web 应用端的验证:

    • 输入值验证;

    • 输出值转义;




20230701075829


因为 JavaScript 代码可以在客户端随便修改或者删除,所以不适合将 JavaScript 验证作为安全的方法策略。保留客户端验证只是为了尽早地辨识输入错误,起到提高 UI 体验的作用。


输入值验证通常是指检查是否是符合系统业务逻辑的数值或检查字符编码等预防对策。


从数据库或文件系统、HTML、邮件等输出 Web 应用处理的数据之际,针对输出做值转义处理是一项至关重要的安全策略。当输出值转义不完全时,会因触发攻击者传入的攻击代码,而给输出对象带来损害。


跨站脚本攻击


跨站脚本攻击(Cross-Site Scripting,XSS)是指通过存在安全漏洞的 Web 网站注册用户的浏览器内运行非法的 HTML 标签或 JavaScript 进行的一种攻击。动态创建的 HTML 部分有可能隐藏这安全漏洞。就这样,攻击者编写脚本设下陷阱,用户在自己的浏览器上运行时,一不小心就会受到被动攻击。


跨站脚本攻击有可能造成一下影响:



  • 利用虚假输入表单骗取用户个人信息;

  • 利用脚本窃取用户的 Cookie 值,被害者在不知情的情况下,帮助攻击者发送恶意请求;

  • 显示伪造的文章或图片;


跨站脚本攻击案例


在动态生成 HTML 处发生


下面以编辑个人信息页面为例讲解跨站脚本攻击,下放界面显示了用户输入的个人信息内容:
20230701090312


确认姐妹按原样显示在编辑解密输入的字符串。此处输入带有山口伊朗这样的 HTML 标签的字符串。


那如果我把输入的内容换成一段 JavaScript 代码呢,阁下又该如何应对?


<script>
alert("落霞与孤鹜齐飞,秋水共长天一色!");
</script>

XSS 是攻击者利用预先设置的陷阱触发的被动攻击


跨站脚本攻击属于被动攻击模式,因此攻击者会事先布置好用于攻击的陷阱。


下图网站通过地址栏中 URI 的查询字段指定 ID,即相当于在表单内自动填写字符串的功能。而就在这个地方,隐藏着可执行跨站脚本攻击的漏洞。
20230701091251


充分熟知此处漏洞特点的攻击者,于是就创建了下面这段嵌入恶意代码的 URL。并隐藏植入事先准备好的欺诈邮件中或 Web 页面内,诱使用户去点击该 URL


浏览器打开该 URI 后,直观感觉没有发生任何变化,但设置好的脚本却偷偷开始运行了。当用户在表单内输入 ID 和密码之后,就会直接发送到攻击者的网站,导致个人登录信息被窃取。


之后,ID 及密码会传给该正规网站,而接下来仍然是按正常登录步骤,用户很难意识到自己的登录信息已遭泄露。


除了在表单中设下圈套之外,下面那种恶意构造的脚本统一能够通过跨站脚本攻击的方式,窃取到用户的
Cookie 信息:


const cookie = document.cookie;

执行上面这段 JavaScript 程序,即可访问到该 Web 应用所处域名下的 Cookie 信息,然后这些信息会发送至攻击者的 Web 网站,记录在它的登录日志中,攻击者就这样窃取到用户的 cookie 信息了。


React 中通过 JSX 语法转义来防止 XSS。在 JSX 语法中,可以通过花括号 {} 插入 JavaScript 表达式。JSX 语法会自动转义被插入到 HTML 标签之间的内容。这意味着任何用户输入的内容都会被转义,以防止恶意脚本的执行。在转义过程中,React 会将特殊字符进行转换,例如将小于号 < 转义为 <、大于号 > 转义为 >、引号 " 转义为 " 等。这样可以确保在渲染时,用户输入的内容被当作纯文本处理,而不会被解析为 HTML 标签或 JavaScript 代码。


SQL 注入攻击


会执行非法 SQL 的 SQL 注入攻击


SQL 注入是指针对 Web 应用使用的数据库,通过运行非法的 SQL 而产生的攻击。该安全隐患有可能引发极大的威胁,有时会直接导致个人信息及机密信息的泄露。


SQL 注入攻击有可能会造成以下等影响:



  • 非法查看或篡改数据库内的数据;

  • 规避认证;

  • 执行和数据库服务器业务关联的程序等;


SQL 注入攻击案例



  • 登录绕过攻击: 攻击者可以在登录表单的用户名和密码字段中插入恶意的 SQL 代码。如果应用程序未对输入进行正确的验证和过滤,攻击者可以通过在用户名字段中输入 ' OR '1'='1 的恶意输入来绕过登录验证,使得 SQL 语句变为:SELECT \* FROM users WHERE username = '' OR '1'='1' AND password = '<输入的密码>',从而成功登录到系统中;

  • 删除或修改数据攻击: 攻击者可以通过注入恶意的 SQL 代码来删除或修改数据库中的数据。例如,攻击者可以在一个表单的输入字段中插入 '; DROP TABLE users;-- 的恶意输入。如果应用程序未正确处理这个输入,攻击者可以成功删除用户表(假设表名为 "users")导致数据丢失;


OS 命令注入攻击


OS 命令注入攻击是指通过 Web 应用,执行非法的操作系统命令达到攻击的目的。只要在能调用 Shell 函数的地方就有存在被攻击的风险。


可以从 Web 应用中通过 Shell 来调用操作系统命令,如果调用 Shell 是存在疏漏,就可以执行插入的非法 OS 命令。


OS 命令注入攻击可以向 Shell 发送命令,让 WindowsLinux 操作系统的命令行启动程序。也就是说,通过 OS 注入攻击可执行 OS 上安装着的各种程序。


OS 注入攻击案例



  • 文件操作攻击: 攻击者可以在应用程序的输入字段中插入恶意的操作系统命令来执行文件操作。例如,如果应用程序在文件上传过程中未对输入进行适当的验证和过滤,攻击者可以在文件名字段中插入 '; rm -rf / ;-- 的恶意输入。如果应用程序在执行文件操作时没有正确处理这个输入,攻击者可能会删除服务器上的所有文件;

  • 远程命令执行攻击: 攻击者可以通过注入恶意的操作系统命令来执行远程系统命令。例如,如果应用程序在一个输入字段中插入 ; ping <恶意 IP 地址> ; 的恶意输入,而没有进行正确的输入验证和过滤,攻击者可以利用该注入漏洞执行远程命令,对目标系统进行攻击或探测;


HTTP 首部注入攻击


HTTP 首部注入攻击是指攻击者通过在响应首部字段内插入换行,添加任意响应首部或主体的一种攻击。属于被动攻击模式。


HTTP 首部注入攻击有可能造成以下一些影响:



  • 设置任何 Cookie 信息;

  • 重定向值任意 URL;

  • 显示任意的主体;


HTTP 首部注入攻击案例


以下是一些 HTTP 首部注入攻击的案例,展示了攻击者是如何利用该漏洞进行攻击的:



  • 重定向攻击: 攻击者可以在 HTTP 响应的 Location 首部中插入恶意 URL,从而将用户重定向到恶意网站或欺骗性的页面。如果应用程序未正确验证和过滤用户输入,并将其直接用作 Location 首部的值,攻击者可以在 URL 中插入换行符和其他特殊字符,添加额外的首部字段,导致用户被重定向到意外的位置;

  • 缓存投毒攻击: 攻击者可以在 HTTP 响应的 Cache-Control 或其他缓存相关首部中插入恶意指令,以欺骗缓存服务器或浏览器,导致缓存数据的污染或泄漏。攻击者可以通过注入换行符等特殊字符来添加额外的首部字段或修改缓存指令,绕过缓存机制或引发信息泄露;

  • HTTP 劫持攻击: 攻击者可以在 HTTP 响应的 LocationRefresh 首部中插入恶意 URL,将用户重定向到恶意网站或欺骗性页面,从而劫持用户的会话或执行其他攻击。通过在响应中插入恶意的 LocationRefresh 值,攻击者可以修改用户的浏览器行为;

  • XSS 攻击: 攻击者可以在 HTTP 响应的 Set-Cookie 或其他首部字段中插入恶意脚本,以执行跨站脚本攻击。如果应用程序未正确过滤和转义用户输入,并将其插入到首部字段中,攻击者可以通过注入恶意代码来窃取用户的会话标识符或执行其他恶意操作;


因会话管理疏忽引发的安全漏洞


会话管理是用来管理用户状态的必备功能,但是如果在会话管理上有所疏忽,就会导致用户的认证状态被窃取等后果。


会话劫持


会话劫持是指攻击者通过某种手段拿到了用户的会话 ID,并非法使用此会话 ID 伪装成用户,达到攻击的目的:
20230701104716


具备人中功能的 Web 应用,使用会话 ID 的会话管理机制,作为管理认证状态的主流方式。会话 ID 中记录客户端的 Cookie 等信息,服务端将会话 ID 与认证状态进行一对一匹配管理。


下面列举了几种攻击者可获得会话 ID 的途径:



  • 通过非正规的生成方法推测会话 ID;

  • 通过窃听或 XSS 攻击盗取会话 ID;

  • 通过会话固定攻击强行获取会话 ID;


会话劫持攻击案例


下面我们以认证功能为例讲解会话劫持。这里的认证功能通过会话管理机制,会将成功认证的用户的会话 ID,保存在用户浏览器的 Cookie 中。
20230701105419


攻击者在得知该 Web 网站存在可跨站攻击的安全漏洞后,就设置好用 JavaScript 调用 document.cookie 以窃取 cookie 信息的陷阱,一旦用户踏入陷阱访问了该脚本,攻击者就能获取含有会话的 IDCookie


攻击者拿到用户的会话 ID 后,往自己的浏览器的 Cookie 中设置该会话 ID,即可伪装成会话 ID 遭窃的用户,访问 Web 网站了。


会话固定攻击


对一切去目标会话 ID 为主动攻击手段的会话劫持而言,会话固定攻击 攻击会强制用户使用指定的会话 ID,属于被动攻击。


会话固定攻击案例


下面我们以认证功能为例讲解会话固定攻击,这个 Web 网站的认证功能,会在认证前发布一个会话 ID,若认证成功,就会在服务器内改变认证状态。


20230701152056


攻击者准备陷阱,先访问 Web 网站拿到会话 ID,此刻,会话 ID 在服务器上的记录仪仍是未认证状态。


攻击者设计好强制用户使用该会话 ID 的陷阱,并等待用户拿着这个会话 ID 前去认证,一旦用户触发陷阱并完成认证,会话 ID 服务器上的状态(用户 A 已认证) 就会被记录下来。


攻击者估计用户差不多已触发陷阱后,再利用之前这个会话 ID 访问网站,由于该会话 ID 目前已是用户 A 已认证状态,于是攻击者作为用户 A 的身份顺利登陆网站。


会话固定攻击预防措施


会话固定攻击利用了应用程序在身份验证和会话管理过程中未正确处理会话标识符的漏洞。为了防止会话固定攻击,开发人员可以采取以下措施:



  1. 生成随机、唯一的会话标识符,并在用户每次登录或创建新会话时重新生成;

  2. 不接受用户提供的会话标识符,而是通过服务器生成并返回给客户端;

  3. 在身份验证之前和之后,对会话标识符进行适当的验证和验证机制;

  4. 设置会话管理策略,包括会话超时时间和注销会话的方式;


通过采取这些预防措施,可以减少会话固定攻击的风险,并提高应用程序的安全性。


跨站点请求伪造


跨站点伪造请求(Cross-Site Require Forgeries,CSRF)攻击是指攻击者通过设置好的陷阱,强制用户对已完成认证的用户进行非预期的个人信息或设定信息等某些状态更新,属于被动攻击。


跨站点请求伪造有可能会造成一下等影响:



  • 利用已通过认证的用户权限更新设定信息等;

  • 利用已通过认证的用户权限购买商品;

  • 利用已通过认证的用户权限在评论区发表言论;


跨站点伪造请求的攻击案例


下面以一个网站的登录访问功能为例,讲解跨站点请求伪造,如下图所示:
20230701160321



  1. 当用户输入账号信息请求登录 A 网站;

  2. A 网站验证用户信息,通过验证后返回给用户一个 cookie;

  3. 在未退出网站 A 之前,在同一浏览器中请求了黑客构造的恶意网站 B;

  4. B 网站收到用户请求后返回攻击性代码,构造访问 A 网站的语句;

  5. 浏览器收到攻击性代码后,在用户不知情的情况下携带 cookie 信息请求了 A 网站。此时 A 网站不知道这是由 B 发起的。那么这时黑客就可以为所欲为了!!!


这首先必须瞒住两个条件:



  • 用户访问站点 A 并产生了 Cookie;

  • 用户没有退出 A 网站同时访问了 B 网站;


CSRF 攻击的防御


当涉及到跨站伪造请求的防御时,一下是一些防御方法和实践:




  1. 验证来源和引用检查:



    • 服务器端应该验证每个请求的来源 Referer 字段和源 Origin 字段以确保请求来自预期的域名或网站。如果来源不匹配,服务器应该拒绝请求。Referer 头部并不是 100% 可靠,因为某些浏览器或网络代理可能会禁用或篡改该字段。因此,Origin 字段被认为是更可靠的验证来源的方式;




  2. CSRF Token:



    CSRF 令牌是一个随机生成的值,嵌入到表单或请求参数中,与用户会话相关联。它的目的是验证请求的合法性,确保请求是由预期的用户发起的,而不是由攻击者伪造的。




    • 在每个表单和敏感操作的请求中,包括一个 CSRF 令牌;

    • 令牌可以作为隐藏字段 input type="hidden" 或请求头,例如 X-CSRF-Token 的一部分发送;

    • 在服务器端,验证请求中的令牌是否与用户会话中的令牌匹配,以确保请求的合法性;




  3. 验证请求的方法: 某些敏感操作应该使用 POSTPUTDELETE 等非幂等方法,而不是 GET 请求。这样可以防止攻击者通过构造图片或链接等 GET 请求来触发敏感操作;




  4. 敏感操作的二次确认: 对于一些敏感操作,例如修改密码、删除账户等,可以在用户执行操作前要求二次确认,以确保用户的意图和授权;




综合采取上述防御措施,可以有效减少跨站伪造请求攻击的风险。然而,没有单一的解决方案可以完全消除跨站伪造请求的威胁,因此建议在开发过程中将安全性作为一个关键考虑因素,并进行全面的安全测试和审查。


参考文献


书籍: 图解HTTP


总结


没有总结,总结个屁,不上网就是

作者:Moment
来源:juejin.cn/post/7251158799318057015
最安全的......

收起阅读 »

API接口对于企业数字化转型有哪些重要意义?

当前,有很多企业都在加速进行数字化转型,随着AI等新技术的不断发展,企业的交付和数据管理能力已经无法满足需求。以数据为例,不少企业内部存在数据难题无法解决,一方面数据孤岛使得数据分散且多个业务系统之间难以打通,业务衔接不够顺畅,导致用户使用系统会更加复杂,数字...
继续阅读 »

当前,有很多企业都在加速进行数字化转型,随着AI等新技术的不断发展,企业的交付和数据管理能力已经无法满足需求。以数据为例,不少企业内部存在数据难题无法解决,一方面数据孤岛使得数据分散且多个业务系统之间难以打通,业务衔接不够顺畅,导致用户使用系统会更加复杂,数字化作用失效;另一方面数据共享开放的需求明显,但是数据安全无法保障,部分企业客户倾向于线下提供数据进行共享,会导致数据无法实时更新而且无法控制数据的流向。这两大问题既无法提高企业的业务效能,也影响到数据的安全。因此企业迫切需要进行数字化转型,才能跟上数字化浪潮的发展。

这其中最关键的方式就是使用API接口服务,那么API是如何赋能企业数字化转型的呢?


首先,不少企业内部都储存了海量的高价值数据,API能够帮助打破数据孤岛怪圈,让数据得到有效利用,开发人员能自由访问、组合数字资产,最终实现整体的协同效果,企业数据管理环境的复杂性得到了解决。
其次,API可以构造多个业务相关的接口服务与交付,企业的交付周期大大缩短,同时因为减少了代码量,开发效率得到有效提升,企业内部快速实现了降本增效。
最后,API开放平台能够实现IT资产和运维可视化以及IT资产的安全管控, 促进生态系统的形成,同时开发人员能够更方便地进行实验,创新并响应不断变化的客户需求。

数聚变平台打造了一个深耕新能源领域的API生态平台,目前已经覆盖了数据采集转发、数据集成共享、数据要素开放流通、企业数字化咨询和API全生命周期管理等多个功能模块,有效助力企业实现数字化转型。

收起阅读 »

你的密码安全吗?这三种破解方法让你大开眼界!

密码破解,是黑客们最喜欢的玩具之一。当你用“123456”这类简单密码来保护你的账户时,就像裸奔一样,等待着黑客的攻击。所以,今天我们就来聊聊密码破解知识,看看那些常见的密码破解方法,以及如何防范它们。 1、暴力破解 首先,我们来介绍一下最简单、最暴力的密码破...
继续阅读 »

密码破解,是黑客们最喜欢的玩具之一。当你用“123456”这类简单密码来保护你的账户时,就像裸奔一样,等待着黑客的攻击。所以,今天我们就来聊聊密码破解知识,看看那些常见的密码破解方法,以及如何防范它们。


1、暴力破解


首先,我们来介绍一下最简单、最暴力的密码破解方法——暴力破解。


什么是暴力破解密码呢?


简单来说,就是 攻击者通过穷举所有可能的密码组合来尝试猜测用户的密码。如果你的密码太简单或者密码空间较小,那么暴力破解密码的成功几率会增加。


暴力破解密码的一般步骤:


step1:确定密码空间


密码空间是指所有可能的密码组合。密码空间的大小取决于密码的长度和使用的字符集。例如,对于一个只包含数字的4位密码,密码空间就是从0000到9999的所有组合。


step2:逐个尝试密码


攻击者使用自动化程序对密码空间中的每个密码进行逐个尝试。这可以通过编写脚本或使用专门的密码破解工具来实现。攻击者从密码空间中选择一个密码并将其用作尝试的密码。


step3:比对结果


对于每个尝试的密码,攻击者将其输入到目标账户进行验证。如果密码正确,那么攻击者成功破解了密码。否则,攻击者将继续尝试下一个密码,直到找到匹配的密码为止。


那么我们该如何防范暴力破解呢?


方案1:增强密码策略


增强密码策略,即选择强密码。强密码应该包括足够的长度、复杂的字符组合和随机性,以增加密码空间的大小,从而增加破解的难度。比如说,“K3v!n@1234”这样的密码就比“123456”要强得多。


方案2:登录尝试限制


限制登录尝试次数,例如设置最大尝试次数和锁定账户的时间。


方案3:双因素身份验证


我们还可以引入双因素身份验证,要求用户提供额外的验证信息,如验证码、指纹或硬件令牌等。


通过综合使用这些安全措施,我们可以大大减少暴力破解密码的成功几率,并提高账户和系统的安全性。


2、彩虹表攻击


接下来,我们来介绍一种更加高级、更加可怕的密码破解方法——彩虹表攻击。


彩虹表攻击是一种 基于预先计算出的哈希值与明文密码对应关系的攻击方式


攻击者通过预先计算出所有可能的哈希值与对应的明文密码,并将其存储在一个巨大的“彩虹表”中。


当需要破解某个哈希值时,攻击者只需要在彩虹表中查找对应的明文密码即可。


攻击者生成彩虹表时需要耗费大量的计算和存储资源,但一旦生成完成,后续的密码破解速度就会非常快。


彩虹表攻击的基本原理如下:


step1:生成彩虹表


攻击者事先生成一张巨大的彩虹表,其中包含了输入密码的哈希值和对应的原始密码。彩虹表由一系列链条组成,每个链条包含一个起始密码和相应的哈希值。生成彩虹表的过程是耗时的,但一旦生成完成,后续的破解过程会变得非常快速。


step2:寻找匹配


当攻击者获取到被保护的密码哈希值时,他们会在彩虹表中搜索匹配的哈希值。如果找到匹配,就意味着找到了原始密码。


step3:链表查找


如果在彩虹表中没有找到直接匹配的哈希值,攻击者将使用哈希值在彩虹表中进行一系列的链表查找。他们会在链表上依次应用一系列的哈希函数和反向函数,直到找到匹配的密码。


那如何防范呢?


方案1:盐值(Salt)


使用随机盐值对密码进行加密。盐值是一个随机的字符串,附加到密码上,使得每次生成的哈希值都不同。这样即使相同的密码使用不同的盐值生成哈希,也会得到不同的结果,使得彩虹表无效。


方案2:迭代哈希函数


多次迭代哈希函数是指对原始密码进行多次连续的哈希运算的过程。


通常情况下,单次哈希函数的计算速度是相当快的,但它可能容易受到彩虹表等预先计算的攻击。为了增加密码的破解难度,我们可以通过多次迭代哈希函数来加强密码的安全性。


在多次迭代哈希函数中,原始密码会被重复输入到哈希函数中进行计算。每次哈希运算的结果会作为下一次的输入,形成一个连续的链式计算过程。例如,假设初始密码为 "password",哈希函数为 SHA-256,我们可以进行如下的多次迭代哈希计算:



  1. 首先,将初始密码 "password" 输入 SHA-256 哈希函数中,得到哈希值 H1。

  2. 将哈希值 H1 再次输入 SHA-256 哈希函数中,得到哈希值 H2。

  3. 将哈希值 H2 再次输入 SHA-256 哈希函数中,得到哈希值 H3。

  4. 以此类推,进行多次迭代。


通过多次迭代哈希函数,我们可以增加密码破解的难度。攻击者需要对每一次迭代都进行大量的计算,从而大大增加了密码破解所需的时间和资源成本。同时,多次迭代哈希函数也提供了更高的密码强度,即使原始密码较为简单,其哈希值也会变得复杂和难以预测。


需要注意的是,多次迭代哈希函数的次数应根据具体的安全需求进行选择。次数过少可能仍然容易受到彩虹表攻击,而次数过多可能会对系统性能产生负面影响。因此,需要在安全性和性能之间进行权衡,并选择适当的迭代次数。


方案3:长度和复杂性要求


要求用户选择强密码,包括足够的长度、复杂的字符组合和随机性,以增加彩虹表的大小和密码破解的难度。


3、字典攻击


最后,我们来介绍一种基于字典的密码破解方法——字典攻击。


字典攻击是 通过使用一个包含常见单词和密码组合的字典文件来尝试破解密码(这文件就是我们常说的字典)。这种方法比暴力破解要高效得多,因为它可以根据常见密码和单词来进行尝试。


如果你使用了常见单词或者简单密码作为密码,那么字典攻击很有可能会成功。


以下是字典攻击的一般步骤:


step1:收集密码字典


攻击者会收集各种常见密码、常用字词、常见姓名、日期、数字序列等组成的密码字典。字典可以是公开的密码列表、泄露的密码数据库或通过爬取互联网等方式获得。


step2:构建哈希表


攻击者会对密码字典中的每个密码进行哈希运算,并将明文密码与对应的哈希值构建成一个哈希表,方便后续的比对操作。


step3:逐个比对


攻击者使用字典中的密码与目标账户的密码进行逐个比对。对于每个密码,攻击者将其进行哈希运算,并与目标账户存储的哈希值进行比较。如果找到匹配的哈希值,那么密码就被破解成功。


字典攻击的成功取决于密码的强度和字典的质量。


如果用户使用弱密码或常见密码,很容易受到字典攻击的威胁。为了抵御字典攻击,用户应该选择强密码,包括使用足够的长度、复杂的字符组合和随机性,以增加密码的猜测难度。而系统设计者可以使用前文介绍的方式来防止密码被破解,如密码加盐和限制登录尝试次数等。


好啦,今天的分享就到这里啦!希望大家都能保护好自己的账户安全,不要成

作者:陈有余Tech
来源:juejin.cn/post/7250866224429563941
为黑客攻击的目标哦!

收起阅读 »

PC网站如何实现微信扫码登录

不管你运营什么类型的网站,用户注册都是很重要的一个环节,用户注册的方式也是很多的,比如邮箱注册、手机号注册、第三方授权登录等。其中,第三方授权登录是最常用的一种方式,微信扫码登录是其中的一种,但是微信扫码登录的实现方式有很多种,比如公众号扫码,小程序扫码,网页...
继续阅读 »

不管你运营什么类型的网站,用户注册都是很重要的一个环节,用户注册的方式也是很多的,比如邮箱注册、手机号注册、第三方授权登录等。其中,第三方授权登录是最常用的一种方式,微信扫码登录是其中的一种,但是微信扫码登录的实现方式有很多种,比如公众号扫码,小程序扫码,网页扫码等。


本文将介绍一种简单的实现方式。


技术栈



  • 后端:NodeJs / 企业级框架 Egg.js

  • 前端:Vue

  • 微信小程序:uni-app

  • 数据库:MySQL


实现思路



  1. PC 端网站生成一个二维码,定时 3s 轮询请求接口,判断用户是否扫码,如果扫码,则返回用户的微信信息。

  2. 用户微信扫码后,会跳转到微信小程序,小程序打开点击注册按钮,会获取到用户的微信信息,然后将用户信息发送到后端。

  3. 后端接收到用户信息后,判断用户是否已经注册,如果已经注册,则直接登录,如果没有注册,则将用户信息 openid 和 mobile 保存到数据库中,新建用户,生成一个 token,返回给 PC 端,展示用户登录成功。

  4. 微信小程序展示用户扫码成功。


实现步骤




  • 需要申请一个微信小程序,用于扫码登录,申请地址:mp.weixin.qq.com/




  • 建表






  • PC 端网站生成二维码



实现效果如下:





  • 微信小程序扫码登录








  • 后端接口实现


路由:app/router.js




  • 生成带唯一 scene 参数的小程序码


app/controller/login.js




收起阅读 »

什么是布隆过滤器?在php里你怎么用?

布隆过滤器(Bloom Filter)是一种用于快速判断一个元素是否属于某个集合的概率型数据结构。它基于哈希函数和位数组实现,可以高效地检索一个元素是否存在,但不提供元素具体的存储和获取功能。 布隆过滤器原理 上面的思路其实就是布隆过滤器的思想,只不过因为 ...
继续阅读 »

布隆过滤器(Bloom Filter)是一种用于快速判断一个元素是否属于某个集合的概率型数据结构。它基于哈希函数和位数组实现,可以高效地检索一个元素是否存在,但不提供元素具体的存储和获取功能。


image.png


布隆过滤器原理


上面的思路其实就是布隆过滤器的思想,只不过因为 hash 函数的限制,多个字符串很可能会 hash 成一个值。为了解决这个问题,布隆过滤器引入多个 hash 函数来降低误判率。


下图表示有三个 hash 函数,比如一个集合中有 x,y,z 三个元素,分别用三个 hash 函数映射到二进制序列的某些位上,假设我们判断 w 是否在集合中,同样用三个 hash 函数来映射,结果发现取得的结果不全为 1,则表示 w 不在集合里面。


image.png


布隆过滤器处理流程


布隆过滤器应用很广泛,比如垃圾邮件过滤,爬虫的 url 过滤,防止缓存击穿等等。下面就来说说布隆过滤器的一个完整流程,相信读者看到这里应该能明白布隆过滤器是怎样工作的。


第一步:开辟空间


开辟一个长度为 m 的位数组(或者称二进制向量),这个不同的语言有不同的实现方式,甚至你可以用文件来实现。


第二步:寻找 hash 函数


获取几个 hash 函数,前辈们已经发明了很多运行良好的 hash 函数,比如 BKDRHash,JSHash,RSHash 等等。这些 hash 函数我们直接获取就可以了。


第三步:写入数据


将所需要判断的内容经过这些 hash 函数计算,得到几个值,比如用 3 个 hash 函数,得到值分别是 1000,2000,3000。之后设置 m 位数组的第 1000,2000,3000 位的值位二进制 1。


第四步:判断


接下来就可以判断一个新的内容是不是在我们的集合中。判断的流程和写入的流程是一致的。


在PHP中如何使用?


在PHP中,可以使用BloomFilter扩展库或自行实现布隆过滤器。下面我将介绍两种方法。


1. 使用BloomFilter扩展库:


PHP中有一些第三方扩展库提供了布隆过滤器的功能。其中比较常用的是phpbloomd扩展,它提供了对布隆过滤器的支持。你可以按照该扩展库的文档进行安装和使用。


示例代码如下:


// 创建一个布隆过滤器
$filter = new BloomFilter();

// 向过滤器添加元素
$filter->add("element1");
$filter->add("element2");
$filter->add("element3");

// 检查元素是否存在于过滤器中
if ($filter->has("element1")) {
echo "Element 1 may exist.";
} else {
echo "Element 1 does not exist.";
}


2. 自行实现布隆过滤器:


如果你不想使用第三方扩展库,也可以自行实现布隆过滤器。下面是一个简单的自实现布隆过滤器的示例代码:


class BloomFilter {
private $bitArray;
private $hashFunctions;

public function __construct($size, $numHashFunctions) {
$this->bitArray = array_fill(0, $size, false);
$this->hashFunctions = $numHashFunctions;
}

private function hash($value) {
$hashes = [];
$hash1 = crc32($value);
$hash2 = fnv1a32($value);

for ($i = 0; $i < $this->hashFunctions; $i++) {
$hashes[] = ($hash1 + $i * $hash2) % count($this->bitArray);
}

return $hashes;
}

public function add($value) {
$hashes = $this->hash($value);

foreach ($hashes as $hash) {
$this->bitArray[$hash] = true;
}
}

public function has($value) {
$hashes = $this->hash($value);

foreach ($hashes as $hash) {
if (!$this->bitArray[$hash]) {
return false;
}
}

return true;
}
}

// 创建一个布隆过滤器
$filter = new BloomFilter(100, 3);

// 向过滤器添加元素
$filter->add("element1");
$filter->add("element2");
$filter->add("element3");

// 检查元素是否存在于过滤器中
if ($filter->has("element1")) {
echo "Element 1 may exist.";
} else {
echo "Element 1 does not exist.";
}


无论是使用扩展库还是自行实现,布隆过滤器在处理大规模数据集合时可以提供高效的元素存在性检查功能,适用于需要快速判断元素是否属于某个集合的场景。


作者:Student_Li
来源:juejin.cn/post/7249933985562984504
收起阅读 »

跨端技术总结

通过淘宝weex,微信,美团kmm,天猫Waft 等不同项目来了解目前各家公司在跨平台方向上有哪些不同的项目,用了什么不同技术实现方式,然后在对比常用的react native ,flutter 和WebAssembly具体在技术上的区别在哪里。 客户端渲染...
继续阅读 »

通过淘宝weex,微信,美团kmm,天猫Waft 等不同项目来了解目前各家公司在跨平台方向上有哪些不同的项目,用了什么不同技术实现方式,然后在对比常用的react native ,flutter 和WebAssembly具体在技术上的区别在哪里。


image.png


客户端渲染执行逻辑


android


层次的底部是 Linux,它提供基本的系统功能,如进程管理,内存管理,设备管理,如:相机,键盘,显示器等内核处理的事情。体系结构第三个部分叫做Java虚拟机,是一种专门设计和优化的 Android Dalvik 虚拟机。应用程序框架层使用Java类形式的应用程序提供了许多的更高级别的服务。允许应用程序开发人员在其应用程序中使用这些服务。应用在最上层,即所有的 Android 应用程序。一般我们编写的应用程序只被安装在这层。应用的例子如:浏览器,游戏等。


image.png


绘制流程




  1. 创建视图




ui生成就是把代码中产生的view和在xml文件配置的view,经过measure,layout,dewa 形成一个完整的view树,并调用系统方法进行绘制。Measure 用深度优先原则递归得到所有视图(View)的宽、高;Layout 用深度优先原则递归得到所有视图(View)的位置;到这里就得到view的在窗口中的布局。Draw 目前 Android 支持了两种绘制方式:软件绘制(CPU)和硬件加速(GPU),会通过系统方法把要绘制的view 合成到不同的缓冲区上


最初的ui配置


image.png


构建成内存中的view tree


image.png


2.视图布局


image.png


3.图层合成


SurfaceFlinger 把缓存 区数据渲染到屏幕,由于是两个不同的进程,所以使用 Android 的匿名共享内存 SharedClient 缓存需要显示的数据来达到目的。 SurfaceFlinger 把缓存区数据渲染到屏幕(流程如下图所示),主要是驱动层的事情,这 里不做太多解释。


image.png


4. 系统绘制


image.png


绘制过程首先是 CPU 准备数据,通过 Driver 层把数据交给 CPU 渲 染,其中 CPU 主要负责 Measure、Layout、Record、Execute 的数据计算工作,GPU 负责 Rasterization(栅格化)、渲染。由于图形 API 不允许 CPU 直接与 GPU 通信,而是通过中间 的一个图形驱动层(Graphics Driver)来连接这两部分。图形驱动维护了一个队列,CPU 把 display list 添加到队列中,GPU 从这个队列取出数据进行绘制,最终才在显示屏上显示出来。


ios:


架构


image.png



  1. Core OS layer



  • 核心操作系统层包括内存管理、文件系统、电源管理以及一些其他的操作系统任务,直接和硬件设备进行交互



  1. Core Services layer



  • 核心服务层,我们可以通过它来访问iOS的一些服务。包含: 定位,网络,数据 sql



  1. Media layer



  • 顾名思义,媒体层可以在应用程序中使用各种媒体文件,进行音频与视频的录制,图形的绘制,以及制作基础的动画效果。



  1. Cocoa Touch layer



  • 本质上来说它负责用户在iOS设备上的触摸交互操作

  • 包括以下这些组件: Multi-Touch Events Core Motion Camera View Hierarchy Localization Alerts Web Views Image Picker Multi-Touch Controls.


ios 的视图树


image.png


ios的 绘制流程:


image.png


image.png


image.png


显示逻辑



  • CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等;

  • RenderServer解析提交的子树状态,生成绘制指令;

  • GPU执行绘制指令;

  • 显示渲染后的数据;


提交流程


image.png


1、布局(Layout)


调用layoutSubviews方法; 调用addSubview:方法;


2、显示(Display)


通过drawRect绘制视图; 绘制string(字符串);



每个UIView都有CALayer,同时图层有一个像素存储空间,存放视图;调用-setNeedsDisplay的时候,仅会设置图层为dirty。 当一个视图第一次或者某部分需要更新的时候iOS系统总是会去请求drawRect:方法。


以下是触发视图更新的一些操作:



  • 移动或删除视图

  • 通过将视图的hidden属性设置为NO

  • 滚动消失的视图再次需要出现在屏幕上

  • 视图显式调用setNeedsDisplay或setNeedsDisplayInRect:方法


视图系统都会自动触发重新绘制。对于自定义视图,就必须重写drawRect:方法去执行所有绘制。视图第一次展示的时候,iOS系统会传递正方形区域来表示这个视图绘制的区域。


在调用drawRect:方法之后,视图就会把自己标记为已更新,然后等待下一次视图更新被触发。



3、准备提交(Prepare)


解码图片; 图片格式转换;


4、提交(Commit)


打包layers并发送到渲染server;


递归提交子树的layers;


web :


web内容准备阶段


web 通常需要将所需要的html,css,js都下载下来,并进行解析执行后才进行渲染,然后是绘制过程,先来看下前期工作


image.png


一个渲染流程会划分很多子阶段,整个处理过程叫渲染流水线,流水线可分为如下几个子阶段:构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。每个阶段都经过输入内容 -->处理过程-->输出内容三个部分。



  1. 渲染进程将HTML内容转换为能够读懂的DOM树结构


image.png



  1. 渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets(生存CSSDOM树),计算出DOM节点的样式


image.png


styleSheets格式
image.png



  1. 创建布局树(LayoutTree),并计算元素的布局信息。


我们有DOM树和DOM树中元素的样式,那么接下来就需要计算出DOM树中可见元素的几何位置,我们把这个计算过程叫做布局。根据元素的可见信息构建出布局树。


image.png
4. 对布局树进行分层,并生成分层树(LayerTree)。


image.png



  1. 为每个图层生成绘制列表,并将其提交到合成线程。


image.png



  1. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。


合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图


image.png


image.png



  1. 合成线程发送绘制图块命令DrawQuad给浏览器进程。浏览器进程根据DrawQuad消息生成页面,并显示到显示器上


image.png


web 在android 中的绘制


WebView实际上是一个ViewGroup,并将后端的具体实现抽象为WebViewProvider,而WebViewChromium正是一个提供基于Chromium的具体实现类。


再回到WebView的情况。当WebView部件发生内容更新时,例如页面加载完毕,CSS动画,或者是滚动、缩放操作导致页面内容更新,同样会在WebView触发invalidate方法,随后在视图系统的统筹安排下,WebView.onDraw方法会被调用,最后实际上调用了AwContents.onDraw方法,它会请求对应的native端对象执行OnDraw方法,将页面的内容更新绘制到WebView对应的Canvas上去。


draw()先得到一块Buffer,这块Buffer是由SurfaceFlinger负责管理的。


然后调用view的draw(canvas)当view draw完后,调用surface.java的unlockAndPostCanvas().


将包含有当前view内容的Buffer传给SurfaceFlinger,SurfaceFlinger将所有的Buffer混合后传给FrameBuffer.至此和native原有的view 渲染就是一样的了。


image.png


成熟的框架的底层原理:


react :


RN 的 Android Bridge 和 IOS Bridge 是两端通信的桥梁, 是由一个转译的桥梁实现的不同语言的通信, 得以实现单靠 JS 就调用移动端原生 APi


架构


image.png



  • RN 的核心驱动力就来自 JS Engine, 我们所有的 JS 代码都会通过 JS Engine 来编译执行, 包括 React 的 JSX 也离不开 JS Engine, JavaScript Core 是其中一种 JS 引擎, 还有 Google 的 V8 引擎, Mozilla 的 SpiderMonkey 引擎。

  • RN 是用类 XML 语言来表示结构, 用 StyleSheet 来规划样式, 但是 UI 控件调用的是 RN 里自己的两端实现控件(android 和 IOS)



  • JavaScript 在 RN 的作用就是给原生组件发送指令来完成 UI 渲染, 所以 JavaScript Core 是 RN 中的核心部分



  • RN 是不用 JS 引擎的 UI 渲染控件的, 但是会用到 JS 引擎的 DOM 操作管理能力来管理所有 UI 节点, 每次在写完 UI 组件代码后会交给 yoga 去做布局排版, 然后调用原生组件绘制

  • bridge 负责js 和native的通讯,以android为例:Java层与Js层的bridge分别存有相同一份模块配置表,Java与Js互相通信时,通过bridge里的配置表将所调用模块方法转为{moduleID,methodID,args}的形式传递给处理层,处理层通过bridge的模块配置表找到对应的方法执行,如果有callback,则回传给调用层


image.png


通讯机制


Java -> Js: Java通过注册表调用到CatalystInstance实例,通过jni,调用到 javascriptCore,传递给调用BatchedBridge.js,根据参数{moduleID,methodID}require相应Js模块执行。


Js -> Java: JS不主动传递数据调用Java。在需要调用调Java模块方法时,会把参数{moduleID,methodID}等数据存在MessageQueue中,等待Java的事件触发,再把MessageQueue中的{moduleID,methodID}返回给Java,再根据模块注册表找到相应模块处理。


事件循环


JS 开发者只需要开发各个组件对象,监听组件事件, 然后利用framework接口调用render方法渲染组件。


而实际上,JS 也是单线程事件循环,不管是 API调用, virtural DOM同步, 还是系统事件监听, 都是异步事件,采用Observer(观察者)模式监听JAVA层事件, JAVA层会把JS 关心的事件通过bridge直接使用javascriptCore的接口执行固定的脚本, 比如"requrire (test_module).test_methode(test_args)"。此时,UI main thread相当于work thread, 把系统事件或者用户事件往JS层抛,同时,JS 层也不断调用模块API或者UI组件 , 驱动JAVA层完成实际的View渲染。JS开发者只需要监听JS层framework定义的事件即可


react 的渲染流程


image.png


首先回顾一下当前Bridge的运行过程。当我们写了类似下面的React源码。


<View style={{ backgroundColor: 'pink', width: 200, height: 200}}/>

JS thread会先对其序列化,形成下面一条消息


UIManager.createView([343,"RCTView",31,{"backgroundColor":-16181,"width":200,"height":200}])

通过Bridge发到ShadowThread。Shadow Tread接收到这条信息后,先反序列化,形成Shadow tree,然后传给Yoga,形成原生布局信息。接着又通过Bridge传给UI thread。UI thread 拿到消息后,同样先反序列化,然后根据所给布局信息,进行绘制。


从上面过程可以看到三个线程的交互都是要通过Bridge,因此瓶颈也就在此。


image.png


首次渲染流程



  1. Native 打开 RN 页面

  2. JS 线程运行,Virtual DOM Tree 被创建

  3. JS 线程异步通知 Shadow Thread 有节点变更

  4. Shadow Thread 创建 Shadow Tree

  5. Shadow Thread 计算布局,异步通知 Main Thread 创建 Views

  6. Main Thread 处理 View 的创建,展示给用户


image.png


react native 新架构


image.png



  • JSI:JSI是Javascript Interface的缩写,一个用C++写成的轻量级框架,它作用就是通过JSI,JS对象可以直接获得C++对象(Host Objects)引用,并调用对应方法


另外JSI与React无关,可以用在任何JS 引擎(V8,Hermes)。有了JSI,JS和Native就可以直接通信了,调用过程如下:JS->JSI->C++->ObjectC/Java



  • Fabric 是 UI Manager 的新名称, 将负责 Native UI 渲染, 和当前 Bridge 不同的是, 可以通过 JSI 导出自己的 Native 函数, 在 JS 层可以直接使用这些函数引用, 反过来 Native 可以直接调用 JS 层, 从而实现同步调用, 这带来更好的数据传输和性能提升


image.png


对比


image.png


flutter:


生产环境中 Dart 通过 AOT 编译成对应平台的指令,同时 Flutter 基于跨平台的 Skia 图形库自建了渲染引擎,最大程度地保证了跨平台渲染的一致性


image.png



  • embedder: 可以称为嵌入器,这是和底层的操作系统进行交互的部分。因为flutter最终要将程序打包到对应的平台中,对于Android平台使用的是Java和C++,对于iOS和macOS平台,使用的是Objective-C/Objective-C++。



  • engine:Flutter engine基本上使用C++写的。engine的存在是为了支持Dart Framework的运行。它提供了Flutter的核心API,包括作图、文件操作、网络IO、dar运行时环境等核心功能。Flutter Engine线程的创建和管理是由embedder负责的。



  • Flutter framework: 这一层是用户编程的接口,我们的应用程序需要和Flutter framework进行交互,最终构建出一个应用程序。


Flutter framework主要是使用dart语言来编写的。framework从下到上,我们有最基础的foundational包,和构建在其上的 animation, painting和 gestures 。


再上面就是rendering层,rendering为我们提供了动态构建可渲染对象树的方法,通过这些方法,我们可以对布局进行处理。接着是widgets layer,它是rendering层中对象的组合,表示一个小挂件。


Widgets 理解


Widgets是Flutter中用户界面的基础。你在flutter界面中能够观察到的用户界面,都是Widgets。大的Widgets又是由一个个的小的Widgets组成,这样就构成了Widgets的层次依赖结构,在这种层次结构中,子Widgets可以共享父Widgets的上下文环境。在Flutter中一切皆可为Widget。


举例,这个Containerks 控件里的child,color,Text 都是Widget。


  color: Colors.blue,
child: Row(
children: [
Image.network('http://www.flydean.com/1.png'),
const Text('A'),
],
),
);

Widgets表示的是不可变的用户UI界面结构。虽然结构是不能够变化的,但是Widgets里面的状态是可以动态变化的。根据Widgets中是否包含状态,Widgets可以分为stateful和stateless widget,对应的类是StatefulWidget和StatelessWidget。


渲染和绘制


渲染就是将上面我们提到的widgets转换成用户肉眼可以感知的像素的过程。Flutter代码会直接被编译成使用 Skia 进行渲染的原生代码,从而提升渲染效率。


代码首先会形成widgets树如下,这些widget在build的过程中,会被转换为 element tree,其中ComponentElement是其他Element的容器,而RenderObjectElement是真正参与layout和渲染的element。。一个element和一个widget对应。然后根据elementrtree 中需要渲染的元素形成RenderTree ,flutter仅会重新渲染需要被重新绘制的element,每次widget变化时element会比较前后两个widget,只有当某一个位置的Widget和新Widget不一致,才会重新创建Element和widget。 最后还会一个layer tree,表示绘制的图层。



四棵树有各自的功能



image.png


Flutter绘制流程


image.png



  • Animate,触发动画更新下一帧的值

  • Build,触发构建或刷新 Widget Tree、Element Tree、RenderObject Tree

  • Layout,触发布局操作,确定布局大小和位置信息

  • CompositeBits,更新需要合成的 Layer 层标记

  • Paint,触发 RenderObject Tree 的绘制操作,构建 Layer Tree

  • Composite,触发 Layer Tree 发送到 Engine,生成 Engine LayerTree


在 UIThread 构建出四棵树,并在 Engine 生成 Scene,最后提交给 RasterThread,对 LayerTree 做光栅化合成上屏。


Flutter 渲染流程


image.png




  • UIThread


    UIThread 是 Platform 创建的子线程,DartVM Root Isolate 所有的 dart 代码都运行在该线程。阻塞 UIThread 会直接导致 Flutter 应用卡顿掉帧。




  • RasterThread


    RasterThread 原本叫做 GPUThread,也是 Platform 创建的子线程,但其实它是运行在 CPU 用于处理数据提交给 GPU,所以 Flutter 团队将其名字改为 Raster,表明它的作用是光栅化。


    C++ Engine 中的光栅化和合成过程运行在该线程。




  • C++ Engine 触发 Platform 注册 VSync 垂直信号回调,通过 Platform -> C++ Engine -> Dart Framework 触发整个绘制流程




  • Dart Framework 构建出四棵树,Widget Tree、Element Tree、RenderObject Tree、Layer Tree,布局、记录绘制区域及绘制指令信息生成 flutter::LayerTree,并保存在 Scene 对象用以光栅化,这个过程运行在 UIThread




  • 通过 Flutter 自建引擎 Skia 进行光栅化和合成操作, 将 flutter::LayerTree 转换为 GPU 指令,并发送给 GPU 完成光栅化合成上屏显示操作,这个过程执行在 RasterThread




整个调度过程是生产者消费者模型,UIThread 负责生产 flutter::Layer Tree,RasterThread 负责消费 flutter::Layer Tree。


flutter 线程模型


image.png


Mobile平台上面每一个Engine实例启动的时候会为UI,GPU,IO Runner各自创建一个新的线程。所有Engine实例共享同一个Platform Runner和线程。




  • Platform Task Runner


    Flutter Engine的主Task Runner,可以理解为是主线程,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个线程供Platform Runner使用。改线程不仅仅处理与Engine交互,它还处理来自平台的消息。




  • UI Task Runner Thread(Dart Runner)


    UI Task Runner被Flutter Engine用于执行Dart root isolate代码,Root isolate运行应用的main code。负责触发构建或刷新 Widget Tree、Element Tree、RenderObject Tree,生成最终的Layer Tree。


    Root Isolate还是处理来自Native Plugins的消息响应,Timers,Microtasks和异步IO(isolate是有自己的内存和单线程控制的运行实体,isolate之间的内存在逻辑上是隔离的)。




  • GPU Task Runner


    GPU Task Runner中的模块负责将Layer Tree提供的信息转化为实际的GPU指令,执行设备GPU相关的skia调用,转换相应平台的绘制方式,比如OpenGL, vulkan, metal等。GPU Task Runner同时也负责配置管理每一帧绘制所需要的GPU资源




  • IO Task Runne




IO Runner的主要功能是从图片存储(比如磁盘)中读取压缩的图片格式,将图片数据进行处理为GPU Runner的渲染做好准备


Dart 是单线程的,但是采用了Event Loop 机制,也就是不断循环等待消息到来并处理。在 Dart 中,实际上有两个队列,一个事件队列(Event Queue),另一个则是微任务队列(Microtask Queue)。在每一次事件循环中,Dart 总是先去第一个微任务队列中查询是否有可执行的任务,如果没有,才会处理后续的事件队列的流程。


image.png


isolate机制尽管 Dart 是基于单线程模型的,但为了进一步利用多核 CPU,将 CPU 密集型运算进行隔离,Dart 也提供了多线程机制,即 Isolate。每个 Isolate 都有自己的 Event Loop 与 Queue,Isolate 之间不共享任何资源,只能依靠消息机制通信,因此也就没有资源抢占问题。Isolate 通过发送管道(SendPort)实现消息通信机制。我们可以在启动并发 Isolate 时将主 Isolate 的发送管道作为参数传给它,这样并发 Isolate 就可以在任务执行完毕后利用这个发送管道给我们发消息。


如果需要在启动一个 Isolate 执行某项任务,Isolate 执行完毕后,发送消息告知我们。如果 Isolate 执行任务时,同时需要依赖主 Isolate 给它发送参数,执行完毕后再发送执行结果给主 Isolate这样的双向通信,让并发 Isolate 也回传一个发送管道即可。


weex:


架构:


image.png



  1. 将weex源码生成JS Bundle,由template、style 和 script等标签组织好的内容,通过转换器转换成JS Bundle

  2. 服务端部署JS Bundle ,将JS Bundle部署在服务器,当接收到终端(Web端、iOS端或Android端)的JS Bundle请求,将JS Bundle下发给终端

  3. WEEX SDK初始化,初始化 JS 引擎,准备好 JS 执行环境

  4. 构建渲染指令树,Weex 里都使用 DOM API 把 Virtual DOM 转换成真实的Element 树,而是使用 JS Framework 里定义的一套 Weex DOM API 将操作转化成渲染指令发给客户端,形成客户端的真实控件

  5. 页面的 js 代码是运行在 js 线程上的,然而原生组件的绘制、事件的捕获都发生在 UI 线程。在这两个线程之间的通信用的是 callNative 和 callJS 这两个底层接口。callNative 是由客户端向 JS 执行环境中注入的接口,提供给 JS Framework 调用。callJS 是由 JS Framework 实现的,并且也注入到了执行环境中,提供给客户端调用。


渲染过程


Weex 里页面的渲染过程和浏览器的渲染过程类似,整体可以分为【创建前端组件】-> 【构建 Virtual DOM】->【生成“真实” DOM】->【发送渲染指令】->【绘制原生 UI】这五个步骤。前两个步骤发生在前端框架中,第三和第四个步骤在 JS Framework 中处理,最后一步是由原生渲染引擎实现的。 页面渲染的大致流程如下


image.png


各家项目的实现方式:


淘宝新⼀代⾃绘渲染引擎 的架构与实践


Weex 技术发展历程


image.png


Weex 2.0 简版架构


最上层的前端生态还是没变的,应该还是以vue的响应式编程为主。


image.png


2.0多了js和c++的直接调用,减少js引擎和布局引擎的通讯开销。


image.png


image.png


Weex 2.0 重写了渲染层的实现,不再依赖系统 UI,改成依赖统一的图形库 Skia 自绘渲染,和 Flutter 原理很像,我们也直接复用了 Flutter Engine 的部分代码。底层把 Weex 对接过的各种原生能力、三方扩展模块都原样接入。对于上层链路,理论上讲业务 JS 代码、Vue/Rax、JS Framework 都是不需要修改的。在 JS 引擎层面也做了一些优化,安卓上把 JavaScriptCore 换成了 QuickJS,用 bytecode 加速二次打开性能,并且结合 Weex js bundle 的特点做针对性的优化。


字节码编译原理


image.png


渲染原理


渲染引擎通用的渲染管线可以简化为【执行脚本】-->【构建节点】-->【布局/绘制】--> 【合成/光栅化】--【上屏】这几个步骤。Weex 里的节点构建逻辑主要在 JS 线程里执行,提交一颗静态节点树到 UI 线程,UI 线程计算布局和绘制属性,生成 Layer Stack 提交到 GPU 线程。


image.png


天猫:WAFT:基于WebAssembly和Skia 的AIoT应用开发框架


整体方案


image.png


为什么选择WebAssemy?


支持 AOT 模式,拔高了性能上限;活跃的开源社区,降低项目推进的风险;支持多语言,拓宽开发者群体。


WebAssembly(又名wasm)是一种高效的,低级别的编程语言。 它让我们能够使用JavaScript以外的语言(例如C,C ++,Rust或其他)编写程序,然后将其编译成WebAssembly,进而生成一个加载和执行速度非常快的Web应用程序


WebAssembly是基于堆栈的虚拟机的二进制指令格式,它被设计为编程语言的可移植编译目标。目前很多语言都已经将 WebAssembly 作为它的编译目标了。


image.png


waft 开发方式


可以看到是采用类前端的开发方式,限定一部分css能力。最后编译为WebAssembly,打包成wasm bundle。 在进行aot 编译城不同架构下的机器码。


image.png


运行流程


可以看到bundle 加载过程中,会执行UI区域的不同的生命周期函数。然后在渲染过程则是从virtual dom tree 转化到widget tree,然后直接通过skia 渲染库直接进行渲染。


image.png


Waft 第二阶段成果-跨平台


image.png


美团KMM在餐饮SaaS中的探索与实践


KMP:Kotlin Multiplatform projects 使用一份kotlin 代码在不同平台上运行


KMM:Kotlin MultiplatformMobile 一个用于跨平台移动应用程序的 SDK。使用 KMM,可以构建多平台移动应用程序并在 Android 和 iOS 应用程序之间共享核心层和业务逻辑等方面。开发人员可以使用单一代码库并通过共享数据层和业务逻辑来实现功能。其实就是把一份逻辑代码编译为多个平台的产物编译中间产物,在不同平台的边缘后端下转为不同的变异后端产物,在不同平台下运行。


image.png


IR 全称是 intermediate representation,表示编译过程中的中间信息,由编译器前端对源码分析后得到,随后会输入到后端进一步编译为机器码


IR 可以有一系列的表现方式,由高层表示逐渐下降(lowering)到低层


我们所讨论的 Kotlin IR 是抽象语法树结构(AST),是比较高层的 IR 表示类型。


有了完备的 IR,就可以利用不同的 后端,编出不同的目标代码,比如 JVM 的字节码,或者运行在 iOS 的机器码,这样就达到了跨端的目的


image.png


对比


image.png


总结


当前存在4种多端方案:



  1. Web 容器方案

  2. 泛 Web 容器方案

  3. 自绘引擎方案

  4. 开放式跨端框架


image.png


引用文章:


zhuanlan.zhihu.com/p/20259704​​


zhuanlan.zhihu.com/p/281238593​​


zhuanlan.zhihu.com/p/388681402​​


juejin.cn/post/708412…​​


guoshuyu.cn/home/wx/Flu…​​


oldbird.run/flutter/u11…​​


w4lle.com/2020/11/09/…​​


blog.51cto.com/jdsjlzx/568…​​


http://www.devio.org/2021/01/10/…​​


gityuan.com/flutter/​​


gityuan.com/2019/06/15/…​​


zhuanlan.zhihu.com/p/78758247​​


http://www.finclip.com/news/f/5

作者:美好世界
来源:juejin.cn/post/7249624871721041975
035…​​

收起阅读 »

日常宕机?聊聊内存存储的Redis如何持久化

Redis 的数据 全部存储 在 内存 中,如果 突然宕机,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态 保存到磁盘 中。 Redis 中的两种持久化方式:...
继续阅读 »

Redis 的数据 全部存储 在 内存 中,如果 突然宕机,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态 保存到磁盘 中。


Redis 中的两种持久化方式: RDB(Redis DataBase)和 AOF(Append Of File)


1. RDB(Redis DataBase)


在指定的 时间间隔内 将内存中的数据集 快照 写入磁盘,也就是行话讲的快照(Snapshot),它恢复时是将快照文件直接读到内存里


1.1 原理


不使用Fork存在的问题





  • Redis 是一个 单线程 的程序,这意味着,我们不仅仅要响应用户的请求,还需要进行内存快照。而后者要求 Redis 必须进行 IO 操作,这会严重拖累服务器的性能。




  • 在 持久化的同时内存数据结构 还可能在 变化,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它删除了,可是这才刚持久化结束。





Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是 最后一次持久化后的数据可能丢失


1.2 fork 函数


根据操作系统多进程 COW(Copy On Write) 机制Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,简单理解也就是基于当前进程 复制 了一个进程,主进程和子进程会共享内存里面的代码块和数据段。




  • Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程

  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术

  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。



1.3 RDB流程图


image.png


1.4 RDB相关配置


1.4.1 配置文件



  • RDB文件默认在redis主目录下的 dump.rdb


image.png



  • 快照默认的保持策略




  1. 先前的快照是在3600秒(1小时)前创建的,并且现在已经至少有 1 次新写入,则将创建一个新的快照;

  2. 先前的快照是在300秒(5分钟)前创建的,并且现在已经至少有 100 次新写入,则将创建一个新的快照;

  3. 先前的快照是在60秒(1分钟)前创建的,并且现在已经至少有 10000 次新写入,则将创建一个新的快照;



image.png


1.4.2 相关指令


save :save时只管保存,其它不管,全部阻塞。手动保存。不建议。


bgsave: Redis 会在后台异步进行快照操作, 快照同时还可以响应客户端请求。


lastsave:获取最后一次成功执行快照的时间


image.png


1.5 RDB如何备份



  1. config get dir  查询rdb文件的目录

  2. 关闭Redis

  3. 将备份文件 dump.rdb 移动到 redis 安装目录并启动 redis 服务即可,备份数据会直接加载。


image.png


2. AOF(Append Of File)


RDB快照不是很持久。如果运行 Redis 的计算机停止运行,电源线出现故障或者您 kill -9 的实例意外发生,则写入 Redis 的最新数据将丢失。


2.1 原理


AOF(Append Of File) 是以 日志 的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来( 读操作不记录 ) , 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作


2.2 AOF流程图



  1. 客户端的请求写命令会被append追加到AOF缓冲区内;

  2. AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;

  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;

  4. Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;


image.png


2.3 AOF相关配置


2.3.1 AOF启动配置


image.png


配置文件默认关闭AOF,将配置文件设置为 appendonly yes 启动AOF。


AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)


2.3.2 AOF 同步fsync频率设置


image.png



  • appendfsync always始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好

  • appendfsync everysec每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。

  • appendfsync noredis不主动进行同步,把同步时机交给操作系统。


借助 glibc 提供的 fsync(int fd) 函数来讲指定的文件内容 强制从内核缓存刷到磁盘。但  "强制开车"  仍然是一个很消耗资源的一个过程,需要  "节制" !通常来说,生产环境的服务器,Redis 每隔 1s 左右执行一次 fsync 操作就可以了。


Redis 同样也提供了另外两种策略,一个是 永不 fsync,来让操作系统来决定合适同步磁盘,很不安全,另一个是 来一个指令就 fsync 一次,非常慢。但是在生产环境基本不会使用,了解一下即可。


2.3.3 查看appendonly.aof文件


image.png
image.png
最后一条del non_existing_key没有追加到appendonly.aof文件中,因为它没有对数据实际造成修改


2.4 AOF如何备份


修改默认的appendonly no,改为yes


2.4.1 正常恢复



  1. config get dir  查询rdb文件的目录

  2. 关闭Redis

  3. 将备份文件 appendonly.aof 移动到 redis 安装目录并启动 redis 服务即可,备份数据会直接加载。


2.4.2 异常恢复



  1. 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof--fix appendonly.aof进行恢复备份被写坏的AOF文件。

  2. 恢复:重启redis,备份数据会直接加载。


2.5 Rewrite重写


Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志 "瘦身"
Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其 原理 就是 开辟 (fork) 一个子进程 对内存进行 遍历 转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件 中。序列化完毕后再将操作期间发生的 增量 AOF 日志 追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。


2.5.1 配置文件


image.png



  • no-appendfsync-on-rewrite=yes:不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)

  • no-appendfsync-on-rewrite=no:还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)


image.png



  • auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)

  • auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。



例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB


系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,


如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。



2.5.2 Rewrit流程图




  1. bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。

  2. 主进程fork出子进程执行重写操作,保证主进程不会阻塞。

  3. 子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。

  4. 子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。

  5. 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。



image.png


3. 总结


3.1 Redis 4.0 混合持久化


重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。


Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小,于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。


3.2 官方建议



  • 官方推荐两个都启用。

  • 如果对数据不敏感,可以选单独用RDB。

  • 不建议单独用 AOF,因为可能会出现Bug。

  • 如果只是做纯内存缓存,可以都
    作者:芒猿君
    来源:juejin.cn/post/7249382407245037623
    不用。

收起阅读 »

Spring Cloud 框架优雅关机和重启

背景 我们编写的Web项目部署之后,经常会因为需要进行配置变更或功能迭代而重启服务,单纯的kill -9 pid的方式会强制关闭进程,这样就会导致服务端当前正在处理的请求失败,那有没有更优雅的方式来实现关机或重启呢? 优雅停机 在项目正常运行的过程中,如果直接...
继续阅读 »

背景


我们编写的Web项目部署之后,经常会因为需要进行配置变更或功能迭代而重启服务,单纯的kill -9 pid的方式会强制关闭进程,这样就会导致服务端当前正在处理的请求失败,那有没有更优雅的方式来实现关机或重启呢?


优雅停机


在项目正常运行的过程中,如果直接不加限制的重启可能会发生一下问题



  1. 项目重启(关闭)时,调用方可能会请求到已经停掉的项目,导致拒绝连接错误(503),调用方服务会缓存一些服务列表导致,服务列表依然还有已经关闭的项目实例信息

  2. 项目本身存在一部分任务需要处理,强行关闭导致这部分数据丢失,比如内存队列、线程队列、MQ 关闭导致重复消费


为了解决上面出现的问题,提供以下解决方案:



  1. 关于问题 1 采用将需要重启的项目实例,提前 40s 从 nacos 上剔除,然后再重启对应的项目,保证有 40s 的时间可以用来服务发现刷新实例信息,防止调用方将请求发送到该项目

  2. 使用 Spring Boot 提供的优雅停机选项,再次预留一部分时间

  3. 使用 shutdonwhook 完成自定的关闭操作


一、主动将服务剔除


该方案主要考虑因为服务下线的瞬间,如果 Nacos 服务剔除不及时,导致仍有部分请求转发到该服务的情况


在项目增加一个接口,同时在准备关停项目前执行 stop 方法,先主动剔除这个服务,shell 改动如下:


run.sh


function stop()  
{
echo "Stop service please waiting...."
echo "deregister."
curl -X POST "127.0.0.1:${SERVER_PORT}/discovery/deregister"
echo ""
echo "deregister [${PROJECT}] then sleep 40 seconds."
# 这里 sleep 40 秒,因为 Nacos 默认的拉取新实例的时间为 30s, 如果调用方不修改的化,这里应该最短为 30s
# 考虑到已经接收的请求还需要一定的时间进行处理,这里预留 10s, 如果 10s 还没处理完预留的请求,调用方肯定也超时了
# 所以这里是 30 + 10 = 40sleep 40
kill -s SIGTERM ${PID}
if [ $? -eq 0 ];then
echo "Stop service done."
else
echo "Stop service failed!"
fi
}

在项目中增加 /discovery/deregister 接口


Spring Boot MVC 版本


import lombok.extern.slf4j.Slf4j;  
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@RestController
@RequestMapping("discovery")
@Slf4j
public class DeregisterInstanceController {

@Autowired
@Lazy
private ServiceRegistry serviceRegistry;

@Autowired
@Lazy
private Registration registration;


@PostMapping("deregister")
public ResultVO<String> deregister() {
log.info("deregister serviceName:{}, ip:{}, port:{}",
registration.getServiceId(),
registration.getHost(),
registration.getPort());
try {
serviceRegistry.deregister(registration);
} catch (Exception e) {
log.error("deregister from nacos error", e);
return ResultVO.failure(e.getMessage());
}
return ResultVO.success();
}
}

Spring Cloud Gateway


通过使用 GatewayFilter 方式来处理


package com.br.zeus.gateway.filter;  

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.isAlreadyRouted;

import com.alibaba.fastjson.JSON;
import com.br.zeus.gateway.entity.RulesResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@Component
@Slf4j
public class DeregisterInstanceGatewayFilter implements GatewayFilter, Ordered {

@Autowired
@Lazy
private ServiceRegistry serviceRegistry;

@Autowired
@Lazy
private Registration registration;

public DeregisterInstanceGatewayFilter() {
log.info("DeregisterInstanceGatewayFilter 启用");
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (isAlreadyRouted(exchange)) {
return chain.filter(exchange);
}

log.info("deregister serviceName:{}, ip:{}, port:{}",
registration.getServiceId(),
registration.getHost(),
registration.getPort());

RulesResult result = new RulesResult();
try {
serviceRegistry.deregister(registration);
result.setSuccess(true);
} catch (Exception e) {
log.error("deregister from nacos error", e);
result.setSuccess(false);
result.setMessage(e.getMessage());
}

ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(result));
response.setStatusCode(HttpStatus.OK);
return response.writeWith(Mono.just(bodyDataBuffer));
}

@Override
public int getOrder() {
return 0;
}
}


在路由配置时,增加接口和过滤器的关系


.route("DeregisterInstance", r -> r.path("/discovery/deregister")  
.filters(f -> f.filter(deregisterInstanceGatewayFilter))
.uri("https://example.com"))

二、Spring Boot 自带的优雅停机方案


要求 Spring Boot 的版本大于等于 2.3


在配置文件中增加如下配置:


application.yaml


server:  
shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 10s

当使用 server.shutdown=graceful 启用时,在 web 容器关闭时,web 服务器将不再接收新请求,并将等待活动请求完成的缓冲期。使用 timeout-per-shutdown-phase 配置最长等待时间,超过该时间后关闭


三、使用 ShutdownHook


public class MyShutdownHook {
public static void main(String[] args) {
// 创建一个新线程作为ShutdownHook
Thread shutdownHook = new Thread(() -> {
System.out.println("ShutdownHook is running...");
// 执行清理操作或其他必要的任务
// 1. 关闭 MQ
// 2. 关闭线程池
// 3. 保存一些数据
});

// 注册ShutdownHook
Runtime.getRuntime().addShutdownHook(shutdownHook);

// 其他程序逻辑
System.out.println("Main program is running...");

// 模拟程序执行
try {
Thread.sleep(5000); // 假设程序运行5秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}

// 当程序退出时,ShutdownHook将被触发执行
}
}
作者:双鬼带单
来源:juejin.cn/post/7249286832168566840

收起阅读 »

35岁愿你我皆向阳而生

35岁是一个让程序员感到焦虑的年龄。这个年纪往往意味着成熟和稳定,但在技术领域,35岁可能意味着过时和被淘汰。在这篇文章中,我将探讨35岁程序员的思考,以及如何应对这种感受,这里我不想贩卖焦虑,只是对现实的一些思考。 当我在20多岁研究生刚刚毕业的时候,恰逢互...
继续阅读 »

35岁是一个让程序员感到焦虑的年龄。这个年纪往往意味着成熟和稳定,但在技术领域,35岁可能意味着过时和被淘汰。在这篇文章中,我将探讨35岁程序员的思考,以及如何应对这种感受,这里我不想贩卖焦虑,只是对现实的一些思考。


当我在20多岁研究生刚刚毕业的时候,恰逢互联网蓬勃发展,到处都是机会,那时候年轻气盛,我充满了能量和热情。我渴望学习新的技术和承担具有挑战性的项目。我花费了无数的时间编码、测试和调试,常常为了追求事业目标而牺牲了个人生活。


慢慢的当我接近30岁的时候,我开始意识到我的优先事项正在转变。我在职业生涯中取得了很多成就,但我也想专注于我的个人生活和关系。我想旅行、和亲人朋友共度时光,并追求曾经被工作忽视的爱好。


现在,35岁的我发现自己处于一个独特的位置。我在自己的领域中获得了丰富的经验,受到同事和同行的尊重。然而,我也感到一种不安和渴望尝试新事物的愿望。这时候我很少在代码上花费时间,而是更多的时间花到了项目管理上,一切似乎很好,但是疫情这几年,行业了很大的影响,公司的运营也变得步履维艰,在安静的会常常想到未来的的规划。


一、焦虑情绪的来源


35岁程序员的焦虑情绪源于其所处的行业环境。技术不断发展,新的编程语言、框架、工具层出不穷,要跟上这些变化需要付出大量的时间和精力。此外,随着年龄的增长,身体和心理健康也会面临各种问题。这些因素加在一起,让35岁程序员感到无从下手,不知道该如何面对未来。


二、面对焦虑情绪的方法


1学习新技能


学习新技能是应对技术革新的必经之路。与其等待公司提供培训或者等待机会,35岁程序员应该主动寻找新技术,并投入时间和精力去学习。通过参加课程、阅读文献,甚至是找到一位 mentor,35岁程序员可以更快地适应新技术,保持竞争力。


2关注行业动态


35岁程序员要时刻关注技术行业的最新动态。阅读技术博客、参加社区活动,以及了解公司的发展方向和战略规划,这些都是成为行业领跑者所必须的。通过增强对行业趋势的了解,35岁程序员可以更好地做出决策,同时也可以通过分享经验获得他人的认可和支持。


3 与年轻人合作


与年轻的程序员合作可以带来许多好处。他们可能拥有更新的知识和技能,并且乐于探索新事物。35岁的程序员应该通过与年轻人合作,学习他们的思考方式和方法论。这样不仅可以加速学习新技能,还可以提高自己的领导能力。


每周我都会组织公司内部的技术交流活动,并积极号召大家发表文章,通过这些技术分享,我发现每个人擅长的东西不同,交流下来大家的收获都很大。


4重新审视个人价值观


在35岁之后,程序员可能会重新审视自己的职业生涯和个人发展方向。当面临焦虑情绪时,建议去回顾一下自己的愿景和目标。这有助于确定下一步的工作方向和计划。此外,35岁程序员也应该考虑个人的非技术技能,例如领导力、沟通能力和团队合作精神,这些技能对长期职业成功至关重要。


5 敞开心扉学会沟通


 程序员给大家的一个刻板印象就是不爱沟通,刻板木讷,大家都是干活的好手,但是一道人际关系处理上就显得有些不够灵活,保持竞争力的一个很关键的点也在于多沟通,多社交,让自己显得更有价值,有一句老话说的好:多一个朋友,多一条路。沟通需要技巧,交朋友更是,这也是我们需要学习的。


三、总结


35岁是程序员生涯中的一个重要节点,同时也是一个充满挑战和机会的时期。如何应对焦虑情绪,保持竞争力并保持个人发展的连续性,这需要程序员深入思考自己的职业规划和发展方向。


通过学习新技能、关注行业动态、与年轻人合作以及审视个人价值观,35岁程序员可以在未来的职业生涯中不断成长和发展。


归根到底,无论如何生活的好与坏都在于我们对待生活的态度,幸福是一种感受,相由心生,无论你处于何种生活状态,

作者:mikezhu
来源:juejin.cn/post/7246778558248632378
都希望大家向阳而生。

收起阅读 »

句柄是什么?一文带你了解!

今天又学习了一个装X概念——句柄,看字面意思,感觉跟某种器具有关,但实际上,这个词可不是用来打造家居用品的。 相信不少人跟我一样,第一眼看到这个词,脑海里只有一个大大的问号。不过,没关系,我们可以一起学习,毕竟,装X路上永远没有止境。 1、官方一点儿的定义 在...
继续阅读 »

今天又学习了一个装X概念——句柄,看字面意思,感觉跟某种器具有关,但实际上,这个词可不是用来打造家居用品的。


相信不少人跟我一样,第一眼看到这个词,脑海里只有一个大大的问号。不过,没关系,我们可以一起学习,毕竟,装X路上永远没有止境。


1、官方一点儿的定义


在计算机科学中,句柄(Handle)是一种引用或标识对象的方式,它可以用来访问或操作底层系统资源。


不同的操作系统可能会有不同的实现和用途,下面我将以不同的操作系统为例来解释句柄的意义。


1. Windows操作系统


在 Windows 中,句柄是一种整数值,用于标识和访问系统对象或资源,如窗口、文件、设备等。


句柄充当了对象的唯一标识符,通过句柄可以对对象进行操作和管理。


示例代码(C++):


HWND hWnd = CreateWindow(L"Button", L"Click Me", WS_VISIBLE | WS_CHILD, 10, 10, 100, 30, hWndParent, NULL, hInstance, NULL);
if (hWnd != NULL) {
// 使用句柄操作窗口对象
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);
// ...
}

在上述代码中,通过 CreateWindow 函数创建一个按钮窗口,并将返回的句柄存储在 hWnd 变量中。然后,可以使用 hWnd 句柄来显示窗口、更新窗口等操作。


2. Linux操作系统


在 Linux 中,句柄通常称为文件描述符(File Descriptor),它是一个非负整数,用于标识打开的文件、设备、管道等。


Linux将所有的I/O操作都抽象为文件,并使用文件描述符来引用和操作这些文件。


示例代码(C):


int fd = open("file.txt", O_RDONLY);
if (fd != -1) {
// 使用文件描述符读取文件内容
char buffer[1024];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
// ...
close(fd);
}

上述代码中,通过 open 函数打开文件 file.txt,并将返回的文件描述符存储在 fd 变量中。然后,可以使用 fd 文件描述符来进行文件读取等操作。


3. macOS操作系统


在 macOS 中,句柄也称为文件描述符(File Descriptor),类似于 Linux 操作系统的文件描述符。它是一个整数,用于标识和访问打开的文件、设备等。


示例代码(Objective-C):


int fileDescriptor = open("/path/to/file.txt", O_RDONLY);
if (fileDescriptor != -1) {
// 使用文件描述符读取文件内容
char buffer[1024];
ssize_t bytesRead = read(fileDescriptor, buffer, sizeof(buffer));
// ...
close(fileDescriptor);
}

在上述代码中,通过 open 函数打开文件 /path/to/file.txt,并将返回的文件描述符存储在 fileDescriptor 变量中。然后,可以使用 fileDescriptor 文件描述符来进行文件读取等操作。


总结起来,句柄(Handle)是一种在操作系统中用于标识、访问和操作系统资源的方式。


不同的操作系统有不同的实现和命名,如 Windows 中的句柄、Linux 和 macOS 中的文件描述符。句柄提供了一种抽象层,使得程序可以使用标识符来引用和操作底层资源,从而实现对系统资源的管理和控制。


2、通俗易懂的理解


可以把句柄理解为一个中间媒介,通过这个中间媒介可控制、操作某样东西。


举个例子。door handle 是指门把手,通过门把手可以去控制门,但 door handle 并非 door 本身,只是一个中间媒介。


又比如 knife handle 是刀柄,通过刀柄可以使用刀。


跟 door handle 类似,我们可以用 file handle 去操作 file, 但 file handle 并非 file 本身。这个 file handle 就被翻译成文件句柄,同理还有各种资源句柄。


3、为什么要发明句柄?


句柄的引入主要是为了解决以下几个问题:




  1. 资源标识:操作系统中存在各种类型的资源,如窗口、文件、设备等。为了标识和引用这些资源,需要一种统一的方式。句柄提供了一个唯一的标识符,可以用于识别特定类型的资源。




  2. 封装和抽象:句柄将底层资源的具体实现进行了封装和抽象,提供了一种更高层次的接口供应用程序使用。这样,应用程序不需要了解资源的内部细节和底层实现,只需要通过句柄进行操作。




  3. 安全性和隔离:句柄可以充当一种权限验证的机制,通过句柄来访问资源可以进行权限检查,从而保证了资源的安全性。此外,句柄还可以实现资源的隔离,不同句柄之间的资源操作互不影响。




  4. 跨平台和兼容性:不同的操作系统和平台有各自的资源管理方式和实现,句柄提供了一种统一的方式来操作不同平台上的资源。这样,应用程序可以在不同的操作系统上运行,并使用相同的句柄接口来访问资源。




总之,句柄提供了一种统一、封装、安全和跨平台的解决方案,使得应用程序可以更方便地操作和管理底层系统资源。


好了,看完以上内容你也许会感叹:句柄?这不就是个把手吗!但是,别小瞧这个

作者:陈有余Tech
来源:juejin.cn/post/7246279539986743354
把手,它可是万能的!

收起阅读 »

拉新、转化、留存,一个做不好,就可能会噶?

用户周期 对于我们各个平台来说(掘金也是),我们用户都会有一个生命周期:引入期--成长期--成熟期--休眠期--流失期。 而一般获客就在引入期,在这个时候我们会通过推广的手段进行拉新;升值期则发生在成长期和成熟期,在两个阶段,我们会通过各种裂变营销(比如红包、...
继续阅读 »

用户周期


对于我们各个平台来说(掘金也是),我们用户都会有一个生命周期:引入期--成长期--成熟期--休眠期--流失期。


而一般获客就在引入期,在这个时候我们会通过推广的手段进行拉新;升值期则发生在成长期和成熟期,在两个阶段,我们会通过各种裂变营销(比如红包、券、积分、满减等手段)去实现用户的转化;那到了休眠期和流失期,平台则会通过精准营销去实现用户的留存。


详细的表格可以看下面:


image.png


运营手法


讲完了用户周期,我们再来说一下运营手法(让大家做一只明白的羊)。目前比较主流的运营手法包括红包、优惠券、返现、积分、裂变营销、砍价、秒杀......


拼xx新人红包:


image.png


饿了X红包:


image.png


拼xx砍价


image.png


其他我们就不列举了,大家生活中肯定有很多体会。在不同的行业,我们被“安排”的方式是不一样的。我们拿3个行业来举例子:


image.png


潜在风险


那从企业的角度出发,在这些环节当中,有可能会出现如下的一些常见风险:客户端风险、账号安全风险、营销活动风险、交易支付风险、爬虫风险。我们一一来过一下这些风险:


1.客户端风险:


image.png


2.账户安全风险


image.png
3.营销活动风险


image.png
4.交易支付风险


image.png
5.爬虫风险


image.png


黑灰产风险


1.简单介绍


目前我们网络黑灰产的从业人数已经超过1000万(和今年毕业的大学生人数有的一比了),其产业造成的损失每年也已经超过千亿。如今数据泄露已经成为社会问题,也引起了各大企业的重视。并且有个点(可能会被喷),部分的黑灰产的安全攻防人员专业度已经超过我们很多安全技术人员了。毕竟只有千年做贼的,没有千年防贼的。


那在我们的AI技术加持之下,我们后续的黑灰产发展必将和产业链会进一步深度融合,同时目前有一个很显著的特征是:黑灰产正在尝试将自己的攻击行为隐藏在其他用户的行为之。


另外,目前受攻击比较多的行业会包括说电商、出行、政务、直播、广告、游戏、社交等,而分布的场景则是我们前面说的爬虫攻击、薅羊毛、账户风险、交易支付等等。


2.欺诈流程


我们来讲一下黑灰产一般的攻击手段,也就是欺诈的一个流程:


image.png


第一步:账号准备。这一步会包括说图里的社工、注册机这些比较常用和典型的,也有其他一些方式。


第二步:情报收集。这一步包括流程的体验以及工具的准备。


第三步:伺机而发。等待活动开始,直接上去薅羊毛


第四步:利益套现。他们会代理或者海鲜市场等等,进行套现。代理的话,游戏行业会用的更多,而海鲜市场,类似于电商的优惠券之类的,会用的比较多。


欺诈工具


目前主流的欺诈工具有如下几种:


1.模拟器:这个主要是针对弱防护的场景


2.设备牧场:现在是应该发展到了第三代全托管牧场。一般可以去做设备识别,针对环境监测和真机检测。


3.接码平台:这个大家应该很熟悉。我们公司做的是安全验证码,而来注册的一部分客户则来找的是接码平台。


4.打码平台:这个其实和接码平台是类似的,不过接码平台针对的是短信验证码,而打码平台针对的是滑动验证码、图片验证码等等。


举个例子:某宝KFC代下单服务泛滥
image.png


这块我就不介绍更多了,生怕有人学坏哈哈


防护措施


因为企业目前对这一块都比较重视,所以随之而来的安全产品目前也发展到了一定的阶段。总的来说,目前一般会采取如下的防御体系:


image.png


基本是3个平台+3个场景+2个服务


产品一般会组合使用(单个场景针对使用也是可以的),会包括:设备指纹+无感验证+端加固+安全SDK
平台:实施决策平台+智能建模平台+关联网络平台
场景:基本上针对的场景就是我前面说的那些营销场景


方案优势


那通过上面这一套方案,我们可以做到:


事前: 需要在事前事中事后多点进行布控,各环节分别进行防控。


事中: 事中的防控可以将更多的黑名单数据反馈到事前环节的判断。


事前: 事后的分析与建模可以将模型能力赋予事中的风险防控,同时也可以积累大量的黑样本供事前风险防控来使用。


image.png


基于标准数据接口进行的模块化组合设计,基于成熟的技术架构和技术优势,可定制、可扩展、可集成、跨平台,在个性化需求的处理方面,有着很好的优势。产品各个模块之间既可相互组合又可自定义配置,灵活的产品配置方式和架构设计思想,可结合不同的业务场景及系统状况进行相应的风险防控方案配置。


结语


在现在AI诈骗频发的时代,其实更受冲击的是金融银行的业务安全,因为我们账号汇款目前用的比较多的是人脸识别,那通过AI换脸,黑灰产完全可以实现相应的技术替换。虽然说这个是用户自己的信息泄露导致的安全问题,但是在问责上,肯定银行也会或多或少受到影响,所以最好是能够有一个合适的风控系统去进行相应的处理。


等端午结束之后吧,有空写一篇关于AI诈骗横行的当下,金融银行要如何应对。<

作者:昀和
来源:juejin.cn/post/7246571942329172027
/p>

以上。

收起阅读 »

妹纸问我怎么下载B站视频?你等我一下

文章已同步至【个人博客】,欢迎访问【我的主页】😃 文章地址:blog.fanjunyang.zone/archives/do… 前言 今天有一个妹纸向我提出了一个问题 是时候"出手"了,本着助人为乐的精神,这个忙必须帮(没办法,我就喜欢帮助别人) 现在我们...
继续阅读 »

文章已同步至【个人博客】,欢迎访问【我的主页】😃

文章地址:blog.fanjunyang.zone/archives/do…



前言


今天有一个妹纸向我提出了一个问题


docker-alltube-1


是时候"出手"了,本着助人为乐的精神,这个忙必须帮(没办法,我就喜欢帮助别人)


现在我们在下载一些比如:Bilibili,YouTube等第三方视频的时候,还是比较困难的,需要找各种下载器和网站,而且还不一定能下载,一些免费好用的下载网站还不好找。
所以我们可以自己动手搭一个下载站点,来下载各大平台上的视频。


搭建的站点(大家轻点薅):dl.junyang.space/

站点的地址会随着时间更新,如果上面的地址不能访问的话,大家可以去我的 博客 ,我会把站点入口放在【顶部菜单栏】->【百宝箱】里面)


相关链接&环境配置


最好用国外的服务器,如果用国内的服务器,是下载不了YouTube等需要魔法网站的视频的


docker、docker-compose安装:blog.fanjunyang.zone/archives/de…
Nginx Proxy Manager安装使用:blog.fanjunyang.zone/archives/ng…

使用的GitHub的开源项目:github.com/Rudloff/all…

使用的Docker镜像:hub.docker.com/r/dnomd343/…


搭建方式


创建相关目录


mkdir -p /root/docker_data/alltube
cd /root/docker_data/alltube

创建yml文件


version: '3.3'
services:
alltube:
restart: always
container_name: alltube
environment:
# 自己网站的title
- 'TITLE=My Alltube Site'
- CONVERT=ON
- STREAM=ON
- REMUX=ON
ports:
# 左侧端口号换成你服务器上未使用的端口号
- '24488:80'
image: dnomd343/alltube

运行yml文件


进入/root/docker_data/alltube文件夹下面,运行命令:docker-compose up -d


或者在任意文件夹下面,运行命令:docker-compose -f /root/docker_data/alltube/docker-compose.yml up -d


访问使用


可以直接使用【IP + PORT】的方式访问(需要放通对应端口号的防火墙或安全组)


最好配置反向代理,用域名访问,可以参考:blog.fanjunyang.zone/archives/ng…


她对我说


当我把下载链接发给她时,她说:你真是个好人,正好我让我男朋友也用一下。


我不能忍,然后我默默的把站点删除、下线,眼里留下了悔恨的泪水。


注意事项&问题



  • 目前解析不出来B站的视频封面(YouTube可以正常解析),不过不影响下载

  • 因为B站音视频是分开的,所以需要下载两次(一次视频、一次音频),然后整合一下就好了

  • 因国内版权限制的原因,部分资源无法解析是正常现象

  • 下载的时候可以选择视频格式


docker-alltube-2

收起阅读 »

腾讯视频技术团队偷懒了?!

腾小云导读 PC Web 端、手机 H5 端、小程序端、App 安卓端、App iOS 端......在多端时代,一个应用往往需要支持多端。若每个端都独立开发一套系统来支持,将消耗巨大的人力和经费!腾讯视频团队想到一个“偷懒”的方法——能不能只开发一套基础系统...
继续阅读 »


动图封面


腾小云导读


PC Web 端、手机 H5 端、小程序端、App 安卓端、App iOS 端......在多端时代,一个应用往往需要支持多端。若每个端都独立开发一套系统来支持,将消耗巨大的人力和经费!腾讯视频团队想到一个“偷懒”的方法——能不能只开发一套基础系统,通过兼容不同平台的特性,来快速编译出不同平台的应用呢?本篇特邀腾讯视频团队为你分享快速编译出支持多端的应用、一套代码行走天下的“偷懒”历程。欢迎阅读。


目录


1 背景


2 设计思路


3 具体实现


4 总结


01、 背景


腾讯视频搜索在多个端都存在:安卓 App 端搜索、iOS App 端搜索、H5 端搜索、小程序端搜索、PC Web 端、PC 客户端搜索。每个端,除了个别模块的样式有细微差异之外,其他都一样,如下面的图片所示。



按照以前的现状,安卓 App 端搜索一套代码、iOS App 端搜索一套代码、手机 H5 端一套代码、小程序端搜索一套代码、PC 客户端一套代码、PC Web 端一套代码......每套代码都是独立开发,独立维护,成本非常高。并且,后端的搜索接口以前也是分散在多个不同的协议中,有的平台是 jce 协议的接口,有的是 PB 协议的接口,也是五花八门。


随着业务增长的需求,我们已经没有足够的时间来维护各自一套独立的系统,我们打算进行升级改革!治理的办法就是:收敛!把后端不同平台的接口都归一到同一个接口中,通过平台号来区分;前端也将不同平台的代码,收敛归一成一套代码,通过条件编译来兼容适配不同平台的差异性,不同的平台,在蓝盾流水线中配置不同的参数来上线,从而达到多合一的效果。


总体来说,我们团队就实现一个“多端合一的万能模板”的想法达成一致。并且,我们希望使用 hippy-vue 技术栈。


理由有以下:


Hippy 是公司级别的中台框架,有专门的团队在进行问题的修复和功能的迭代开发,并且广泛应用到了很多公司级的应用中,暂时不会出现“荒芜丢弃”的局面; Hippy 是为了抹平 iOS、Android 双端差异,提供接近 Web 的开发体验而生,在上层支持了 React 和 Vue 两套界面框架,前端开发人员可以通过它,将前端代码转换为终端的原生指令,进行原生终端 App 开发; Hippy 在底层进行了大量的优化,使利用 Hippy 框架开发的终端 App 应用,在启动速度,可复用列表组件、渲染效率、动画速度、网络通信等方面都提供了业内顶尖的性能表现,值得信赖; Hippy 在上层支持 Vue 技术栈,正好我们团队目前所有的前端项目也都统一为 Vue 技术栈,开发人员上手毫无违和感。

02、设计思路


系统的架构图如下所示:



通用模版为了简化开发、提高开发效率,在模版中集成了大量现有组件和工具包,具体可以分为以下三层:



  • 第三方工具层


在通用工具库中,模版包装并提供了很多常用方法,比如 cookie 的设置和 cookie 的获取方法;DOM 的操作方法;Cache 的设置,Cache 的获取,Cache 的过期时间等。在第三方接入库中,模版已经接好了 Aegis 监控,Tab 实验的实验值获取,大同上报等;在打包编译库中,模版提供了通用的 Hippy App 打包安卓脚本和 IOS 脚本、H5 的打包脚本、小程序地打包脚本、一套代码,运行不同的打包命令,执行不同的编译打包脚本,就可以生成不同平台对应的发布包。编译打包在后续还会详细讲解。



  • 数据管理层


在这层中,模版集成了跟数据处理相关的模块。


在 Store 层,由于该模块是基于 Vue2 实现的(Vue3 会在下一个版本中提供),模版已经集成好了 Vuex、State、Getters、Mutations、Actions 等,并且都有实例代码(该模版是基于 Vue2 实现的,Vue3 会在下一个版本中提供);


在 Model 层,模版提供了一套将 PB 文件转化为 TS 类文件的方法,方便快速接入后端PB协议接口请求;同时,还包装了接口通用请求方法,以及全局的统一错误处理上报方法;在数据配置中,模版提供了全局的常量配置文件,应用的版本配置文件(版本的配置对 Hippy App 的应用非常实用),以及 UI 样式的配置(正常模式样式还是暗黑模式样式,宽屏,窄屏等)。



  • UI 层


为了提高开发速度,提高开发效率,模版提供了示例页面代码。同时,根据脚手架来选择是否需要路由,来动态添加应用的路由;以及常用的基础组件库。这些组件库中的组件,是从众多 Hippy 应用中提取出来,实用又高效。


03、具体实现


本文将从 Hippy App 端实现,Hippy H5 端的实现和 Hippy 微信小程序端端实现来分别展开介绍。


下图是 Hippy App 端实现逻辑。



App 端的入口文件为 main-native.ts。在里面,声明了一个 App 实例,指定 phone 下的一些属性设置,比如状态栏、背景色等等。同时,需要用到的 native 组件,都需要在 main-native 中进行声明绑定,才可以在页面中使用。


例如:下图示例中注册声明了两个 native 组件,LottieView 和 VideoView,在页面中就可以直接使用这两个 native 组件。


Vue.registerElement('LottieView');
Vue.registerElement('VideoView', {
component: {
name: 'VideoView',
processEventData(event: any, nativeEventName: string, nativeEventParams: any) {
// To do something for the native component event
return event;
},
},});

main-native 中还有一个重要的方法:app.$start() 方法。


该方法为 Hippy 引擎初始化完成后回调到 Hippy 前端的方法;Hippy 端跟 App 方法进行通信,通过 jsbridge 来进行,模版中已经封装好了具体方法;Hippy 请求后端接口,通过 fetch 协议,也有具体的协议方法封装;Hippy 在 App 内部的跳转,是通过伪协议跳转来实现的。


Hippy App 应用的部署分为以下三种情况:



  • 本地调试


本地调试是通过 Hippy + Chrome Devtools 来完成,通过 WS 通道转发消息,具体流程如下图。




  • 部署测试环境


模版中引入了环境变量参数,同时在代码模版中做了大量环境变量的兼容逻辑,比如测试环境用测试环境的接口,正式环境用正式环境的接口;测试环境用测试环境的 CDN,静态文件上传到测试环境,测试环境部署测试环境的离线包等;测试环境的调试我们是通过离线包的方式来实现的,有专门的测试环境流水线接入使用,只需要稍微做少许调整即可,有需要的可以私聊。



  • 部署正式环境


正式环境流程会做这样几件事情:正式环境接口、正式环境的 CDN、正式环境的日志上、部署正式环境的离线包平台、图片的特殊处理。因为 App 端是采用离线包的形式,如果所有本地图片都打包到离线包中,会导致离线包包体积很大,会影响到 App 的整体体积大小和离线包的下载速度。模版中做了针对图片的特殊脚本处理:引入了图片编译大小变量:STATIC_SIZE_LIMIT。当大于该限制条件的图片都一律上传到 CDN,如果想保留的,则需要增加特殊声明:inline。


具体流程如下图所示:



Hippy H5 的实现流程如下图所示。



Hippy H5 的实现跟 App 的实现流程类似,但是差异如下:


App 的入口文件为 main-native.ts,h5 的入口文件为 main.ts 文件。H5 的入口文件中,没有关于 iphone 的设置,跟 Web 的设置一样;H5 的路由用 vue-router,页面中的路由跳转都是 H5 超链接,不是伪协议;H5 的本地调试很简单,跟 Vue Web 一样,都是在本地起 http-server 来测试;测试环境的部署和正式环境的部署都是采用的服务器来部署,不是离线包。

这里重点讨论一下大同上报的实现。大同上报在 App 端的上报参数声明跟 H5 端的上报参数声明不一致,如何统一这些差异?模版中的解决方案是:封装自定义标签 Directive。


具体实现如下:在 Directive 标签中兼容 App 和 H5 的不一致。


/**
* @example
*
* <element v-report="elementReportInfo" />
* <element v-report="{ eid, ...extra }" />
* <page v-report="{ pgid, ...extra }" />
* <page v-report.page="assertPageReport" />
*/

Vue.directive('report', {
bind(el) {
el.addEventListener('layout', throttledForceReport);
},
unbind(el) {
el.removeEventListener('layout', throttledForceReport);
},
inserted: setReport,
update: setReport,
} as DirectiveOptions);

很多人可能会问,Hippy App 跟 Hippy H5 有很多不同的地方,如果写两套代码,会不会导致代码的体积变得很大?答案是一定的,为了解决以上问题,该万能模版提供了条件编译。引入环境变量:isNative。然后,根据该条件,进行条件编译,不同的平台,生成不同平台的代码,避免了生成大量冗余代码。


Hippy 微信小程序的实现流程如下图所示:



小程序的实现是基于 Taro Vue 框架。该框架跟 Hippy Vue 框架天然兼容,但是也有一些小程序的特殊地方:


小程序的入口文件约定为 app.ts,创建 app 实例是在 app.ts 中来完成;小程序的主页面文件为 app.vue,在其中定义小程序的状态栏,标题栏,页面等;小程序的全局配置在 app.config.ts 中;小程序的构建脚本在 script 中的 index.js。小程序的代码是基于 Hippy Vue 的代码通过 Taro 自动构建转化而成,很多配置都是自动生成的,只需要在开发的时候,遵循约定的命名规范即可。

为了一套代码能够同时支持 App,H5 和微信小程序,需要遵循一些约定的规范,否则在从 Hippy Vue 转化为 Taro Vue 的时候会遇到一些问题:


文件夹命名规范:全部小写加“-”, 例如:node-redis, agent-base 等,不要用大驼峰等;文件的命名规范:跟文件夹命名规范一致,全部小写加“-”,例如:eslint-recomment.js;属性的命名:也采用小写加“-”, 例如:data-url。

04、总结


目前该模版已经在腾讯视频的搜索场景落地,并且上线应用,但是,还是有一些需要共同打磨的地方:


Vue3 的支持:目前我们是基于 Hippy Vue2 来实现的。随着 Vue3 的广泛应用,后续我们需要升级到 Vue3。


组件丰富:通用组件的种类还不是特别丰富,只是基于我们腾讯视频搜索场景进行的封装,后续可以补充更多更丰富的组件。


迭代升级:通用组件目前还是通过源代码的方式存放在代码模版中,不利于后续组件的升级迭代。计划后续会把组件给迁移到我们的应用组件库平台 Athena,该平台我们会后续发专文介绍,大家敬请期待。


以上是本次分享全部内容,如果觉得文章还不错的话欢迎分享~


-End-


原创作者|熊才刚


技术责编|陈恕胜



你有什么开发提效小技巧?欢迎在腾讯云开发者公众号评论区中分享你的经验和看法。我们将选取1则最有意义的分享,送出腾讯云开发者-文化衫1件(见下图)。6月21日中午12点开奖。



图片


图片
图片
图片


作者:腾讯云开发者
来源:juejin.cn/post/7246056370624495671
收起阅读 »

23美团一面:双检锁单例会写吗?(总结所有单例模式写法)

面试经历 (后来别人跟我说这种写法nacos里也常见) 记录一次面试经历 2023.06.01 美团海笔的,本来以为笔的情况也不咋好 看到牛客网上一堆ak说没面试机会的 结果也不知怎地到了一面 (略过自我介绍和项目介绍~) 面试官:会写单例吗,写个单例看看 ...
继续阅读 »

面试经历


(后来别人跟我说这种写法nacos里也常见)


记录一次面试经历


2023.06.01


美团海笔的,本来以为笔的情况也不咋好 看到牛客网上一堆ak说没面试机会的


结果也不知怎地到了一面


image.png


(略过自我介绍和项目介绍~)


面试官:会写单例吗,写个单例看看


我:


// 饿汉式
public class SingleObject {
private static SingleObject instance = new SingleObject();

//让构造函数为 private
private SingleObject(){}

public static SingleObject getInstance(){
return instance;
}
}

面试官:嗯 你这个单例在没有引用的时候就创建了对象?优化一下


我:应该是懒汉模式!


public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
// 多了一个判断是否为null的过程
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

面试官:你这个线程不安全啊 再优化一下?


我:那就加个锁吧


public class Singleton {  
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

面试官:这种写法 多线程来了的话会阻塞在一起 能否再优化?


我:。。。 不会了


面试官:回去看看双检锁单例


···


之后问了数据库事务



  1. 读未提交(read uncommitted)

  2. 读已提交(read committed)

  3. 可重复读(repeatable read)

  4. 序列化(serializable)以及默认是哪个(repeatable read) 、


数据库的范式了解吗 等等


不出意外:


image.png


还是非常可惜的 这次机会 再加油吧


单例模式整理


学习自菜鸟教程


1.饿汉式


懒加载 no
多线程安全 yes
缺点:没有实现懒加载,即还未调用就创建了对象


public class Singleton {  
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

2.懒汉式(线程不安全)


懒加载 yes
多线程安全 no


public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

3.懒汉式(线程安全)


懒加载 yes
多线程安全 yes
缺点:和面试官说的那样,多线程访问会阻塞


public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

4.双检锁/双重校验锁(DCL,即 double-checked locking)


public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

这段代码是一个单例模式的实现,使用了双重检查锁定的方式来保证线程安全和性能。双重检查锁定是指在加锁前后都进行了一次判空的操作,以避免不必要的加锁操作。


而为了保证双重检查锁定的正确性,需要使用volatile关键字来修饰singleton变量,以禁止指令重排序优化。(JUC的知识串起来了!)如果没有volatile关键字修饰,可能会出现一个线程A执行了new Singleton()但是还没来得及赋值给singleton,而此时另一个线程B进入了第一个if判断,判断singleton不为null,于是直接返回了一个未初始化的实例,导致程序出错。


使用volatile关键字可以确保多线程环境下的可见性和有序性,即一个线程修改了singleton变量的值,其他线程能够立即看到最新值,并且编译器不会对其进行指令重排序优化。这样就能够保证双重检查锁定的正确性。


学到了 !!!


后来别人提醒:


image.png


image.png


(牛逼。。)


5.登记式/静态内部类


public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

这段代码是一个单例模式的实现,使用了静态内部类的方式来保证线程安全和性能。静态内部类是指在外部类中定义一个静态的内部类,该内部类可以访问外部类的所有静态成员和方法,但是外部类不能访问内部类的成员和方法。


在这个单例模式的实现中,SingletonHolder就是一个静态内部类,它里面定义了一个静态的、final的、类型为Singleton的变量INSTANCE。由于静态内部类只有在被调用时才会被加载,因此INSTANCE对象也只有在getInstance()方法被调用时才会被初始化,从而实现了懒加载的效果。


由于静态内部类的加载是线程安全的,因此不需要加锁就可以保证线程安全。同时,由于INSTANCE是静态final类型的,因此保证了它只会被实例化一次,并且在多线程环境下也能正确地被发布和共享。


这种方式相对于双重检查锁定来说更加简单和安全,因此在实际开发中也比较常用。


6. 枚举


public enum Singleton {  
INSTANCE;
public void whateverMethod() {
}
}

这段代码是使用枚举类型实现单例模式的一种方式。在Java中,枚举类型是天然的单例,因为枚举类型的每个枚举值都是唯一的,且在类加载时就已经被初始化。


在这个示例代码中,Singleton是一个枚举类型,其中只定义了一个枚举值INSTANCE。由于INSTANCE是一个枚举值,因此它在类加载时就已经被初始化,并且保证全局唯一。在使用时,可以通过Singleton.INSTANCE来获取该单例对象。


需要注意的是,虽然枚举类型天然的单例特性可以保证线程安全和反序列化安全,但是如果需要延迟初始化或者有其他特殊需求,仍然需要使用其他方式来实现单例模式。


7. 容器式单例


Java中可以使用容器来实现单例模式,比如使用Spring框架中的Bean容器。下面是一个使用Spring框架实现单例的示例代码:



  1. 定义一个单例类,比如MySingleton:


public class MySingleton {
private static MySingleton instance;
private MySingleton() {}
public static MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton();
}
return instance;
}
public void doSomething() {
// do something
}
}


  1. 在Spring的配置文件中定义该类的Bean:


<bean id="mySingleton" class="com.example.MySingleton" scope="singleton"/>


  1. 在Java代码中获取该Bean:


ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
MySingleton mySingleton = (MySingleton) context.getBean("mySingleton");
mySingleton.doSomething();

在上面的代码中,我们在Spring的配置文件中定义了一个名为mySingleton的Bean,它的类是com.example.MySingleton,作用域为singleton(即单例)。然后在Java代码中,我们通过ApplicationContext获取该Bean,并调用它的doSomething()方法。


使用Spring框架可以方便地管理单例对象,同时也可以很容易地实现依赖注入和控制反转等功能。


总结完成 继续加油!!!


作者:ovO
来源:juejin.cn/post/7244820297290465317
收起阅读 »

记一次雪花算法遇到的 生产事故!

你好,我是悟空。 最近生产环境遇到一个问题: 现象:创建工单、订单等地方,全都创建数据失败。 初步排查:报错信息为duplicate key,意思是保存数据的时候,报主键 id 重复,而这些 id 都是由雪花算法生成的,按道理来说,雪花算法是生成分布式唯一 I...
继续阅读 »

你好,我是悟空。


最近生产环境遇到一个问题:


现象:创建工单、订单等地方,全都创建数据失败。


初步排查:报错信息为duplicate key,意思是保存数据的时候,报主键 id 重复,而这些 id 都是由雪花算法生成的,按道理来说,雪花算法是生成分布式唯一 ID,不应该生成重复的 ID。


大家可以先猜猜是什么原因。


有的同学可能对雪花算法不熟悉,这里做个简单的说明。


一、雪花算法


snowflake(雪花算法):Twitter 开源的分布式 id 生成算法,64 位的 long 型的 id,分为 4 部分:


snowflake 算法



  • 1 bit:不用,统一为 0

  • 41 bits:毫秒时间戳,可以表示 69 年的时间。

  • 10 bits:5 bits 代表机房 id,5 个 bits 代表机器 id。最多代表 32 个机房,每个机房最多代表 32 台机器。

  • 12 bits:同一毫秒内的 id,最多 4096 个不同 id,自增模式


优点:



  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。

  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。

  • 可以根据自身业务特性分配bit位,非常灵活。


缺点:



  • 强依赖机器时钟,如果机器上时钟回拨(可以搜索 2017 年闰秒 7:59:60),会导致发号重复或者服务会处于不可用状态。


看了上面的关于雪花算法的简短介绍,想必大家能猜出个一二了。


雪花算法和时间是强关联的,其中有 41 位是当前时间的时间戳,


二、排查


2.1 雪花算法有什么问题?


既然是雪花算法的问题,那我们就来看下雪花算法出了什么问题:


(1)What:雪花算法生成了重复的 ID,这些 ID 是什么样的?


(2)Why:雪花算法为什么生成了重复的 key


第一个问题,我们可以通过报错信息发现,这个重复的 ID 是 -1,这个就很奇怪了。一般雪花算法生成的唯一 ID 如下所示,我分别用二进制和十进制来表示:


十进制表示:2097167233578045440

二进制表示:0001 1101 0001 1010 1010 0010 0111 1100 1101 1000 0000 0010 0001 0000 0000 0000

找到项目中使用雪花算法的工具类,生成 ID 的时候有个判断逻辑:



当前时间小于上次的生成时间就会返回 -1,所以问题就出在这个逻辑上面。(有的雪花算法是直接抛异常)



if (timestamp < this.lastTimestamp) {
return -1;
}


由于每次 timestamp 都是小于 lastTimeStamp,所以每次都返回了 -1,这也解释了为什么生成了重复的 key。


2.2 时钟回拨或跳跃


那么问题就聚焦在为什么当前时间还会小于上次的生成时间


下面有种场景可能发生这种情况:


首先假定当前的北京时间是 9:00:00。另外上次生成 ID 的时候,服务器获取的时间 lastTimestamp=10:00:00,而现在服务器获取的当前时间 timestamp=09:00:00,这就相当于服务器之前是获取了一个未来时间,现在突然跳跃到当前时间。


而这种场景我们称之为时钟回拨时钟跳跃


时钟回拨:服务器时钟可能会因为各种原因发生不准,而网络中会提供 NTP 服务来做时间校准,因此在做校准的时候,服务器时钟就会发生时钟的跳跃或者回拨问题。


2.3 时钟同步


那么服务器为什么会发生时钟回拨或跳跃呢?



我们猜测是不是服务器上的时钟不同步后,又自动进行同步了,前后时间不一致。



首先我们的每台服务器上都安装了 ntpdate 软件,作为 NTP 客户端,会每隔 10 分钟NTP 时间服务器同步一次时间。


如下图所示,服务器 1 和 服务器 2 部署了应用服务,每隔 10 分钟向时间服务器同步一次时间,来保证服务器 1 和服务器 2 的时间和时间服务器的时间一致。



每隔 10 分钟同步的设置:


*/10 * * * * /usr/sbin/ntpdate <ip>

另外时间服务器会向 NTP Pool同步时间,NTP Pool 正在为世界各地成百上千万的系统提供服务。 它是绝大多数主流Linux发行版和许多网络设备的默认“时间服务器”。(参考ntppool.org)


那问题就是 NTP 同步出了问题??


2.4 时钟不同步


我们到服务器上查看了下时间,确实和时钟服务器不同步,早了几分钟。


当我们执行 NTP 同步的命令后,时钟又同步了,也就是说时间回拨了。


ntpdate  <时钟服务器 IP>

在产生事故之前,我们重启过服务器 1。我们推测服务器重启后,服务器因网络问题没有正常同步。而在下一次定时同步操作到来之前的这个时间段,我们的后端服务已经出现了因 ID 重复导致的大量异常问题。


这个 NTP 时钟回拨的偶发现象并不常见,但时钟回拨确实会带了很多问题,比如润秒 问题也会带来 1s 时间的回拨。


闰秒就是通过给“世界标准时间”加(或减)1秒,让它更接近“太阳时”。例如,两者相差超过0.9秒时,就在23点59分59秒与00点00分00秒之间,插入一个原本不存在的“23点59分60秒”,来将时间调慢一秒钟。


为了预防这种情况的发生,网上也有一些开源解决方案。


三、解决方案


(1)方式一:使用美团 Leaf方案,基于雪花算法。


(2)方式二:使用百度 UidGenerator,基于雪花算法


(3)方式三:用 Redis 生成自增的分布式 ID。弊端是 ID 容易被猜到,有安全风险。


3.1 美团的 Leaf 方案


美团的开源项目 Leaf 的方案:采用依赖 ZooKeeper 的数据存储。如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,Leaf 会等待时钟同步到最后一次主键生成的时间后再继续工作


重点就是需要等待时钟同步!



3.2 百度 UidGenerator 方案


百度UidGenerator方案不在每次获取 ID 时都实时计算分布式 ID,而是利用 RingBuffer 数据结构,通过缓存的方式预生成一批唯一 ID 列表,然后通过 incrementAndGet() 方法获取下一次的时间,从而脱离了对服务器时间的依赖,也就不会有时钟回拨的问题。


重点就是预生成一批 ID!


Github地址:


https://github.com/baidu/uid-generator

四、总结


本篇通过一次偶发的生产事故,引出了雪花算法的原理、雪花算法的不足、对应的开源解决方案。


雪花算法强依赖服务器的时钟,如果时钟产生了回拨,就会造成很多问题。


我们的系统虽然做了 NTP 时钟同步,但也不是 100% 可靠,而且润秒这种场景也是出现过很多次。鉴于此,美团和百度也有对应的解决方案。


最后,我们的生产环境也是第一次遇到因 NTP 导致的时钟回拨,而且系统中用到雪花算法的地方并不多,所以目前并没有采取以上的替换方案。


https://github.com/Jackson0714/PassJava-Platform/blob/master/passjava-common/src/main/java/com/jackson0714/passjava/common/utils/SnowflakeUtilV2.java

参考资料:


time.geekbang.org/dailylesson…


blog.csdn.net/liangcsdn11…


http://www.jianshu.com/p/2911

作者:悟空聊架构
来源:juejin.cn/post/7244339465559408695
10ca6…

收起阅读 »

Spring Boot定时任务详解与案例代码

概述 Spring Boot是一个流行的Java开发框架,它提供了许多便捷的特性来简化开发过程。其中之一就是定时任务的支持,让开发人员可以轻松地在应用程序中执行定时任务。本文将详细介绍如何在Spring Boot中使用定时任务,并提供相关的代码示例。 实际案例...
继续阅读 »

image.png





概述


Spring Boot是一个流行的Java开发框架,它提供了许多便捷的特性来简化开发过程。其中之一就是定时任务的支持,让开发人员可以轻松地在应用程序中执行定时任务。本文将详细介绍如何在Spring Boot中使用定时任务,并提供相关的代码示例。


实际案例


在Spring Boot中,使用定时任务非常简单。首先,需要在应用程序的入口类上添加@EnableScheduling注解,以启用定时任务的支持。该注解将告诉Spring Boot自动配置并创建一个线程池来执行定时任务。


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

一旦启用了定时任务支持,就可以在任何Spring管理的Bean中创建定时任务。可以通过在方法上添加@Scheduled注解来指定定时任务的执行规则。下面是一个简单的示例,演示了每隔一分钟执行一次的定时任务:


import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class MyScheduledTask {

@Scheduled(cron = "0 * * * * *") // 每分钟执行一次
public void executeTask() {
// 在这里编写定时任务的逻辑
System.out.println("定时任务执行中...");
}
}

在上面的示例中,我们创建了一个名为MyScheduledTask的组件,并在其中定义了一个名为executeTask的方法。通过使用@Scheduled(cron = "0 * * * * *")注解,我们指定了该方法应该每分钟执行一次。当定时任务触发时,executeTask方法中的逻辑将被执行。


需要注意的是,@Scheduled注解支持不同的任务触发方式,如基于固定延迟时间、固定间隔时间或cron表达式等。可以根据实际需求选择适合的方式。


以上就是使用Spring Boot进行定时任务的基本示例。通过简单的注解配置,您可以轻松地在应用程序中添加和管理定时任务。希望本文能对您理解和使用Spring Boot定时任务提供帮助。


总结


Spring Boot提供了便捷的方式来实现定时任务。通过添加@EnableScheduling注解来启用定时任务支持,并使用@Scheduled注解来指定任务的执行规则。可以根据需求选择不同的触发方式。


除了上述基本示例外,Spring Boot还提供了更多高级功能和配置选项,以满足更复杂的定时任务需求。



  1. 方法参数和返回值:您可以在定时任务方法中添加参数和返回值,Spring Boot会自动注入合适的值。例如,可以将java.util.Date类型的参数添加到方法中,以获取当前时间。返回值可以是voidjava.util.concurrent.Futurejava.util.concurrent.CompletableFuture等类型。

  2. 并发执行和线程池配置:默认情况下,Spring Boot的定时任务是串行执行的,即每个任务完成后再执行下一个任务。如果需要并发执行任务,可以通过配置线程池来实现。可以在application.propertiesapplication.yml文件中设置相关的线程池属性,如核心线程数、最大线程数和队列容量等。

  3. 异常处理:定时任务可能会抛出异常,因此需要适当处理异常情况。您可以使用@Scheduled注解的exceptionHandler属性来指定异常处理方法,以便在任务执行过程中捕获和处理异常。

  4. 动态调度:有时需要根据运行时的条件来动态调整定时任务的触发时间。Spring Boot提供了TaskScheduler接口和CronTrigger类,您可以使用它们来在运行时动态设置定时任务的执行规则。

  5. 集群环境下的定时任务:如果应用程序部署在多个节点的集群环境中,可能会遇到定时任务重复执行的问题。为了避免这种情况,可以使用分布式锁机制,如Redis锁或数据库锁,来确保只有一个节点执行定时
    作者:百思不得小赵
    来源:juejin.cn/post/7244089396567638072
    任务。

收起阅读 »

区块链中的平行链

随着区块链技术的不断发展,人们对于区块链的应用也越来越广泛。在这个过程中,平行链技术应运而生。那么,什么是平行链呢? 1. 平行链的定义和概念 平行链是一种特定的应用程序数据结构,它是全局一致的,由 Polkadot 中继链的验证节点进行验证] 。它允许交易...
继续阅读 »

随着区块链技术的不断发展,人们对于区块链的应用也越来越广泛。在这个过程中,平行链技术应运而生。那么,什么是平行链呢?


1. 平行链的定义和概念


平行链是一种特定的应用程序数据结构,它是全局一致的,由 Polkadot 中继链的验证节点进行验证] 。它允许交易在专用的 Layer1 区块链生态系统中并行分布和处理,从而显著提高了吞吐量和可扩展性


平行链技术来自于波卡项目。Polkadot 是一个可伸缩的异构多链系统,其本身不提供任何内在的功能应用,主要是连接各个区块链协议,并维护协议通讯有效安全,并保存这些通讯信息。平行链可以作为公共或私有网络运行,可以用于企业或组织运行,可以作为平台运行,让其他人在他们的基础上构建应用程序或作为公共利益平行链,来造福整个 Polkadot 生态系统,以及各种各样的其他模型 。


简单来说,平行链是一种依赖于波卡中继链提供安全性并与其他中继链通信的自主运行的原生区块链。它是一种简单、易扩展的区块链,它的安全性由“主链”提供。


2. 平行链与主链的关系


平行链是一种独立的区块链,它与主链(mainchain)通过双向桥接相连。它允许代币或其他数字资产在主链和平行链之间进行转移。每个平行链都是一个独立的区块链网络,拥有自己的代币、协议、共识机制和安全性


双向桥(two-way bridge)是一种连接两个区块链的技术,它允许资产在两个区块链之间进行转移。有单向(unidirectional)桥和双向(bidirectional)桥两种类型。单向桥意味着用户只能将资产桥接到一个目标区块链,但不能返回其原生区块链。双向桥允许资产在两个方向上进行桥接


平行链和主链保持既独立又连结的关系,在主链之下,它可以拥有自己的超级节点、状态机和原始交易数据。主链可以给平行链做跨链操作,从而形成链条生态系统。


3. 平行链的优势


平行链不仅可以利用系统的共识保证安全,还可以共享原有的生态。它们执行的计算本质上是独立,但又连结在一起。平行链之间有明确的隔离分界线,可以立即执行所有交易,而不用担心和其他链产生冲突。


使用平行链的原因是它能够显著提高吞吐量和可扩展性,并且能够支持多种不同的应用场景。


平行链可以用来运行区块链应用程序,如去中心化应用程序(dapps),并将计算负载从主链上移除,从而帮助扩展区块链。它们还可以与其他扩展解决方案结合使用。尽管平行链看起来是一个有前途的解决方案,但它们增加了区块链设计的复杂性,并且需要大量的努力和投资进行初始设置。由于平行链是独立的区块链,它们的安全性可能会受到损害,因为它们不受主链的保护。另一方面,如果一个平行链被攻击,它不会影响主链,因此它们可以用来试验新的协议和对主链的改进


4. 平行 链的关键特征


平行 链具有许多关键特征。例如,它们可以多条并行处理交易,效率可提升十倍。此外,由于只需要下载平行 链相关的数据,因此相对效率更高,速度更快。这些平行 链开枝散叶,可以打造自己独有的生态系。


5. 平行链的应用实例


目前市场上已经出现了许多基于平行 链技术开发 的项目。例如 Crust Network 是一个激励去中心化云服务 的应用型公 链。其通过对平行 链技术 的应用 ,既完整地保存了项目 的自主发展空间 ,也可更好地拓展项目功能 ,为用户提供更好地使用体验。


6. 平行链的经济政策


针对目前公 链技术自主权较弱 的问题 ,Polkadot 平 行 链 上 的社 区根据自己 的意志治理他们 的网络 ,不受波卡网络管理 的限制 ,拥有绝对 的自主权 。通过分片协议连接 的区块 链网络 ,可较好地实现拓展定制 。而基于 Polkadot 技术应用开发 的 Crust Network 是一个激励去中心化云服务 的应用型公 链。其通过对平行 链技术 的应用 ,既完整地保存了项目 的自主发展空间 ,也可更好地拓展项目功能 ,为用户提供更好地使用体验。from刘金,转载请

作者:Pomelo_刘金
来源:juejin.cn/post/7244174365172187193
注明原文链接。感谢!

收起阅读 »

你以为搞个流水线每天跑,团队就在使用CI/CD实践了?

在实践中,很多团队对于DevOps 流水线没有很透彻的理解,要不就创建一大堆流水线,要不就一个流水线通吃。实际上,流水线的设计和写代码一样,需要基于“业务场景”进行一定的设计编排,特别是很多通过“开源工具”搭建的流水线,更需要如此(商业的一体化平台大部分已经把...
继续阅读 »

在实践中,很多团队对于DevOps 流水线没有很透彻的理解,要不就创建一大堆流水线,要不就一个流水线通吃。实际上,流水线的设计和写代码一样,需要基于“业务场景”进行一定的设计编排,特别是很多通过“开源工具”搭建的流水线,更需要如此(商业的一体化平台大部分已经把设计思想融入自己产品里了)。



  • 流水线的设计与分支策略有关

  • 流水线的设计与研发活动有关


清晰的代码结构,标准的环境配置,原子化的流水线任务编排,再加上团队的协作纪律,和持续优化的动作,才是真正的践行CI/CD实践


流水线设计原则


1. 确定好变量



  • 哪些是构建/部署需要变化的,比如构建参数,代码地址,分支名称,安装版本,部署机器IP等,控制变化的,保证任务的可复制性,不要写很多hardcode进去


2. 流水线变量/命名的规范化



  • 标准化的命名,有助于快速复制;有意义的流水线命名,有助于团队新成员快速了解


3. 一次构建,多次部署



  • 一次构建,多次部署(多套环境配置+多套构建版本标签);杜绝相同代码重复打包

  • 相似技术栈/产品形态具备共性,通过以上原则可以抽取复用脚本,良好的设计有助于后续的可维护性!


4. 步骤标准化/原子化



  • 比如docker build/push, helm build/deploy, Maven构建等动作标准化,避免重复性写各种脚本逻辑

  • 根据业务场景组装,例如. 提测场景,每日构建场景,回归测试场景


image.png
5. 快速失败



  • 尽可能把不稳定的,耗时短的步骤 放在流水线的最前面,如果把一个稳定的步骤放在前面,并且耗时几十分钟,后面的某个步骤挂了,反馈周期就会变长


从零开始设计流水线


流水线分步骤实施, 从 “点” 到 “线” 结合业务需要串起来,适合自己团队协作开发节奏的流水线才是最好的。



  1. 价值流进行建模并创建简单的可工作流程

  2. 将 构建 和 部署 流程自动化

  3. 将 单元测试和 代码分析 自动化

  4. 将 验收测试 自动化

  5. 将 发布 自动化


image.png


流水线的分层


由于产品本身的形态不同,负责研发的团队人员组成不同,代码的版本管理分支策略不同,使用的部署流水线形式也会各不相同,所以基于实际业务场景设计流水线是团队工程实践成熟的重要标志



1. 提交构建流水线(个人级)


适用场景:每名研发工程师都创建了自己专属的流水线(一般对应个人的开发分支),用于个人在未推送代码到团队仓库之前的快速质量反馈。
注意:个人流水线并不会部署到 团队共同拥有的环境中,而是仅覆盖个人开发环节。如图所示,虚线步骤非必选

image.png


2. 集成验收流水线(团队级)


适用场景:每个团队都根据代码仓库(master/release/trunk)分支,创建产品专属的流水线,部署到 团队共同拥有的环境中e.g. dev)。
注意:如图所示,虚线步骤非必选,根据情况可通过 启动参数true/flase 跳过执行,自动化测试仅限于保证基本功能的用例。

image.png


3. 部署测试流水线(团队级)


适用场景:每个团队的测试工程师都需要专门针对提测版本的自动化部署/测试流水线,部署到团队共同拥有的环境中(e.g. test).
注意:如图所示,该条流水线的起点不是代码,而是提测的特定版本安装包;虚线步骤非必选,根据情况可通过 启动参数true/flase 跳过执行 或 裁剪。

image.png


4. 多组件集成流水线


适用场景:如果一个产品由多个组件构建而成,每个组件均有独自的代码仓库,并且每个组件由一个单独的团队负责开发与维护,那么,整个产品 的部署流水线的设计通常如下图所示。 集成部署流水线的集成打包阶段将自动从企业软件包库中获取每个组件最近成功的软件包,对其进行产品集成打包
image.png


5. 单功能流水线


适用场景:适用于和代码变更无关的场景,不存在上面步骤复杂的编排 (也可通过上述流水线的 启动参数进行条件控制,跳过一些步骤)



  • 针对某个环境的漏洞扫描

  • 针对某个已部署环境的自动化测试

  • 定时清理任务

  • ...


6. 全功能(持续交付)流水线


适用场景:需求、代码构建、测试、部署环境内嵌自动化能力,每次提交都触发完整流水线,中间通过人工审批层次卡点,从dev环境,test环境,stage环境一直到 prod环境。 常适用于快速发布的 PASS/SASS服务,对团队各项能力和流程制度要求较高,支持快速发布(策略)和快速回滚(策略)
image.png


流水线运转全景图


团队研发工程师每人每天都会提交一次。因此,流水线每天都会启动多次。当然并不是每次提交的变更都会走到最后的“上传发布” 。 也不是每次提交都会走到UAT 部署,因为开发人员并不是完成一个功能需求后才提交代码,而是只要做完一个开发任务,就可以提交。每个功能可能由 多个开发任务组成,研发工程师需要确保即使提交了功能尚未开发完成的代码,也不会影响已开发完成的那些功能。
制品经过一个个质量卡点,经历各种门禁验证,最终交付给客户 可以工作的软件
pipeline-status.jpg

收起阅读 »

接口耗时2000多秒!我人麻了!

接口耗时2000多秒!我人麻了! 前几天早上,有个push服务不断报警,报了很多次,每次都得运维同学重启服务来维持,因为这个是我负责的,所以我顿时紧张了起来,匆忙来到公司,早饭也不吃了,赶紧排查! 1、 现象与排查步骤: 下面是下午时候几次告警的截图: ...
继续阅读 »

接口耗时2000多秒!我人麻了!



  • 前几天早上,有个push服务不断报警,报了很多次,每次都得运维同学重启服务来维持,因为这个是我负责的,所以我顿时紧张了起来,匆忙来到公司,早饭也不吃了,赶紧排查!


1、 现象与排查步骤:


下面是下午时候几次告警的截图:



  • 来看下图。。。。接口超时 2000多秒。。。。我的心碎了!!!人也麻了!!!脑瓜子嗡嗡的。。。



  • image.png



  • 另外还总是报pod不健康、不可用 这些比较严重的警告!



  • image.png


我的第一反应是调用方有群发操作,然后看了下接口的qps 貌似也不高呀!也就 9req/s,
之后我去 grafana 监控平台 观察jvm信息发现,线程数量一直往上涨,而且线程状态是 WAITING 的也是一直涨。


如下是某一个pod的监控:


image.png
image.png


为了观察到底是哪个线程状态一直在涨,我们点进去看下详情:


image.png


上图可以看到 该pod的线程状态图 6种线程状态全列出来了, 分别用不同颜色的线代表。而最高那个同时也是14点以后不断递增那个是蓝线 代表的是 WAITING 状态下的线程数量。


通过上图现象,我大概知道了,肯定有线程是一直在wait无限wait下去,后来我找运维同学 dump了线程文件,分析一波,来看看到底是哪个地方使线程进入了wait !!!


如下 是dump下来的线程文件,可以看到搜索出427个WAITING这也基本和 grafana 监控中状态是WAITTING的线程数量一致


image.png


重点来了(这个 WAITING 状态的堆栈信息,还都是从 IOSPushStrategy#pushMsgWithIOS 这个方法的某个地方出来的(151行出来的)),于是我们找到代码来看看,是哪个小鬼在作怪?



image.png
而类 PushNotificationFuture 继承了 CompletableFuture,他自己又没有get方法,所以本质上 就是调用的 CompletableFuture的 get 方法。
image.png
ps:提一嘴,我们这里的场景是 等待ios push服务器的结果,不知道啥情况,今天(指发生故障的那天)ios push服务器(域名: api.push.apple.com )一直没返回,所以就导致一直等待下去了。。



看到这, 我 豁然开朗 && 真相大白 了,原来是在使用 CompletableFutureget时候,没设置超时时间,这样的话会导致一直在等结果。。。(但代码不是我写的,因为我知道 CompletableFuture 的get不设置参数会一直等下去 ,我只是维护,后期也没怎么修改这块的逻辑,哎 ,说多了都是泪呀!)


好一个 CompletableFuture#get();


(真是 死等啊。。。一点不含糊的等待下去,等的天荒地老海枯石烂也要等下去~~~ )


到此,问题的原因找到了。


2、 修复问题


解决办法很简单,给CompletableFuture的get操作 加上超时时间即可,如下代码即可修复:
image.png


在修复后,截止到今天(6月8号)没有这种报警情况了,而且线程数和WAITING线程都比较稳定,都在正常范围内,如下截图(一共4个pod):
image.png


至此问题解决了~~~ 终于可以睡个好觉啦!


3、 复盘总结


3.1、 代码浅析


既然此次的罪魁祸首是 CompletableFuture的get方法 那么我们就浅析一下 :



  1. 首先看下 get(); 方法
    image.png
    image.png


上边可以看到
不带参数的get方法: if(deadLine==0) 成立 ,也就是最终调用了LockSupport的park(this);方法,而这个方法最终调了unsafe的这个方法-> unsafe.park(false, 0L); 其中第二个参数就是等待多长时间后,unpark即唤醒被挂起的线程,而0 则代表无限期等待。



  1. 再来看下 get(long timeOut,TimeUnit unit);方法
    image.png
    我们可以看到
    带参数的get方法: if(deadLine==0) 不成立,走的else逻辑 也就是最终调用了LockSupportparkNanos(this,nanos);方法,而这个方法最终调了unsafe的这个方法-> unsafe.park(false, nanos); 其中第二个参数就是你调用get时,传入的tiimeOut参数(只不过底层转成纳秒了)


    我们跑了下程序,发现超过指定时间后,get(long timeOut,TimeUnit unit); 方法抛出 TimeoutException异常,而至于超时后我们开发者怎么处理,就在于具体情况了。
    Exception in thread "main" java.util.concurrent.TimeoutException
    at java.base/java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1886)
    at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2021)



而在 我的另一篇文章 万字长文分析synchroized 这篇文章中,我们其实深入过openjdk的源码,见识过parkunpark操作,我们截个图回忆一下:


image.png


3.2、最后总结:


1.在调用外部接口时。一定要注意设置超时时间,防止三方服务出问题后影响自身服务。


2.以后无论看到什么类型的Future,都要谨慎,因为这玩意说的是异步,但是调用get方法时,他本质上是同步等待,所以必须给他设置个超时时间,否则他啥时候能返回结果,就得看天意了!


3.凡是和第三方对接的东西,都要做最坏的打算,快速失败的方式很有必要。


4.遇到天大的问题,都尽可能保持冷静,不要乱了阵脚!
作者:蝎子莱莱爱打怪
来源:juejin.cn/post/7242237897993814075
strong>

收起阅读 »

flutter 刷新、加载与占位图一站式服务(基于easy_refresh扩展)

前文 今天聊到的是滚动视图的 刷新与加载,对于这个老生常谈的问题解决方案就太多了,优秀的插件也屡见不鲜,譬如 pull_to_refresh、 easy_refresh、还有笔者前段时间使用过的 infinite_scroll_pagination 这些都是...
继续阅读 »

前文


今天聊到的是滚动视图刷新与加载,对于这个老生常谈的问题解决方案就太多了,优秀的插件也屡见不鲜,譬如 pull_to_refresheasy_refresh、还有笔者前段时间使用过的 infinite_scroll_pagination 这些都是极具代表性的优秀插件。
那你在使用时有没有类似情况:



  • 为了重复的样版式代码感到厌倦?

  • 为了Ctrl V+C感到无聊?

  • 看到通篇类似的代码想大刀阔斧的整改?

  • 更有甚者有没有本来只想自定义列表样式,却反而浪费更多的时间来完成基础配置?


现在我们来解决这类问题,欢迎来到走近科学探索发现 - Approaching Scientific Exploration and Discovery。(可能片场走错了:) )


注意



  • 本文以 easy_refresh 作为刷新框架举例说明(其他框架的类似)

  • 本文案例demo以 getx 作为项目状态管理框架(与刷新无关仅作为项目基础框架构成,不喜勿喷)


正文


现在请出我们重磅成员mixin,对于这个相信大家已经非常熟悉了。我们要做的就是利用easy_refresh提供的refresh controller对视图逻辑进行拆分,从而精简我们的样板式代码。


首页拆离我们刷新和加载方法:


import 'dart:async';
import 'package:easy_refresh/easy_refresh.dart';
import 'state.dart';

mixin PagingMixin<T> {
/// 刷新控制器
final EasyRefreshController _pagingController = EasyRefreshController(
controlFinishLoad: true, controlFinishRefresh: true);
EasyRefreshController get pagingController => _pagingController;

/// 初始页码 <---- 单独提出这个的原因是有时候我们请求的起始页码不固定,有可能是0,也有可能是1
int _initPage = 0;

/// 当前页码
int _page = 0;
int get page => _page;

/// 列表数据
List<T> get items => _state.items;
int get itemCount => items.length;

/// 错误信息
dynamic get error => _state.error;

/// 关联刷新状态管理的控制器 <---- 自定义状态类型,后文会有阐述主要包含列表数据、初始加载是否为空、错误信息
PagingMixinController get state => _state;
final PagingMixinController<T> _state = PagingMixinController(
PagingMixinData(items: []),
);

/// 是否加载更多 <---- 可以在控制器中初始化传入,控制是否可以进行加载
bool _isLoadMore = true;
bool get isLoadMore => _isLoadMore;

/// 控制刷新结束回调(异步处理) <---- 手动结束异步操作,并返回结果
Completer? _refreshComplater;

/// 挂载分页器
/// `controller` 关联刷新状态管理的控制器
/// `initPage` 初始页码值(分页起始页)
/// `isLoadMore` 是否加载更多
void initPaging({
int initPage = 0,
isLoadMore = true,
}) {
_isLoadMore = isLoadMore;
_initPage = initPage;
_page = initPage;
}

/// 获取数据
FutureOr fetchData(int page);

/// 刷新数据
Future onRefresh() async {
_refreshComplater = Completer();
_page = _initPage;
fetchData(_page);
return _refreshComplater!.future;
}

/// 加载更多数据
Future onLoad() async {
_refreshComplater = Completer();
_page++;
fetchData(_page);
return _refreshComplater!.future;
}

/// 获取数据后调用
/// `items` 列表数据
/// `maxCount` 数据总数,如果为0则默认通过 `items` 有无数据判断是否可以分页加载, null为非分页请求
/// `error` 错误信息
/// `limit` 单页显示数量限制,如果items.length < limit 则没有更多数据
void endLoad(
List<T>? list, {
int? maxCount,
// int limit = 5,
dynamic error,
}) {
if (_page == _initPage) {
_refreshComplater?.complete();
_refreshComplater = null;
}

final dataList = List.of(_state.value.items);
if (list != null) {
if (_page == _initPage) {
dataList.clear();
// 更新数据
_pagingController.finishRefresh();
_pagingController.resetFooter();
}
dataList.addAll(list);
// 更新列表
_state.value = _state.value.copyWith(
items: dataList,
isStartEmpty: page == _initPage && list.isEmpty,
);

// 默认没有总数量 `maxCount`,用获取当前数据列表是否有值判断
// 默认有总数量 `maxCount`, 则判断当前请求数据list+历史数据items是否小于总数
// bool hasNoMore = !((items.length + list.length) < maxCount);
bool isNoMore = true;
if (maxCount != null) {
isNoMore = page > 1; // itemCount >= maxCount;
}
var state = IndicatorResult.success;
if (isNoMore) {
state = IndicatorResult.noMore;
}
_pagingController.finishLoad(state);
} else {
_state.value = _state.value.copyWith(items: [], error: error ?? '数据请求错误');
}
}

}


创建PagingMixin<T>混入类型,泛型<T>属于列表子项的数据类型


void initPaging(...):初始化的时候可以写入基本设置(可以不调用)


Future onRefresh() Future onLoad():供外部调用的刷新加载方法


FutureOr fetchData(int page):由子类集成重写,主要是完成数据获取方法,在获取到数据后,需要调用方法void endLoad(...)来结束整个请求操作,通知视图刷新


PagingMixinController继承自ValueNotifier,是对数据相关状态的缓存,便于独立逻辑操作与数据状态:


class PagingMixinController<T> extends ValueNotifier<PagingMixinData<T>> {
PagingMixinController(super.value);

dynamic get error => value.error;
List<T> get items => value.items;
int get itemCount => items.length;
}
// flutter 关于easy_refresh更便利的打开方式
class PagingMixinData<T> {
// 列表数据
final List<T> items;

/// 错误信息
final dynamic error;

/// 首次加载是否为空
bool isStartEmpty;

PagingMixinData({
required this.items,
this.error,
this.isStartEmpty = false,
});

....

}

完成这两个类的编写,我们对于逻辑部分的拆离已经完成了。


下面是对easy_refresh的使用,封装:


class PullRefreshControl extends StatelessWidget {
const PullRefreshControl({
super.key,
required this.pagingMixin,
required this.childBuilder,
this.header,
this.footer,
this.locatorMode = false,
this.refreshOnStart = true,
this.startRefreshHeader,
});

final Header? header;
final Footer? footer;

final bool refreshOnStart;
final Header? startRefreshHeader;

/// 列表视图
final ERChildBuilder childBuilder;

/// 分页控制器
final PagingMixin pagingMixin;

/// 是否固定刷新偏移
final bool locatorMode;

@override
Widget build(BuildContext context) {
final firstRefreshHeader = startRefreshHeader ??
BuilderHeader(
triggerOffset: 70,
clamping: true,
position: IndicatorPosition.above,
processedDuration: Duration.zero,
builder: (ctx, state) {
if (state.mode == IndicatorMode.inactive ||
state.mode == IndicatorMode.done) {
return const SizedBox();
}
return Container(
padding: const EdgeInsets.only(bottom: 100),
width: double.infinity,
height: state.viewportDimension,
alignment: Alignment.center,
child: SpinKitFadingCube(
size: 25,
color: Theme.of(context).primaryColor,
),
);
},
);

return EasyRefresh.builder(
controller: pagingMixin.pagingController,
header: header ??
RefreshHeader(
clamping: locatorMode,
position: locatorMode
? IndicatorPosition.locator
: IndicatorPosition.above,
),
footer: footer ?? const ClassicFooter(),
refreshOnStart: refreshOnStart,
refreshOnStartHeader: firstRefreshHeader,
onRefresh: pagingMixin.onRefresh,
onLoad: pagingMixin.isLoadMore ? pagingMixin.onLoad : null,
childBuilder: (context, physics) {
return ValueListenableBuilder(
valueListenable: pagingMixin.state,
builder: (context, value, child) {
if (value.isStartEmpty) {
return _PagingStateView(
isEmpty: value.isStartEmpty,
onLoading: pagingMixin.onRefresh,
);
}
return childBuilder.call(context, physics);
},
);
},
);
}
}

创建PullRefreshControl类型,设置必须属性pagingMixinchildBuilder,前者是我们创建的PagingMixin对象(可以是任何类型,只要支持混入就可以了),后者是对我们滚动列表的实现。 其他的都是对 easy_refresh的属性配置,参考相关文档就行了。


到这里我们减配版的封装就完成了,使用方式如下:


截图


截图


但是我们并没有完成我们前文所说的简化操作,还是需要一遍又一遍创建重复的滚动列表,所以我们继续:


/// 快速构建 `ListView` 形式的分页列表
/// 其他详细参数查看 [ListView]
class SpeedyPagedList<T> extends StatelessWidget {
const SpeedyPagedList({
super.key,
required this.controller,
required this.itemBuilder,
this.scrollController,
this.padding,
this.header,
this.footer,
this.locatorMode = false,
this.refreshOnStart = true,
this.startRefreshHeader,
double? itemExtent,
}) : _separatorBuilder = null,
_itemExtent = itemExtent;

const SpeedyPagedList.separator({
super.key,
required this.controller,
required this.itemBuilder,
required IndexedWidgetBuilder separatorBuilder,
this.scrollController,
this.padding,
this.header,
this.footer,
this.locatorMode = false,
this.refreshOnStart = true,
this.startRefreshHeader,
}) : _separatorBuilder = separatorBuilder,
_itemExtent = null;

final PagingMixin<T> controller;

final Widget Function(BuildContext context, int index, T item) itemBuilder;

final Header? header;
final Footer? footer;

final bool refreshOnStart;
final Header? startRefreshHeader;
final bool locatorMode;

/// 参照 [ScrollView.controller].
final ScrollController? scrollController;

/// 参照 [ListView.itemExtent].
final EdgeInsetsGeometry? padding;

/// 参照 [ListView.separator].
final IndexedWidgetBuilder? _separatorBuilder;

/// 参照 [ListView.itemExtent].
final double? _itemExtent;

@override
Widget build(BuildContext context) {
return PullRefreshControl(
pagingMixin: controller,
header: header,
footer: footer,
refreshOnStart: refreshOnStart,
startRefreshHeader: startRefreshHeader,
locatorMode: locatorMode,
childBuilder: (context, physics) {
return _separatorBuilder != null
? ListView.separated(
physics: physics,
padding: padding,
controller: scrollController,
itemCount: controller.itemCount,
itemBuilder: (context, index) {
final item = controller.items[index];
return itemBuilder.call(context, index, item);
},
separatorBuilder: _separatorBuilder!,
)
: ListView.builder(
physics: physics,
padding: padding,
controller: scrollController,
itemExtent: _itemExtent,
itemCount: controller.itemCount,
itemBuilder: (context, index) {
final item = controller.items[index];
return itemBuilder.call(context, index, item);
},
);
},
);
}
}

...


归纳我们所需要的使用方式(我这里只写了ListView/GridView),构创建快速初始化加载列表的方法,将我们仅需要的Widget Function(BuildContext context, int index, T item) itemBuilder单个元素的创建(因为对于大多列表来说我们仅关心单个元素样式)暴露出来,简化PullRefreshControl的使用。


截图


对比前面的使用方式,现在更加简洁了,总计代码也就十几行吧。


到这里就结束啦,文章也仅算是对繁杂重复使用的东西进行一些归纳总结,没有特别推崇的意思,更优秀的方案也比比皆是,所以仁者见仁了各位。


附GIF展示:


GIF 2023-6-9 14-23-45.gif


附Demo地址: boomcx/templat

e_getx

收起阅读 »

mybatis拦截器实现数据权限

前端的菜单和按钮权限都可以通过配置来实现,但很多时候,后台查询数据库数据的权限需要通过手动添加SQL来实现。 比如员工打卡记录表,有id,name,dpt_id,company_id等字段,后两个表示部门ID和分公司ID。 查看员工打卡记录SQL为:selec...
继续阅读 »

前端的菜单和按钮权限都可以通过配置来实现,但很多时候,后台查询数据库数据的权限需要通过手动添加SQL来实现。

比如员工打卡记录表,有id,name,dpt_id,company_id等字段,后两个表示部门ID和分公司ID。

查看员工打卡记录SQL为:select id,name,dpt_id,company_id from t_record


当一个总部账号可以查看全部数据此时,sql无需改变。因为他可以看到全部数据。

当一个部门管理员权限员工查看全部数据时,sql需要在末属添加 where dpt_id = #{dpt_id}


如果每个功能模块都需要手动写代码去拿到当前登陆用户的所属部门,然后手动添加where条件,就显得非常的繁琐。

因此,可以通过mybatis的拦截器拿到查询sql语句,再自动改写sql。


mybatis 拦截器


MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:



  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • ParameterHandler (getParameterObject, setParameters)

  • ResultSetHandler (handleResultSets, handleOutputParameters)

  • StatementHandler (prepare, parameterize, batch, update, query)


这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。


通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。


分页插件pagehelper就是一个典型的通过拦截器去改写SQL的。



可以看到它通过注解 @Intercepts 和签名 @Signature 来实现,拦截Executor执行器,拦截所有的query查询类方法。

我们可以据此也实现自己的拦截器。



import com.skycomm.common.util.user.Cpip2UserDeptVo;
import com.skycomm.common.util.user.Cpip2UserDeptVoUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

@Component
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
@Slf4j
public class MySqlInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = statement.getBoundSql(parameter);
String originalSql = boundSql.getSql();
Object parameterObject = boundSql.getParameterObject();

SqlLimit sqlLimit = isLimit(statement);
if (sqlLimit == null) {
return invocation.proceed();
}

RequestAttributes req = RequestContextHolder.getRequestAttributes();
if (req == null) {
return invocation.proceed();
}

//处理request
HttpServletRequest request = ((ServletRequestAttributes) req).getRequest();
Cpip2UserDeptVo userVo = Cpip2UserDeptVoUtil.getUserDeptInfo(request);
String depId = userVo.getDeptId();

String sql = addTenantCondition(originalSql, depId, sqlLimit.alis());
log.info("原SQL:{}, 数据权限替换后的SQL:{}", originalSql, sql);
BoundSql newBoundSql = new BoundSql(statement.getConfiguration(), sql, boundSql.getParameterMappings(), parameterObject);
MappedStatement newStatement = copyFromMappedStatement(statement, new BoundSqlSqlSource(newBoundSql));
invocation.getArgs()[0] = newStatement;
return invocation.proceed();
}

/**
* 重新拼接SQL
*/
private String addTenantCondition(String originalSql, String depId, String alias) {
String field = "dpt_id";
if(StringUtils.isBlank(alias)){
field = alias + "." + field;
}

StringBuilder sb = new StringBuilder(originalSql);
int index = sb.indexOf("where");
if (index < 0) {
sb.append(" where ") .append(field).append(" = ").append(depId);
} else {
sb.insert(index + 5, "
" + field +" = " + depId + " and ");
}
return sb.toString();
}

private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.cache(ms.getCache());
builder.useCache(ms.isUseCache());
return builder.build();
}


/**
* 通过注解判断是否需要限制数据
* @return
*/
private SqlLimit isLimit(MappedStatement mappedStatement) {
SqlLimit sqlLimit = null;
try {
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("
."));
String methodName = id.substring(id.lastIndexOf("
.") + 1, id.length());
final Class<?> cls = Class.forName(className);
final Method[] method = cls.getMethods();
for (Method me : method) {
if (me.getName().equals(methodName) && me.isAnnotationPresent(SqlLimit.class)) {
sqlLimit = me.getAnnotation(SqlLimit.class);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return sqlLimit;
}


public static class BoundSqlSqlSource implements SqlSource {

private final BoundSql boundSql;

public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}

@Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
}


顺便加了个注解 @SqlLimit,在mapper方法上加了此注解才进行数据权限过滤。

同时注解有两个属性,


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface SqlLimit {
/**
* sql表别名
* @return
*/

String alis() default "";

/**
* 通过此列名进行限制
* @return
*/

String columnName() default "";
}

columnName表示通过此列名进行限制,一般来说一个系统,各表当中的此列是统一的,可以忽略。


alis用于标注sql表别名,如 针对sql select * from tablea as a left join tableb as b on a.id = b.id 进行改写,如果不知道表别名,会直接在后面拼接 where dpt_id = #{dptId},

那此SQL就会错误的,通过别名 @SqlLimit(alis = "a") 就可以知道需要拼接的是 where a.dpt_id = #{dptId}


执行结果


原SQL:select * from person, 数据权限替换后的SQL:select * from person where dpt_id = 234

原SQL:select * from person where id > 1, 数据权限替换后的SQL:select * from person where dpt_id = 234 and id > 1


但是在使用PageHelper进行分页的时候还是有问题。



可以看到先执行了_COUNT方法也就是PageHelper,再执行了自定义的拦截器。


在我们的业务方法中注入SqlSessionFactory


@Autowired
@Lazy
private List<SqlSessionFactory> sqlSessionFactoryList;


PageInterceptor为1,自定义拦截器为0,跟order相反,PageInterceptor优先级更高,所以越先执行。




mybatis拦截器优先级




@Order




通过@Order控制PageInterceptor和MySqlInterceptor可行吗?



将MySqlInterceptor的加载优先级调到最高,但测试证明依然不行。


定义3个类


@Component
@Order(2)
public class OrderTest1 {

@PostConstruct
public void init(){
System.out.println(" 00000 init");
}
}

@Component
@Order(1)
public class OrderTest2 {

@PostConstruct
public void init(){
System.out.println(" 00001 init");
}
}

@Component
@Order(0)
public class OrderTest3 {

@PostConstruct
public void init(){
System.out.println(" 00002 init");
}
}

OrderTest1,OrderTest2,OrderTest3的优先级从低到高。

顺序预期的执行顺序应该是相反的:


00002 init
00001 init
00000 init

但事实上执行的顺序是


00000 init
00001 init
00002 init

@Order 不控制实例化顺序,只控制执行顺序。
@Order 只跟特定一些注解生效 如:@Compent @Service @Aspect … 不生效的如: @WebFilter


所以这里达不到预期效果。


@Priority 类似,同样不行。




@DependsOn




使用此注解将当前类将在依赖类实例化之后再执行实例化。


在MySqlInterceptor上标记@DependsOn("queryInterceptor")



启动报错,

这个时候queryInterceptor还没有实例化对象。




@PostConstruct




@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。

在同一个类里,执行顺序为顺序如下:Constructor > @Autowired > @PostConstruct。


但它也不能保证不同类的执行顺序。


PageHelper的springboot start也是通过这个来初始化拦截器的。





ApplicationRunner




在当前springboot容器加载完成后执行,那么这个时候pagehelper的拦截器已经加入,在这个时候加入自定义拦截器,就能达到我们想要的效果。

仿照PageHelper来写


@Component
public class InterceptRunner implements ApplicationRunner {

@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;

@Override
public void run(ApplicationArguments args) throws Exception {
MySqlInterceptor mybatisInterceptor = new MySqlInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
configuration.addInterceptor(mybatisInterceptor);
}
}
}

再执行,可以看到自定义拦截器在拦截器链当中下标变为了1(优先级与order刚好相反)



后台打印结果,达到了预期效果。


收起阅读 »

单例模式

单例设计模式 单例设计模式是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。 好处: 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。 由于new操作的次数减少,因而对...
继续阅读 »

单例设计模式


单例设计模式是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。


好处:



  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。

  • 由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。


单例模式的六种写法:


一、饿汉单例设计模式


步骤:



  1. 私有化构造函数。

  2. 声明本类的引用类型变量,并且使用该变量指向本类对象。

  3. 提供一个公共静态的方法获取本类的对象。


//饿汉单例设计模式 ----> 保证Single在在内存中只有一个对象。
public class HungrySingleton {
//声明本类的引用类型变量,并且使用该变量指向本类对象
private static final HungrySingleton instance = new HungrySingleton();
//私有化构造函数
private HungrySingleton(){
System.out.println("instance is created");
}
//提供一个公共静态的方法获取本类的对象
public static HungrySingleton getInstance(){
return instance;
}
}


不足:无法对instance实例做延迟加载


优化:懒汉


二、懒汉单例设计模式



  1. 私有化构造函数。

  2. 声明本类的引用类型变量,但是不要创建对象。

  3. 提供公共静态的方法获取本类的对象,获取之前先判断是否已经创建了本类对象,如果已经创建了,那么直接返回对象即可,如果还没有创建,那么先创建本类的对象,然后再返回。


//懒汉单例设计模式 ----> 保证Single在在内存中只有一个对象。
public class LazySingleton {
//声明本类的引用类型变量,不创建本类的对象
private static LazySingleton instance;
//私有化构造函数
private LazySingleton(){

}
public static LazySingleton getInstance(){
//第一次调用的时候会被初始化
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}


不足:在多线程的情况下,无法保证内存中只有一个实例


public class MyThread extends Thread{

@Override
public void run() {
System.out.println(LazySingleton.getInstance().hashCode());
}

public static void main(String[] args) {
MyThread[] myThread = new MyThread[10];
for(int i=0;i<myThread.length;i++){
myThread[i] = new MyThread();
}
for(int j=0;j<myThread.length;j++){
myThread[j].start();
}
}
}


打印结果:


257688302
1983483740
1983483740
1983483740
1983483740
1983483740
1983483740
1388138972
1983483740
257688302

在多线程并发下这样的实现无法保证实例是唯一的。


优化:懒汉线程安全


三、懒汉线程安全


通过使用同步函数或者同步代码块保证


public class LazySafetySingleton {

private static LazySafetySingleton instance;
private LazySafetySingleton(){

}
//方法中声明synchronized关键字
public static synchronized LazySafetySingleton getInstance(){
if(instance == null){
instance = new LazySafetySingleton();
}
return instance;
}

//同步代码块实现
public static LazySafetySingleton getInstance1(){
synchronized (LazySafetySingleton.class) {
if(instance == null){
instance = new LazySafetySingleton();
}
}
return instance;
}
}

不足:使用synchronized导致性能缺陷


优化:DCL


四、DCL


DCL:double check lock (双重检查锁机制)


public class DclSingleton {

private static DclSingleton instance = null;

private DclSingleton(){

}
public static DclSingleton getInstance(){
//避免不必要的同步
if(instance == null){
//同步
synchronized (DclSingleton.class) {
//在第一次调用时初始化
if(instance == null){
instance = new DclSingleton();
}
}
}
return instance;
}

}


不足:在if判断中执行的instance = new DclSingleton(),该操作不是一个原子操作,JVM首先会按照逻辑,第一步给instance分配内存;第二部,调用DclSingleton()构造方法初始化变量;第三步将instance对象指向JVM分配的内存空间;JVM的缺点:在即时编译器中,存在指令重排序的优化,即以上三步不一定会按照顺序执行,就会造成线程不安全。


优化:给instance的声明加上volatile关键字,volatile能保证线程在本地不会存有instance的副本,而是每次都到内存中读取。即禁止JVM的指令重排序优化。即按照原本的步骤。把instance声明为volatile之后,对它的写操作就会有一个内存屏障,这样,在它的赋值完成之前,就不会调用读操作。



注意:volatile阻止的不是instance = new DclSingleton();这句话内部的指令排序,而是保证了在一个写操作完成之前,不会调用读操作(if(instance == null))



public class DclSingleton {

private static volatile DclSingleton instance = null;

private DclSingleton(){

}
public static DclSingleton getInstance(){
//避免不必要的同步
if(instance == null){
//同步
synchronized (DclSingleton.class) {
//在第一次调用时初始化
if(instance == null){
instance = new DclSingleton();
}
}
}
return instance;
}

}

五、静态内部类


JVM提供了同步控制功能:static final,利用JVM进行类加载的时候保证数据同步。


在内部类中创建对象实例,只要应用中不使用内部类,JVM就不会去加载该类,就不会创建我们要创建的单例对象,


public class StaticInnerSingleton {

private StaticInnerSingleton(){

}
/**
* 在第一次加载StaticInnerSingleton时不会初始化instance,
* 只在第一次调用了getInstance方法时,JVM才会加载StaticInnerSingleton并初始化instance
* @return
*/

public static StaticInnerSingleton getInstance(){
return SingletonHolder.instance;
}
//静态内部类
private static class SingletonHolder{
private static final StaticInnerSingleton instance = new StaticInnerSingleton();
}

}


优点:JVM本身机制保证了线程安全,没有性能缺陷。


六、枚举


public enum EnumSingleton {
//定义一个枚举的元素,它就是Singleton的一个实例
INSTANCE;

public void doSomething(){

}
}

优点:写法简单,线程安全



注意:如果在枚举类中有其他实例方法或实例变量,必须确保是线程安全的。


作者:我可能是个假开发
来源:juejin.cn/post/7242614671001862199

收起阅读 »

来自智能垃圾桶的诱惑,嵌入式开发初探

背景原因 最近裸辞离职在老家,正在收拾老家的房子准备住进去,但迫于经济压力,只能先装非常有必要的东西,例如床、马桶、浴室柜等。 但是垃圾桶是生活必备品,我发现大家现在都在用智能垃圾桶,那种可以感应开盖的,还有那种自动封袋的。我极其的羡慕。但是迫于经济压力,...
继续阅读 »

背景原因



最近裸辞离职在老家,正在收拾老家的房子准备住进去,但迫于经济压力,只能先装非常有必要的东西,例如床、马桶、浴室柜等。



但是垃圾桶是生活必备品,我发现大家现在都在用智能垃圾桶,那种可以感应开盖的,还有那种自动封袋的。我极其的羡慕。但是迫于经济压力,我没有足够的资金购买智能垃圾桶!



作为一个学美术的程序员,我在想我要不要去少儿美术培训教小孩,或者去街头卖艺,甚至去给人画遗照赚点钱呢?我想了想算了,给别人留点活路吧,我这么专业的美术人才去了,那些老师、街头艺人、画师不得喝西北风去。我想了想,我除了是学美术的,我还是个程序员啊!


虽然我没有学过嵌入式、硬件、IOT等等技术,但是入门应该不太困难吧!已经好多年没有从0开始学习一门技术了,非常怀念当时学习Web技术的那种感觉,可以没日没夜的去探索新知识,学习东西。之前上班的时候根本没有精力去学习或者搞一些有趣的东西,下班只想躺着看动漫,听爽文。既然现在赋闲在家,那就搞一搞吧!


项目启动


俗话说得好,找到一个好师傅就是成功的一半!在这方面我有着得天独厚的优势,我的前公司,北京犬安科技就是一个做车联网安全的公司,这家公司的Slogan是:从硅到云,守护网联汽车安全。可以看出,犬安的硬件软件安全能力都非常的强。像我,天天和测试组的同事去山西面馆吃鱼香肉丝盖饭,喝青岛雪花,他们组里面都是硬件大佬,甚至有人造过火箭。此时不向他们学习,更待何时?


他们告诉我,我需要一个开发板、一个超声波传感器、一个舵机。


我还在研究什么是stm32/esp32的时候,他们告诉我用Arduino,特别简单。我就去研究Arduino,在B站上搜了 Arduino,有一个播放量比较高的系列教程看了一下。我顿时就悟了!


我本来以为我需要买烙铁,焊电路板的,后来我发现,只需要买以上提到的三个东西,外加一个面包版,一些杜邦线就可以,甚至可以不需要面包版。



当然为了学习,我在淘宝上买了一些乱七八糟的东西,比如按钮、各种电阻、各种颜色led灯、面包版。买各种颜色的LED灯的时候,购买欲泛滥了,就想每个颜色都买一些,什么白发红,白发黄,白发翠绿,后来我买回来发现,不亮的情况下都是白色的,我根本分不清颜色。原来那个白发黄的意思就是不亮的时候是白色,亮了以后发黄色的光~我还买了接受RGB三色的LED灯,不知道为啥不是RGBA,难道不能让灯透明吗?



我还发现不同欧姆的电阻他上面的“杠杠”是不一样的。



主要是这玩意太便宜了,两块钱就买好多,但是作为新手玩家来说,根本用不了。我只买了公对公的杜邦线,然后我又单独下单了其他杜邦线,果然两块多就能包邮。



我在不同的店铺里面选了很多配件,包括我使用的主要配件:Arduino uno 开发板、SG90舵机、HC-SR04 超声波传感器。


开搞


设备买回来以后,废了9牛2虎之力才成功的能把我写的程序上传到开发板里。


一开始一直报下面这个错误,网上众说纷纭,始终没有找到解决方案。


avrdude: stk500_getsync() attempt 1 of 10: not in sync: resp=0x30

后来我去淘宝详情页仔细研究了一番才搞明白,我本以为我买的是Arduino的开发板,其实是esp8266,只是兼容Arduino。我从淘宝详情页找到了正确姿势,成功的刷入了程序。


看了 Arduino 的视频,我主要悟出来了什么?


开发板、舵机、传感器上有很多小尖尖或者小洞洞,他叫引脚,我们需要把舵机、传感器的引脚,通过一种名为杜邦线的线(通常有公对公/母对母/公对母,代表线两头是小尖尖还是小洞洞),连接到开发板的引脚上。然后使用程序控制某个引脚的电平向舵机发送信号,或者接受传感器的信号。


第一个项目


我举一个最简单的LED灯闪烁的例子: 线是这么接的:



面包版上面,竖着的五个小洞洞里面其实是连在一起的。我的电路是从一个引脚出来,到了一个电阻上,然后连接一个LED灯,然后接地。 接地的概念应该和负极的概念很像,但是不是,不过我暂且不关心它。 为什么不直接从引脚出来接LED,再接地呢,因为视频上说,LED几乎是没有电阻的,如果直接从串口出来接到LED,直接接地的话,开发板就废了,所以加了个电阻。


接下来我让ChatGPT帮我写一个小LED灯闪烁的代码:



我们可以看到在loop里面,调用了digitalWrite,把那个引脚进行了高低电平的切换,这样就实现了小LED灯闪烁的效果,高电平就相当于给小LED灯供电了。


我们只需要把他写的代码中的引脚的编号改成我们的编号就好了。


每种开发板的引脚编号都不一样!我在淘宝上买的这个板,他没有给我资料,我干了一件特别愚蠢的事情,分别给不同的数字编号供电,插线看哪个亮,就记录对应开发板上的引脚号和数字。


第二天我在网上稍微搜了一下资料就找到了。后来我发现,竟然还有一个编号对应两个引脚的,具体为什么我也不知道。


不过具体怎么给舵机传信号,或者接受传感器的信号呢?如果在 Arduino IDE 里面的话就是简单的调用API就好了。这一块我没写代码,都是ChatGPT帮我写的。



实现目标的主要部分


我使用同样的方法,制作了垃圾桶的功能的电路和代码。 使用超声波传感器的时候,我发现这玩意都好神奇啊,仿佛是魔法一样。 比如超声波传感器,要先发送一个超声波信号出去,然后等待回波信息,拿到发送和接收的时间差,通过计算得到距离。



我在调试超声波传感器的时候,我发现我能听到超声波传感器发出的声音,只要耳朵对着它不用靠太近就可以听得到。我朋友说我是超级耳。


有了LED灯的经验以后,超声波识别距离,调用舵机旋转很快就实现了。



最开始我在考虑这个舵机的力道到底能不能支撑起来垃圾盖子,不过现在感觉力道还是挺大的。不过现在没有办法试,因为我还没有做出来合适的垃圾桶。


接下来


接下来我要去找到一个合适的垃圾桶,我也不确定需要用什么方式去打开垃圾桶的盖子,是不是需要其他的比如初中学过的滑轮、齿轮、杠杆原理什么的?这块还没有开始涉猎,等我接下来研究研究来写第二部分。



作者:明非
来源:juejin.cn/post/7215217068803145785

感谢大家阅读~

收起阅读 »

转转商品到手价

1 背景介绍 1.1 问题 搜索结果落地页,按照价格筛选及排序,结果不太准确; 用户按照价格筛选后的商品与实际存在的商品不符,可能会缺失部分商品,影响到用户购物体验。 1.2 到手价模块在促销架构中所处的位置 在整体架构中,商品的到手价涉及红包,...
继续阅读 »

1 背景介绍


1.1 问题




  • 搜索结果落地页,按照价格筛选及排序,结果不太准确;




  • 用户按照价格筛选后的商品与实际存在的商品不符,可能会缺失部分商品,影响到用户购物体验。




image


1.2 到手价模块在促销架构中所处的位置


在整体架构中,商品的到手价涉及红包,活动等模块的协同工作。通过将商品售价、红包、活动等因素纳入综合考虑,计算出最终的到手价,为顾客提供良好的购物体验。


image


2 设计目标



  • 体验:用户及时看到商品的最新到手价,提升用户购物体验;

  • 实时性:由原来的半小时看到商品的最新到手价,提升到3分钟内。


3 技术方案核心点


3.1 影响因素


image


影响商品到手价的主要因素:




  1. 商品,发布或改价;




  2. 红包,新增/删除或过期;




  3. 活动/会馆,加入或踢出。




3.2 计算公式


image


如图所示,商品详情页到手价的优惠项明细可用公式总结如下:


商品的到手价 = 商品原价 - 活动促销金额 - 红包最大优惠金额


4 落地过程及效果


image


随着业务需求的变化,系统也需要不断地增加新功能或者对现有功能进行改进,通过版本演进,可以逐步引入新的功能模块或优化现有模块,以满足业务的需求。


商品的到手价设计也是遵循这样规则,从开始的v1.0快速开发上线,响应业务; 到v2.0,v3.0进行性能优化,升级改造,使用户体验更佳,更及时。


4.1 v1.0流程


image


v1.0流程一共分为两步:




  1. 定时任务拉取拉取特定业务线的全量商品,将这批商品全量推送给各个接入方;




  2. 促销系统提供回查接口,根据商品id等参数,查询商品的到手价;




4.2 v1.0任务及接口


image




  1. v1.0任务执行时间长,偶尔还会出现执行失败的情况;而在正常情况下用户大概需要半小时左右才能感知到最新的商品到手价;




  2. 需要提供额外的单商品及批量商品接口查询到手价,无疑会对系统产生额外的查询压力,随着接入方的增加,接口qps会成比例增加;




4.3 v2.0设计


针对v1.0版本长达半个小时更新一次到手价问题,v2.0解决方案如下:



  • 实时处理部分


商品上架/商品改价;


商品加入/踢出活动;


商品加入/踢出会馆;


这部分数据的特点是,上述这些操作是能实时拿到商品的infoId,基于这些商品的infoId,可以立即计算这些商品的到手价并更新出去。


image


商品发布/改价,加入活动/会馆,踢出活动/会馆;接收这些情况下触发的mq消息,携带商品infoId,直接计算到手价。



  • 3min任务,计算特定业务线的全量商品到手价


红包: 新增/更新/删除/过期;


这部分数据的特点是,一是不能很容易能圈出受到影响的这批商品infoIds,二是有些红包的领取和使用范围可能会涉及绝大部分的商品,甚至有些时候大型促销会配置一些全平台红包,影响范围就是全量商品。


综上,结合这两种情况,以及现有的接口及能力实现v2.0;


image


推送商品的到手价,消息体格式如下,包括商品id,平台类型,到手价:


[
{"infoId":"16606xxxxxxx174465"
"ptType":"10""
realPrice"
:"638000"}
]

image


首先在Redis维护全量商品,根据商品上架/下架消息,新增或删除队列中的商品;其次将全量商品保存在10000队列中,每个队列只存一部分商品:


queue1=[infoId...]

queue2=[infoId...]

...

queue9999=[infoId...]

右图显示的是每个队列存储的商品数,队列商品数使其能保持在同一个量级。


image


多线程并发计算,每个线程只计算自己队列的商品到手价即可;同时增加监控及告警,查看每个线程的计算耗时,右图可以看到大概在120s内各线程计算完成。


注意事项:




  1. 避免无意义的计算: 将每次变化的红包维护在一个队列中,任务执行时判断是否有红包更新;




  2. 并发问题: 当任务正在执行中时,此时恰巧商品有变化(改价,加入活动等),将此次商品放入补偿队列中,延迟执行;




综上,结合这两种场景可以做到:




  1. 某些场景影响商品的到手价(如改价等),携带了商品infoId,能做到实时计算并立即推送最新的到手价;




  2. 拆分多个商品队列,并发计算; 各司其职,每个线程只计算自己队列商品的到手价;




  3. 降低促销系统压力,接入方只需要监听到手价消息即可。




4.4 v3.0设计


image


可以看到随着商品数的增加,计算耗时也成比例增加。


image


解决办法:




  1. 使用分片,v2.0是将一个大任务,由jvm多线程并发执行各自队列的商品;
    v3.0则是将这个大任务,由多个分片去执行各自队列中的商品,使其分布式执行来提高任务的执行效率和可靠性;




  2. 扩展性及稳定性强,随着商品的增多,可以适当增加分片数,降低计算耗时。




5 总结




  • 系统扩展性 数据量日渐增大,系统要能做升级扩展;




  • 系统稳定性 业务迭代,架构升级,保持系统稳定;




  • 完备的监控告警 及时的监控告警,快速发现问题,解决问题;




  • 演进原则 早期不过度设计,不同时期采用不同架构,持续迭代。







关于作者



熊先泽,转转交易营销技术部研发工程师。代码创造未来,勇于挑战,不断学习,不断成长。



转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。
关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~


作者:转转技术团队
来源:juejin.cn/post/7240006787947135034

收起阅读 »

分享近期研究的 6 款开源API网关

随着API越来越广泛和规范化,对标准化、安全协议和可扩展性的需求呈指数级增长。随着对微服务的兴趣激增,这一点尤其如此,微服务依赖于API进行通信。API网关通过一个相对容易实现的解决方案来满足这些需求。 也许最重要的是,API网关充当用户和数据之间的中介。AP...
继续阅读 »

随着API越来越广泛和规范化,对标准化、安全协议和可扩展性的需求呈指数级增长。随着对微服务的兴趣激增,这一点尤其如此,微服务依赖于API进行通信。API网关通过一个相对容易实现的解决方案来满足这些需求。


也许最重要的是,API网关充当用户和数据之间的中介。API网关是针对不正确暴露的端点的基本故障保护,而这些端点是黑客最喜欢的目标。考虑到一个被破坏的API在某些情况下可能会产生惊人的灾难性后果,仅此一点就使得API网关值得探索。网关还添加了一个有用的抽象层,这有助于将来验证您的API,防止由于版本控制或后端更改而导致的中断和服务中断。


不幸的是,许多API网关都是专有的,而且价格不便宜!值得庆幸的是,已经有几个开源API网关来满足这一需求。我们已经回顾了六个著名的开源API网关,您可以自行测试,而无需向供应商作出大量承诺。


Kong Gateway (Open Source)


Kong Gateway(OSS)是一个受欢迎的开源API网关,因为它界面流畅、社区活跃、云原生架构和广泛的功能。它速度极快,重量轻。Kong还为许多流行的基于容器和云的环境提供了现成的部署,从Docker到Kubernetes再到AWS。这使您可以轻松地将Kong集成到现有的工作流程中,从而使学习曲线不那么陡峭。


Kong支持日志记录、身份验证、速率限制、故障检测等。更好的是,它有自己的CLI,因此您可以直接从命令行管理Kong并与之交互。您可以在各种发行版上安装开源社区Kong Gateway。基本上,Kong拥有API网关所需的一切。


Tyk Open-Source API Gateway


Tyk被称为“行业最佳API网关”。与我们列表中的其他API网关不同,Tyk确实是开源的,而不仅仅是开放核心或免费增值。它为开源解决方案提供了一系列令人印象深刻的特性和功能。和Kong一样,Tyk也是云原生的,有很多插件可用。Tyk甚至可以用于以REST和GraphQL格式发布自己的API。


Tyk对许多功能都有本机支持,包括各种形式的身份验证、配额、速率限制和版本控制。它甚至可以生成API文档。最令人印象深刻的是,Tyk提供了一个API开发者门户,允许您发布托管API,因此第三方可以注册您的API,甚至管理他们的API密钥。Tyk通过其开源API网关提供了如此多的功能,实在令人难以置信。


KrakenD Open-Source API Gateway


KrakenD的开源API网关是在Go中编写的,它有几个显著的特点,尤其是对微服务的优化。它的可移植性和无状态性是其他强大的卖点,因为它可以在任何地方运行,不需要数据库。由于KrakenDesigner,它比我们列表中的其他一些API网关更灵活、更易于接近,这是一个GUI,它可以让您直观地设计或管理API。您还可以通过简单地编辑JSON文件轻松地编辑API。


KrakenD包括速率限制、过滤、缓存和授权等基本功能,并且提供了比我们提到的其他API网关更多的功能。在不修改源代码的情况下,可以使用许多插件和中间件。它的效率也很高-据维护人员称,KrakenD的吞吐量优于Tyk和Kong的其他API网关。它甚至具有本地GraphQL支持。所有这些,KrakenD的网关非常值得一看。


Gravitee OpenSource API Management


Gravite.io是另一个API网关,它具有一系列令人印象深刻的功能,这次是用Java编写的。Gravitee有三个模块用于发布、监控和记录API:




  • API管理(APIM):APIM是一个开源模块,可以让您完全控制谁访问您的API以及何时何地。




  • 访问管理(AM):Gravite为身份和访问管理提供了一个本地开源授权解决方案。它基于OAuth 2.0/OpenID协议,具有集中的身份验证和授权服务。




  • 警报引擎(AE):警报引擎是一个用于监视API的模块,允许您自定义多渠道通知,以提醒您可疑活动。




Gravitee还具有API设计器Cockpit和命令行界面graviteio-cli。所有这些都使Gravitee成为最广泛的开源API网关之一。您可以在GitHub上查看Gravite.io OpenSource API管理,或直接下载AWS、Docker、Kubernetes、Red Hat,或在此处作为Zip文件。


Apinto Microservice Gateway


显然,Go是编写API网关的流行语言。Apinto API网关用Go编写,旨在管理微服务,并提供API管理所需的所有工具。它支持身份验证、API安全以及流控制。


Apinto支持HTTP转发、多租户管理、访问控制和API访问管理,非常适合微服务或具有多种类型用户的任何开发项目。Apinto还可以通过多功能的用户定义插件系统轻松地为特定用户定制。它还具有API健康检查和仪表板等独特功能。


Apinto Microservice针对性能进行了优化,具有动态路由和负载平衡功能。根据维护人员的说法,Apinto比Nginx或Kong快50%。


Apache APISIX API Gateway


我们将使用世界上最大的开源组织之一Apache软件基金会的一个开源API网关来完善我们的开源API网关列表。Apache APISIX API网关是另一个云原生API网关,具有您目前已经认识到的所有功能——负载平衡、身份验证、速率限制和API安全。然而,有一些特殊的功能,包括多协议支持和Kubernetes入口控制。


关于开源API网关的最后思考


不受限制、不受限制的API访问时代已经结束。随着API使用的广泛普及,有无数理由实现API网关以实现安全性,因为不正确暴露的API端点可能会造成严重损害。API网关可以帮助围绕API设置速率限制,以确保安全使用。而且,如果您向第三方供应商支付高昂的价格,开源选项可能会减少您的每月IT预算。


总之,API网关为您的API环境添加了一个重要的抽象层,这可能是它们最有用的功能。这样的抽象层是防止API端点和用户数据不当暴露的一些最佳方法。然而,几乎同样重要的是它为您的API增加了灵活性。


如果没有抽象层,即使对后端的微小更改也可能导致下游的严重破坏。添加API网关可以使敏捷框架受益,并有助于

作者:CV_coder
来源:juejin.cn/post/7241778027876401213
简化CI/CD管道。

收起阅读 »

图像识别,不必造轮子

闲来无事研究了百度图像识别 API,发现该功能还算强大,在此将其使用方法总结成教程,提供大家学习参考 首先预览下效果 从以上预览图中可看出,每张图片识别出5条数据,每条数据根据识别度从高往下排,每条数据包含物品名称、识别度、所属类目 准备工作 1、注册百度账...
继续阅读 »

闲来无事研究了百度图像识别 API,发现该功能还算强大,在此将其使用方法总结成教程,提供大家学习参考


首先预览下效果


图片


从以上预览图中可看出,每张图片识别出5条数据,每条数据根据识别度从高往下排,每条数据包含物品名称识别度所属类目


准备工作


1、注册百度账号


2、登录百度智能云控制台


3、在产品列表中找到 人工智能->图像识别


4、点击创建应用,如下图:


图片


图片


图片


已创建好的应用列表


代码部分


1、获取access_token值


注意:使用图像识别需用到access_token值,因此需先获取到,以便下面代码的使用


access_token获取的方法有多种,这里使用PHP获取,更多有关access_token获取的方法以及说明可查看官方文档:


ai.baidu.com/docs#/Auth/…


创建一个get_token.php文件,用来获取access_token值


PHP获取access_token代码示例:


<?php

//请求获取access_token值函数
function request_post($url = '', $param = '') {

if (empty($url) || empty($param)) {
return false;
}

$postUrl = $url;
$curlPost = $param;
$curl = curl_init();//初始化curl
curl_setopt($curl, CURLOPT_URL,$postUrl);//抓取指定网页
curl_setopt($curl, CURLOPT_HEADER, 0);//设置header
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);//要求结果为字符串且输出到屏幕上
curl_setopt($curl, CURLOPT_POST, 1);//post提交方式
curl_setopt($curl, CURLOPT_POSTFIELDS, $curlPost);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
$data = curl_exec($curl);//运行curl
curl_close($curl);

return $data;
}

$url = 'https://aip.baidubce.com/oauth/2.0/token'; //固定地址
$post_data['grant_type'] = 'client_credentials'; //固定参数
$post_data['client_id'] = '你的 Api Key'; //创建应用的API Key;
$post_data['client_secret'] = '你的 Secret Key'; //创建应用的Secret Key;
$o = "";
foreach ( $post_data as $k => $v )
{
$o.= "$k=" . urlencode( $v ). "&" ;
}
$post_data = substr($o,0,-1);

$res = request_post($url, $post_data);//调用获取access_token值函数

var_dump($res);

?>

返回的数据如下,红框内的就是我们所要的access_token值


图片


2、图片上传及识别


2.1、在项目的根目录下创建一个upload文件夹,用于存放上传的图片


2.2、创建一个index.html文件,用于上传图片及数据渲染


代码如下:


<!DOCTYPE html>  
<html>
<head>
<meta charset="utf-8"> 
<title>使用百度 API 实现图像识别</title> 
<style type="text/css">
  .spanstyle{
    display:inline-block;
    width:500px;
    height:500px;
    position: relative;
  }
</style>
<script src="https://cdn.bootcss.com/jquery/1.10.2/jquery.min.js"></script>

<script>

  function imageUpload(imgFile) {

    var uploadfile= imgFile.files[0]  //获取图片文件流

    var formData = new FormData();    //创建一个FormData对象

    formData.append('file',uploadfile);
    //将图片放入FormData对象对象中(由于图片属于文件格式,不能直接将文件流直接通过ajax传递到后台,需要放入FormData对象中。在传递)

    $("#loading").css("opacity",1);


     $.ajax({
          type: "POST",       //POST请求
          url: "upload.php",  //接收图片的地址(同目录下的php文件)
          data:formData,      //传递的数据
          dataType:"json",    //声明成功使用json数据类型回调

          //如果传递的是FormData数据类型,那么下来的三个参数是必须的,否则会报错
          cache:false,  //默认是true,但是一般不做缓存
          processData:false, //用于对data参数进行序列化处理,这里必须false;如果是true,就会将FormData转换为String类型
          contentType:false,  //一些文件上传http协议的关系,自行百度,如果上传的有文件,那么只能设置为false

         success: function(msg){  //请求成功后的回调函数


              console.log(msg.result)

              //预览上传的图片
              var filereader = new FileReader();
              filereader.onload = function (event) {
                  var srcpath = event.target.result;
                  $("#loading").css("opacity",0);
                  $("#PreviewImg").attr("src",srcpath);
                };
              filereader.readAsDataURL(uploadfile);


                //将后台返回的数据进行进一步处理
                var data=  '<li style="margin:2% 0"><span>物品名称:'+msg.result[0].keyword+';</span> <span style="padding: 0 2%">识别度:'+msg.result[0].score*100+'%'+';</span><span>所属类目:'+msg.result[0].root+';</span></li>'

                data=data+  '<li style="margin:2% 0"><span>物品名称:'+msg.result[1].keyword+';</span> <span style="padding: 0 2%">识别度:'+msg.result[1].score*100+'%'+';</span><span>所属类目:'+msg.result[1].root+';</span></li>'

                data=data+  '<li style="margin:2% 0"><span>物品名称:'+msg.result[2].keyword+';</span> <span style="padding: 0 2%">识别度:'+msg.result[2].score*100+'%'+';</span><span>所属类目:'+msg.result[2].root+';</span></li>'

                data=data+  '<li style="margin:2% 0"><span>物品名称:'+msg.result[3].keyword+';</span> <span style="padding: 0 2%">识别度:'+msg.result[3].score*100+'%'+';</span><span>所属类目:'+msg.result[3].root+';</span></li>'


                data=data+  '<li style="margin:2% 0"><span>物品名称:'+msg.result[4].keyword+';</span> <span style="padding: 0 2%">识别度:'+msg.result[4].score*100+'%'+';</span><span>所属类目:'+msg.result[4].root+';</span></li>'



                //将识别的数据在页面渲染出来
               $("#content").html(data);


        }
  });


   }



</script>
</head>
<body>

  <fieldset>
     <input type="file"  onchange="imageUpload(this)" >
     <legend>图片上传</legend>
  </fieldset>



<div style="margin-top:2%">
    <span class="spanstyle">
      <img id="PreviewImg" src="default.jpg" style="width:100%;max-height:100%"  >
      <img id="loading" style="width:100px;height:100px;top: 36%;left: 39%;position: absolute;opacity: 0;" src="loading.gif" >
    </span>


    <span class="spanstyle" style="vertical-align: top;border: 1px dashed #ccc;background-color: #4ea8ef;color: white;">
        <h4 style="padding-left:2%">识别结果:</h4>
        <ol style="padding-right: 20px;" id="content">

        </ol>
    </span>

</div>



</body>
</html>

2.3、创建一个upload.php文件,用于接收图片及调用图像识别API


备注:百度图像识别API接口有多种,这里使用的是【通用物体和场景识别高级版】 ;该接口支持识别10万个常见物体及场景,接口返回大类及细分类的名称结果,且支持获取图片识别结果对应的百科信息


该接口调用的方法也有多种,这里使用PHP来调用接口,更多有关通用物体和场景识别高级版调用的方法以及说明可查看官方文档:


ai.baidu.com/docs#/Image…


PHP请求代码示例:


<?php  

        //图像识别请求函数    
        function request_post($url ''$param ''){

            if (empty($url) || empty($param)) {
                return false;
            }

            $postUrl $url;
            $curlPost $param;
            // 初始化curl
            $curl curl_init();
            curl_setopt($curl, CURLOPT_URL, $postUrl);
            curl_setopt($curl, CURLOPT_HEADER, 0);
            // 要求结果为字符串且输出到屏幕上
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
            // post提交方式
            curl_setopt($curl, CURLOPT_POST, 1);
            curl_setopt($curl, CURLOPT_POSTFIELDS, $curlPost);
            // 运行curl
            $data curl_exec($curl);
            curl_close($curl);

            return $data;
        }

        $temp explode("."$_FILES["file"]["name"]);
        $extension end($temp);     // 获取图片文件后缀名


        $_FILES["file"]["name"]=time().'.'.$extension;//图片重命名(以时间戳来命名)

        //将图片文件存在项目根目录下的upload文件夹下
        move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $_FILES["file"]["name"]);


        $token '调用鉴权接口获取的token';//将获取的access_token值放进去
        $url 'https://aip.baidubce.com/rest/2.0/image-classify/v2/advanced_general?access_token=' . $token;
        $img file_get_contents("upload/" . $_FILES["file"]["name"]);//本地文件路径(存入后的图片文件路径)
        $img base64_encode($img);//文件进行base64编码加密

        //请求所需要的参数
        $bodys array(
            'image' => $img,//Base64编码字符串
            'baike_num'=>5  //返回百科信息的结果数 5条
        );
        $res request_post($url$bodys);//调用请求函数

        echo $res;  //将识别的数据输出到前端


?>

结语补充


在实际开发过程中,获取access_token值并不是单独写成一个页面文件,而是写在项目系统的配置中;由于access_token值有效期为30天,可通过判断是否失效,来重新请求acc

作者:程序员Winn
来源:juejin.cn/post/7241874482574770235
ess_token值

收起阅读 »

什么是优雅的代码设计

今天我来解释一下什么样的代码才是优雅的代码设计。当然我们的代码根据实际的应用场景也分了很多维度,有偏向于底层系统的,有偏向于中间件的,也有偏向上层业务的,还有偏向于前端展示的。今天我主要来跟大家分析一下我对于业务代码的理解以及什么样才是我认为的优雅的业务代码设...
继续阅读 »

今天我来解释一下什么样的代码才是优雅的代码设计。当然我们的代码根据实际的应用场景也分了很多维度,有偏向于底层系统的,有偏向于中间件的,也有偏向上层业务的,还有偏向于前端展示的。今天我主要来跟大家分析一下我对于业务代码的理解以及什么样才是我认为的优雅的业务代码设计。


大家吐槽非常多的是,我们这边的业务代码会存在着大量的不断地持续的变化,导致我们的程序员对于业务代码设计得就比较随意。往往为了快速上线随意堆叠,不加深入思考,或者是怕影响到原来的流程,而不断在原来的代码上增加分支流程。


这种思想进一步使得代码腐化,使得大量的程序员更失去了“好代码”的标准。


那么如果代码优雅,那么要有哪些特征呢?或者说我们做哪些事情才会使得代码变得更加优雅呢?


结构化


结构化定义是指对某一概念或事物进行系统化、规范化的分析和定义,包括定义的范围、对象的属性、关系等方面,旨在准确地描述和定义所要表达的概念或事物。



我觉得首要的是代码,要一个骨架。就跟我们所说的思维结构是一样,我们对一个事物的判断,一般都是综合、立体和全面的,否则就会成为了盲人摸象,只见一斑。因此对于一个事物的判断,要综合、结构和全面。对于一段代码来说也是一样的标准,首先就是结构化。结构化是对一段代码最基本的要求,一个有良好结构的代码才可能称得上是好代码,如果只是想到哪里就写到哪里,一定成不了最优质的代码。


代码的结构化,能够让维护的人一眼就能看出主次结构、看出分层结构,能够快速掌握一段代码或者一段模块要完成的核心事情。


精简



代码跟我们抽象现实的物体一样,也要非常地精简。其实精简我觉得不仅在代码,在所有艺术品里面都是一样的,包括电影。电影虽然可能长达一个小时,两个小时,但你会发现优雅的电影它没有一帧是多余的,每出现的一个画面、一个细节,都是电影里要表达的某个情绪有关联。我们所说的文章也是一样,没有任何一个伏笔是多余的。代码也是一样,严格来说代码没有一个字符、函数、变量是多余的,每个代码都有它应该有的用处。就跟“奥卡姆剃刀”原理一样,每块代码都有它存在的价值包括注释。


但正如我们的创作一样,要完成一个功能,我们把代码写得复杂是简单的,但我们把它写得简单是非常难的。代码是思维结构的一种体现,而往往抽象能力是最为关键的,也是最难的。合适的抽象以及合理的抽象才能够让代码浓缩到最少的代码函数。


大部分情况来说,代码行数越少,则运行效率会越高。当然也不要成为极端的反面例子,不要一味追求极度少量的代码。代码的优雅一定是精要的,该有的有,不该有的一定是没有的。所以在完成一个业务逻辑的时候,一定要多问自己这个代码是不是必须有的,能不能以一种简要的方式来表达。


善用最佳实践


俗话说太阳底下没有新鲜事儿,一般来说,没有一个业务场景所需要用到的编码方式是需要你独创发明的。你所写的代码功能大概率都有人遇到过,因此对于大部分常用的编码模式,也都大家被抽象出来了一些最佳实践。那么最经典的就是23种设计模式,基本上可以涵盖90%以上的业务场景了。


以下是23种设计模式的部分简单介绍:

  1. 单例模式(Singleton Pattern):确保类只有一个实例,并提供全局访问点。
  2. 工厂模式(Factory Pattern):定义一个用于创建对象的接口,并让子类决定实例化哪个对象。
  3. 模板方法模式(Template Method Pattern):提供一种动态的创建对象的方法,通过使用不同的模板来创建对象。
  4. 装饰器模式(Decorator Pattern):将对象包装成另一个对象,从而改变原有对象的行为。
  5. 适配器模式(Adapter Pattern):将一个类的接口转换成客户希望的另一个接口,以使其能够与不同的对象交互。
  6. 外观模式(Facade Pattern):将对象的不同方面组合成一个单一的接口,从而使客户端只需访问该接口即可使用整个对象。

我们所说的设计模式就是一种对常用代码结构的一种抽象或者说套路。并不是说我们一定要用设计模式来实现功能,而是说我们要有一种最高效,最通常的方式去实现。这种方式带来了好处就是高效,而且别人理解起来也相对来说比较容易。


我们也不大推荐对于一些常见功能用一些花里胡哨的方式来实现,这样往往可能导致过度设计,但实际用处可能反而会带来其他问题。我觉得要用一些新型的代码,新型的思维方式应该是在一些比较新的场景里面去使用,去验证,而不应该在我们已有最佳实践的方式上去造额外的轮子。


这个就比如我们如果要设计一辆汽车,我们应该采用当前最新最成熟的发动机方案,而不应该从零开始自己再造一套新的发动机。但是如果这个发动机是在土星使用,要面对极端的环境,可能就需要基于当前的方案研制一套全新的发动机系统,但是大部分人是没有机会碰到土星这种业务环境的。所以通常情况下,还是不要在不需要创新的地方去创新。


除了善用最佳实践模式之快,我们还应该采用更高层的一些最佳实践框架的解决方案。比如我们在面对非常抽象,非常灵活变动的一些规则的管理上,我们可以使用大量的规则引擎工具。比如针对于流程式的业务模型上面,我们可以引入一些工作流的引擎。在需要RPC框架的时候,我们可以根据业务情况去调研使用HTTP还是DUBBO,可以集百家之所长。


持续重构



好代码往往不是一蹴而就的,而是需要我们持续打磨。有很多时候由于业务的变化以及我们思维的局限性,我们没有办法一次性就能够设计出最优的代码质量,往往需要我们后续持续的优化。所以除了初始化的设计以外,我们还应该在业务持续的发展过程中动态地去对代码进行重构。


但是往往程序员由于业务繁忙或者自身的懒惰,在业务代码上线正常运行后,就打死不愿意再动原来的代码。第一个是觉得跑得没有问题了何必去改,第二个就是改动了反而可能引起故障。这就是一种完全错误的思维,一来是给自己写不好的线上代码的一个借口,二来是没有让自己持续进步的机会。


代码重构的原则有很多,这里我就不再细讲。但是始终我觉得对线上第一个要敬畏,第二个也要花时间持续续治理。往往我们在很多时候初始化的架构是比较优雅的,是经过充分设计的,但是也是由于业务发展的迭代的原因,我们持续在存量代码上添加新功能。


有时候有一些不同的同学水平不一样,能力也不一样,所以导致后面写上的代码会非常地随意,导致整个系统就会变得越来越累赘,到了最后就不敢有新同学上去改,或者是稍微一改可能就引起未知的故障。


所以在这种情况下,如果还在追求优质的代码,就需要持续不断地重构。重构需要持续改善,并且最好每次借业务变更时,做小幅度的修改以降低风险。长此以往,整体的代码结构就得以大幅度的修改,真正达到集腋成裘的目的。下面是一些常见的重构原则:

  1. 单一职责原则:每个类或模块应该只负责一个单一的任务。这有助于降低代码的复杂度和维护成本。
  2. 开闭原则:软件实体(类、模块等)应该对扩展开放,对修改关闭。这样可以保证代码的灵活性和可维护性。
  3. 里氏替换原则:任何基类都可以被其子类替换。这可以减少代码的耦合度,提高代码的可扩展性。
  4. 接口隔离原则:不同的接口应该是相互独立的,它们只依赖于自己需要的实现,而不是其他接口。
  5. 依赖倒置原则:高层模块不应该依赖低层模块,而是依赖应用程序的功能。这可以降低代码的复杂度和耦合度。
  6. 高内聚低耦合原则:尽可能使模块内部的耦合度低,而模块之间的耦合度高。这可以提高代码的可维护性和可扩展性。
  7. 抽象工厂原则:使用抽象工厂来创建对象,这样可以减少代码的复杂度和耦合度。
  8. 单一视图原则:每个页面只应该有一个视图,这可以提高代码的可读性和可维护性。
  9. 依赖追踪原则:对代码中的所有依赖关系进行跟踪,并在必要时进行修复或重构。
  10. 测试驱动开发原则:在编写代码之前编写测试用例,并在开发过程中持续编写和运行测试用例,以确保代码的质量和稳定性。

综合


综上所述,代码要有结构化、可扩展、用最佳实践和持续重构。追求卓越的优质代码应该是每一位工程师的基本追求和基本要求,只有这样,才能不断地使得自己成为一名卓越的工程师。



作者:ali老蒋
来源:juejin.cn/post/7241115614102863928

收起阅读 »

初学后端,如何做好表结构设计?

前言 最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计? 大家关心的问题阳哥必须整理出来,希望对大家有帮助。 先说结论 这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从...
继续阅读 »

前言


最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计?


大家关心的问题阳哥必须整理出来,希望对大家有帮助。


先说结论


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:

  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)
  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度。

4个方面


设计数据库表结构需要考虑到以下4个方面:

  1. 数据库范式:通常情况下,我们希望表的数据符合某种范式,这可以保证数据的完整性和一致性。例如,第一范式要求表的每个属性都是原子性的,第二范式要求每个非主键属性完全依赖于主键,第三范式要求每个非主键属性不依赖于其他非主键属性。
  2. 实体关系模型(ER模型):我们需要先根据实际情况画出实体关系模型,然后再将其转化为数据库表结构。实体关系模型通常包括实体、属性、关系等要素,我们需要将它们转化为表的形式。
  3. 数据库性能:我们需要考虑到数据库的性能问题,包括表的大小、索引的使用、查询语句的优化等。
  4. 数据库安全:我们需要考虑到数据库的安全问题,包括表的权限、用户角色的设置等。

设计原则


在设计数据库表结构时,可以参考以下几个优雅的设计原则:

  1. 简单明了:表结构应该简单明了,避免过度复杂化。
  2. 一致性:表结构应该保持一致性,例如命名规范、数据类型等。
  3. 规范化:尽可能将表规范化,避免数据冗余和不一致性。
  4. 性能:表结构应该考虑到性能问题,例如使用适当的索引、避免全表扫描等。
  5. 安全:表结构应该考虑到安全问题,例如合理设置权限、避免SQL注入等。
  6. 扩展性:表结构应该具有一定的扩展性,例如预留字段、可扩展的关系等。

最后,需要提醒的是,优雅的数据库表结构需要在实践中不断迭代和优化,不断满足实际需求和新的挑战。



下面举个示例让大家更好的理解如何设计表结构,如何引入内存,有哪些优化思路:



问题描述



如上图所示,红框中的视频筛选标签,应该怎么设计数据库表结构?除了前台筛选,还想支持在管理后台灵活配置这些筛选标签。


这是一个很好的应用场景,大家可以先自己想一下。不要着急看我的方案。


需求分析

  1. 可以根据红框的标签筛选视频
  2. 其中综合标签比较特殊,和类型、地区、年份、演员等不一样
  • 综合是根据业务逻辑取值,并不需要入库
  • 类型、地区、年份、演员等需要入库

3.设计表结构时要考虑到:

  • 方便获取标签信息,方便把标签信息缓存处理
  • 方便根据标签筛选视频,方便我们写后续的业务逻辑

设计思路

  1. 综合标签可以写到配置文件中(或者写在前端),这些信息不需要灵活配置,所以不需要保存到数据库中
  2. 类型、地区、年份、演员都设计单独的表
  3. 视频表中设计标签表的外键,方便视频列表筛选取值
  4. 标签信息写入缓存,提高接口响应速度
  5. 类型、地区、年份、演员表也要支持对数据排序,方便后期管理维护

表结构设计


视频表


字段注释
id视频主键id
type_id类型表外键id
area_id地区表外键id
year_id年份外键id
actor_id演员外键id

其他和视频直接相关的字段(比如名称)我就省略不写了


类型表


字段注释
id类型主键id
name类型名称
sort排序字段

地区表


字段注释
id类型主键id
name类型名称
sort排序字段

年份表


字段注释
id类型主键id
name类型名称
sort排序字段

原以为年份字段不需要排序,要么是年份正序排列,要么是年份倒序排列,所以不需要sort字段。


仔细看了看需求,还有“10年代”还是需要灵活配置的呀~


演员表


字段注释
id类型主键id
name类型名称
sort排序字段

表结构设计完了,别忘了缓存


缓存策略


首先这些不会频繁更新的筛选条件建议使用缓存:


  1. 比较常用的就是redis缓存
  2. 再进阶一点,如果你使用docker,可以把这些配置信息写入docker容器所在物理机的内存中,而不用请求其他节点的redis,进一步降低网络传输带来的耗时损耗
  3. 筛选条件这类配置信息,客户端和服务端可以约定一个更新缓存的机制,客户端直接缓存配置信息,进一步提高性能

列表数据自动缓存


目前很多框架都是支持自动缓存处理的,比如goframe和go-zero


goframe


可以使用ORM链式操作-查询缓存


示例代码:


package main

import (
"time"

"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)

func main() {
var (
db = g.DB()
ctx = gctx.New()
)

// 开启调试模式,以便于记录所有执行的SQL
db.SetDebug(true)

// 写入测试数据
_, err := g.Model("user").Ctx(ctx).Data(g.Map{
"name": "xxx",
"site": "https://xxx.org",
}).Insert()

// 执行2次查询并将查询结果缓存1小时,并可执行缓存名称(可选)
for i := 0; i < 2; i++ {
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}

// 执行更新操作,并清理指定名称的查询缓存
_, err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: -1,
Name: "vip-user",
Force: false,
}).Data(gdb.Map{"name": "smith"}).Where("uid", 1).Update()
if err != nil {
g.Log().Fatal(ctx, err)
}

// 再次执行查询,启用查询缓存特性
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}

go-zero


官方都做了详细的介绍,不作为本文的重点。


讨论


我的方案也在我的技术交流群里引起了大家的讨论,也和大家分享一下:


Q1 冗余设计和一致性问题



提问: 一个表里做了这么多外键,如果我要查各自的名称,势必要关联4张表,对于这种存在多外键关联的这种表,要不要做冗余呢(直接在主表里冗余各自的名称字段)?要是保证一致性的话,就势必会影响性能,如果做冗余的话,又无法保证一致性



回答:


你看文章的上下文应该知道,文章想解决的是视频列表筛选问题。


你提到的这个场景是在视频详情信息中,如果要展示这些外键的名称怎么设计更好。


我的建议是这样的:

  1. 根据需求可以做适当冗余,比如你的主表信息量不大,配置信息修改后同步修改冗余字段的成本并不高。
  2. 或者像我文章中写的不做冗余设计,但是会把外键信息缓存,业务查询从缓存中取值。
  3. 或者将视频详情的查询结果整体进行缓存

还是看具体需求,如果这些筛选信息不变化或者不需要手工管理,甚至不需要设计表,直接写死在代码的配置文件中也可以。进一步降低DB压力,提高性能。


Q2 why设计外键?



提问:为什么要设计外键关联?直接写到视频表中不就行了?这么设计的意义在哪里?



回答:

  1. 关键问题是想解决管理后台灵活配置
  2. 如果没有这个需求,我们可以直接把筛选条件以配置文件的方式写死在程序中,降低复杂度。
  3. 站在我的角度:这个功能的筛选条件变化并不会很大,所以很懂你的意思。也建议像我2.中的方案去做,去和产品经理拉扯喽~

总结


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:

  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)
  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度

作者:王中阳Go
来源:juejin.cn/post/7212828749128876092


收起阅读 »

Springboot如何优雅的进行数据校验

基于 Spring Boot ,如何“优雅”的进行数据校验呢? 引入依赖 首先只需要给项目添加上 spring-boot-starter-web 依赖就够了,它的子依赖包含了我们所需要的东西。 注意: Spring Boot 2.3 1 之后,spring-...
继续阅读 »

基于 Spring Boot ,如何“优雅”的进行数据校验呢?


引入依赖


首先只需要给项目添加上 spring-boot-starter-web 依赖就够了,它的子依赖包含了我们所需要的东西。


image.png


注意:
Spring Boot 2.3 1 之后,spring-boot-starter-validation 已经不包括在了 spring-boot-starter-web 中,需要我们手动加上!


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

验证 Controller 的输入


一定一定不要忘记在类上加上 @ Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。


验证请求体


验证请求体即使验证被 @RequestBody 注解标记的方法参数。


PersonController


我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException。默认情况下,Spring 会将此异常转换为 HTTP Status 400(错误请求)。


@RestController
@RequestMapping("/api/person")
@Validated
public class PersonController {

@PostMapping
public ResponseEntity<PersonRequest> save(@RequestBody @Valid PersonRequest personRequest) {
return ResponseEntity.ok().body(personRequest);
}
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PersonRequest {

@NotNull(message = "classId 不能为空")
private String classId;

@Size(max = 33)
@NotNull(message = "name 不能为空")
private String name;

@Pattern(regexp = "(^Man$|^Woman$|^UGM$)", message = "sex 值不在可选范围")
@NotNull(message = "sex 不能为空")
private String sex;

}

使用 Postman 验证


image.png


验证请求参数


验证请求参数(Path Variables 和 Request Parameters)即是验证被 @PathVariable 以及 @RequestParam 标记的方法参数。


PersonController


@RestController
@RequestMapping("/api/persons")
@Validated
public class PersonController {

@GetMapping("/{id}")
public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5, message = "超过 id 的范围了") Integer id) {
return ResponseEntity.ok().body(id);
}

@PutMapping
public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6, message = "超过 name 的范围了") String name) {
return ResponseEntity.ok().body(name);
}
}

使用 Postman 验证


image.png


image.png


嵌套校验


在一个校验A对象里另一个B对象里的参数


需要在B对象上加上@Valid注解


image.png


image.png


常用校验注解总结


JSR303 定义了 Bean Validation(校验)的标准 validation-api,并没有提供实现。Hibernate Validation是对这个规范/规范的实现 hibernate-validator,并且增加了 @Email、@Length、@Range 等注解。Spring Validation 底层依赖的就是Hibernate Validation。


JSR 提供的校验注解:



  • @Null 被注释的元素必须为 null

  • @NotNull 被注释的元素必须不为 null

  • @AssertTrue 被注释的元素必须为 true

  • @AssertFalse 被注释的元素必须为 false

  • @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

  • @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

  • @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

  • @Size(max=, min=) 被注释的元素的大小必须在指定的范围内

  • @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内

  • @Past 被注释的元素必须是一个过去的日期

  • @Future 被注释的元素必须是一个将来的日期

  • @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式


Hibernate Validator 提供的校验注解



  • @NotBlank(message =) 验证字符串非 null,且长度必须大于 0

  • @Email 被注释的元素必须是电子邮箱地址

  • @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内

  • @NotEmpty 被注释的字符串的必须非空

  • @Range(min=,max=,message=) 被注释的元素必须在合适的范围内


image.png


@JsonFormat与@DateTimeFormat注解的使用


@JsonFormat用于后端传给前端的时间格式转换,@DateTimeFormat用于前端传给后端的时间格式转换


JsonFormat


1、使用maven引入@JsonFormat所需要的jar包


        <dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.8.8</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.8</version>
</dependency>

<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.13</version>
</dependency>

2、在需要查询时间的数据库字段对应的实体类的属性上添加@JsonFormat


   @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateDate;

注: timezone:是时间设置为东八区,避免时间在转换中有误差,pattern:是时间转换格式


DataTimeFormat


1、添加依赖


       <dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.3</version>
</dependency>

2、我们在对应的接收前台数据的对象的属性上加@DateTimeFormat


@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime acquireDate;

3.这样我们就可以将前端获取的时间转换为一个符合自定义格式的时间格式存储到数据库了
全局异常统一处理:拦截并处理校验出错的返回数据
写一个全局异常处理类


@ControllerAdvice

public class GlobalExceptionHandler{
/**
* 处理参数校验异常
*/

@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseBody
public ErrorResponseData validateException(MethodArgumentNotValidException e) {
log.error("参数异常"+e.getBindingResult().getFieldError().getDefaultMessage(),e);
return new ErrorResponseData(10001,e.getBindingResult().getFieldError().getDefaultMessage());
}

/**
* 处理json转换异常(比如 @DateTimeFormat注解转换日期格式时)
*/

@ExceptionHandler({HttpMessageNotReadableException.class})
@ResponseBody
public ErrorResponseData jsonParseException(HttpMessageNotReadableException e) {
log.error("参数异常"+e.getLocalizedMessage(),e);
return new ErrorResponseData(10001,e.getCause().getMessage());
}

}

作者:Hypnosis
来源:juejin.cn/post/7241114001324228663
收起阅读 »

慢慢的喜欢上泛型 之前确实冷落了

前言 下图 CSDN 水印 为自身博客 什么泛型 通俗意义上来说泛型将接口的概念进一步延伸,”泛型”字面意思就是广泛的类型,类、接口和方法代码可以应用于非常广泛的类型,代码与它们能够操作的数据类型不再绑定在一起,同一套代码,可以用于多种数据类型,这样,不仅可...
继续阅读 »

前言


下图 CSDN 水印 为自身博客


什么泛型



通俗意义上来说泛型将接口的概念进一步延伸,”泛型”字面意思就是广泛的类型,类、接口和方法代码可以应用于非常广泛的类型,代码与它们能够操作的数据类型不再绑定在一起,同一套代码,可以用于多种数据类型,这样,不仅可以复用代码,降低耦合,同时,还可以提高代码的可读性和安全性。



泛型带来的好处



在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。
那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的



public class GlmapperGeneric<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }

public static void main(String[] args) {
// do nothing
}

/**
* 不指定类型
*/

public void noSpecifyType(){
GlmapperGeneric glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 需要强制类型转换
String test = (String) glmapperGeneric.get();
System.out.println(test);
}

/**
* 指定类型
*/

public void specifyType(){
GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 不需要强制类型转换
String test = glmapperGeneric.get();
System.out.println(test);
}
}



上面这段代码中的 specifyType 方法中 省去了强制转换,可以在编译时候检查类型安全,可以用在类,方法,接口上。



泛型中通配符



我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V 等等,这些通配符又都是什么意思呢?



常用的 T,E,K,V,?



本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:




  • ?表示不确定的 java 类型

  • T (type) 表示具体的一个java类型

  • K V (key value) 分别代表java键值中的Key Value

  • E (element) 代表Element

  • < T > 等同于 < T extends Object>

  • < ? > 等同于 < ? extends Object>


?无界通配符



先从一个小例子看起:



// 范围较广
static int countLegs (List<? extends Animal > animals ) {
int retVal = 0;
for ( Animal animal : animals )
{
retVal += animal.countLegs();
}
return retVal;
}
// 范围定死
static int countLegs1 (List< Animal > animals ){
int retVal = 0;
for ( Animal animal : animals )
{
retVal += animal.countLegs();
}
return retVal;
}

public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
// 不会报错
countLegs( dogs );
// 报错
countLegs1(dogs);
}



对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。像 countLegs 方法中,限定了上届,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 countLegs1 就不行。



上界通配符 < ? extends E>



上届:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:



1.如果传入的类型不是 E 或者 E 的子类,编译不成功
2. 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用


List<? extends Number> eList = null;
eList = new ArrayList<Integer>();
//语句1取出Number(或者Number子类)对象直接赋值给Number类型的变量是符合java规范的。
Number numObject = eList.get(0); //语句1,正确

//语句2取出Number(或者Number子类)对象直接赋值给Integer类型(Number子类)的变量是不符合java规范的。
Integer intObject = eList.get(0); //语句2,错误

//List<? extends Number>eList不能够确定实例化对象的具体类型,因此无法add具体对象至列表中,可能的实例化对象如下。
eList.add(new Integer(1)); //语句3,错误

下界通配符 < ? super E>



下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object



在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。


List<? super Integer> sList = null;
sList = new ArrayList<Number>();

//List<? super Integer> 无法确定sList中存放的对象的具体类型,因此sList.get获取的值存在不确定性
//,子类对象的引用无法赋值给兄弟类的引用,父类对象的引用无法赋值给子类的引用,因此语句错误
Number numObj = sList.get(0); //语句1,错误

//Type mismatch: cannot convert from capture#6-of ? super Integer to Integer
Integer intObj = sList.get(0); //语句2,错误
//子类对象的引用可以赋值给父类对象的引用,因此语句正确。
sList.add(new Integer(1)); //语句3,正确

1. 限定通配符总是包括自己
2. 上界类型通配符:add方法受限
3. 下界类型通配符:get方法受限
4. 如果你想从一个数据类型里获取数据,使用 ? extends 通配符
5. 如果你想把对象写入一个数据结构里,
6. 使用 ? super 通配符 如果你既想存,又想取,那就别用通配符
7. 不能同时声明泛型通配符上界和下界


?和 T 的区别


// 指定集合元素只能是T类型
List<T> list = new ArrayList<T>();
// 集合元素可以是任意类型的,这种是 没有意义的 一般是方法中只是为了说明用法
List<?> list = new Arraylist<?>();


?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 :



// 可以
T t = operate();

// 不可以
?car = operate();


T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。



区别1 通过 T 来 确保 泛型参数的一致性


   public <T extends Number> void test1(List<T> dest, List<T> src) {
System.out.println();
}

public static void main(String[] args) {
test test = new test();
// integer 是number 的子类 所以是正确的
List<Integer> list = new ArrayList<Integer>();
List<Integer> list1 = new ArrayList<Integer>();
test.test1(list,list1);
}

在这里插入图片描述



通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型



public void
test(List<? extends Number> dest, List<? extends Number> src)

GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric<>();
List<String> dest = new ArrayList<>();
List<Number> src = new ArrayList<>();
glmapperGeneric.testNon(dest,src);
//上面的代码在编译器并不会报错,但是当进入到 testNon 方法内部操作时(比如赋值),对于 dest 和 src 而言,就还是需要进行类型转换

区别2:类型参数可以多重限定而通配符不行



使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 MultiLimitInterfaceA 和 MultiLimitInterfaceB 的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定



区别3:通配符可以使用超类限定而类型参数不行



类型参数 T 只具有 一种 类型限定方式



T extends A



但是通配符 ? 可以进行 两种限定



? extends A
? super A

Class和 Class<?>区别


Class<"T"> (默认没有双引号 系统会自动把T给我换成特殊字符才加的引号) 在实例化的时候,T 要替换成具体类。Class<?>它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况


作者:进阶的派大星
来源:juejin.cn/post/7140472064577634341
收起阅读 »

都JDK17了,你还在用JDK8

Spring Boot 3.1.0-M1 已经发布一段时间了,不知道各位小伙伴是否关注了。随着Spring 6.0以及SpringBoot 3.0的发布,整个开发界也逐步进入到jdk17的时代。大有当年从jdk6 到jdk8升级过程,痛苦并快乐着。 为了不被时...
继续阅读 »

Spring Boot 3.1.0-M1 已经发布一段时间了,不知道各位小伙伴是否关注了。随着Spring 6.0以及SpringBoot 3.0的发布,整个开发界也逐步进入到jdk17的时代。大有当年从jdk6 到jdk8升级过程,痛苦并快乐着。


为了不被时代抛弃,开发者应追逐新的技术发展,拥抱变化,不要固步自封。


0x01 纵观发展




  • Pre-alpha(Dev)指在软件项目进行正式测试之前执行的所有活动




  • LTS(Long-Term Support)版本指的是长期支持版本




  • Alpha 软件发布生命周期的alpha阶段是软件测试的第一阶段




  • Beta阶段是紧随alpha阶段之后的软件开发阶段,以希腊字母第二个字母命名




  • Release candidate 发行候选版(RC),也被称为“银色版本”,是具备成为稳定产品的潜力的 beta 版本,除非出现重大错误,否则准备好发布




  • Stable release 稳定版又称为生产版本,是通过所有验证和测试阶段的最后一个发行候选版(RC)




  • Release 一旦发布,软件通常被称为“稳定版”




下面我们来看下JDK9~JDK17的发展:


版本发布时间版本类型支持时间新特性
JDK 92017年9月长期支持版(LTS)5年- 模块化系统(Jigsaw)
- JShell
- 接口的私有方法
- 改进的 try-with-resources
- 集合工厂方法
- 改进的 Stream API
JDK 102018年3月短期支持版(non-LTS)6个月- 局部变量类型推断
- G1 垃圾回收器并行全阶段
- 应用级别的 Java 类数据共享
JDK 112018年9月长期支持版(LTS)8年- HTTP 客户端 API
- ZGC 垃圾回收器
- 移除 Java EE 和 CORBA 模块
JDK 122019年3月短期支持版(non-LTS)6个月- switch 表达式
- JVM 原生 HTTP 客户端
- 微基准测试套件
JDK 132019年9月短期支持版(non-LTS)6个月- switch 表达式增强
- 文本块
- ZGC 垃圾回收器增强
JDK 142020年3月短期支持版(non-LTS)6个月- switch 表达式增强
- 记录类型
- Pattern Matching for instanceof
JDK 152020年9月短期支持版(non-LTS)6个月- 移除 Nashorn JavaScript 引擎
- ZGC 垃圾回收器增强
- 隐藏类和动态类文件
JDK 162021年3月短期支持版(non-LTS)6个月- 位操作符增强
- Records 类型的完整性
- Vector API
JDK 172021年9月长期支持版(LTS)8年- 垃圾回收器改进
- Sealed 类和接口
- kafka客户端更新
- 全新的安全存储机制

需要注意的是,LTS 版本的支持时间可能会受到 Oracle 官方政策变化的影响,因此表格中的支持时间仅供参考。


0x02 详细解读


JDK9 新特性


JDK 9 是 Java 平台的一个重大版本,于2017年9月发布。它引入了多项新特性,其中最重要的是模块化系统。以下是 JDK 9 新增内容的详细解释:



  1. 模块化系统(Jigsaw):


Jigsaw 是 JDK 9 引入的一个模块化系统,它将 JDK 拆分为约 90 个模块。这些模块相互独立,可以更好地管理依赖关系和可见性,从而提高了代码的可维护性和可重用性。模块化系统还提供了一些新的工具和命令,如 jmod 命令和 jlink 命令,用于构建和组装模块化应用程序。



  1. JShell:


JShell 是一个交互式的 Java 命令行工具,可以在命令行中执行 Java 代码片段。它可以非常方便地进行代码测试和调试,并且可以快速地查看和修改代码。JShell 还提供了一些有用的功能,如自动补全、实时反馈和历史记录等。



  1. 接口的私有方法:


JDK 9 允许在接口中定义 private 和 private static 方法。这些方法可以被接口中的其他方法调用,但不能被实现该接口的类使用。这样可以避免在接口中重复编写相同的代码,提高了代码的重用性和可读性。



  1. 改进的 try-with-resources:


在 JDK 9 中,可以在 try-with-resources 语句块中使用 final 或 effectively final 的变量。这样可以避免在 finally 语句块中手动关闭资源,提高了代码的可读性和可维护性。



  1. 集合工厂方法:


JDK 9 提供了一系列工厂方法,用于创建 List、Set 和 Map 等集合对象。这些方法可以使代码更加简洁和易读,而且还可以为集合对象指定初始容量和类型参数。



  1. 改进的 Stream API:


JDK 9 引入了一些新的 Stream API 方法,如 takeWhile、dropWhile 和 ofNullable 等。takeWhile 和 dropWhile 方法可以根据指定的条件从流中选择元素,而 ofNullable 方法可以创建一个包含一个非空元素或空元素的 Stream 对象。


除了以上几个新特性,JDK 9 还引入了一些其他的改进和优化,如改进的 Stack-Walking API、改进的 CompletableFuture API、Java 应用程序启动时优化(Application Class-Data Sharing)等等。这些新特性和改进都为 Java 应用程序的开发和运行提供了更好的支持。


JDK10 新特性


JDK10是JDK的一个短期支持版本,于2018年3月发布。它的主要特性如下:




  1. 局部变量类型推断:Java 10中引入了一种新的语法——var关键字,可以用于推断局部变量的类型,使代码更加简洁。例如,可以这样定义一个字符串类型的局部变量:var str = "hello"




  2. G1 垃圾回收器并行全阶段:JDK10中对G1垃圾回收器进行了改进,使其可以在并行全阶段进行垃圾回收,从而提高了GC效率。




  3. 应用级别的 Java 类数据共享:Java 10中引入了一项新的特性,即应用级别的 Java 类数据共享(AppCDS),可以在多个JVM进程之间共享Java类元数据,从而加速JVM的启动时间。




  4. 线程局部握手协议:Java 10中引入了线程局部握手协议(Thread-Local Handshakes),可以在不影响整个JVM性能的情况下,暂停所有线程执行特定的操作。




  5. 其他改进:Java 10还包含一些其他的改进,例如对Unicode 10.0的支持,对时间API的改进,以及对容器API的改进等等。




总的来说,JDK10主要关注于提高Java应用程序的性能和可维护性,通过引入一些新的特性和改进对JDK进行优化。


JDK11 新特性


JDK11是JDK的一个长期支持版本,于2018年9月发布。它的主要特性如下:




  1. HTTP 客户端 API:Java 11中引入了一个全新的HTTP客户端API,可以用于发送HTTP请求和接收HTTP响应,而不需要依赖第三方库。




  2. ZGC 垃圾回收器:Java 11中引入了ZGC垃圾回收器(Z Garbage Collector),它是一种可伸缩且低延迟的垃圾回收器,可以在数百GB的堆上运行,且最大停顿时间不超过10ms。




  3. 移除Java EE和CORBA模块:Java 11中移除了Java EE和CORBA模块,这些模块在Java 9中已被标记为“过时”,并在Java 11中被完全移除。




  4. Epsilon垃圾回收器:Java 11中引入了一种新的垃圾回收器,称为Epsilon垃圾回收器,它是一种无操作的垃圾回收器,可以在不进行垃圾回收的情况下运行应用程序,适用于测试和基准测试等场景。




  5. 其他改进:Java 11还包含一些其他的改进,例如对Lambda参数的本地变量类型推断,对字符串API的改进,以及对嵌套的访问控制的改进等等。




总的来说,JDK11主要关注于提高Java应用程序的性能和安全性,通过引入一些新的特性和改进对JDK进行优化。其中,HTTP客户端API和ZGC垃圾回收器是最值得关注的特性之一。


JDK12 新特性


JDK12是JDK的一个短期支持版本,于2019年3月发布。它的主要特性如下:




  1. Switch 表达式:Java 12中引入了一种新的Switch表达式,可以使用Lambda表达式风格来简化代码。此外,Switch表达式也支持返回值,从而可以更方便地进行流程控制。




  2. Microbenchmark Suite:Java 12中引入了一个Microbenchmark Suite,可以用于进行微基准测试,从而更好地评估Java程序的性能。




  3. JVM Constants API:Java 12中引入了JVM Constants API,可以用于在运行时获取常量池中的常量,从而更好地支持动态语言和元编程。




  4. Shenandoah 垃圾回收器:Java 12中引入了Shenandoah垃圾回收器,它是一种低暂停时间的垃圾回收器,可以在非常大的堆上运行,且最大暂停时间不超过几毫秒。




  5. 其他改进:Java 12还包含一些其他的改进,例如对Unicode 11.0的支持,对预览功能的改进,以及对集合API的改进等等。




总的来说,JDK12主要关注于提高Java应用程序的性能和可维护性,通过引入一些新的特性和改进对JDK进行优化。其中,Switch表达式和Shenandoah垃圾回收器是最值得关注的特性之一。


JDK13 新特性


JDK13是JDK的一个短期支持版本,于2019年9月发布。它的主要特性如下:




  1. Text Blocks:Java 13中引入了一种新的语法,称为Text Blocks,可以用于在代码中编写多行字符串,从而简化代码编写的复杂度。




  2. Switch 表达式增强:Java 13中对Switch表达式进行了增强,支持多个表达式和Lambda表达式。




  3. ZGC 并行处理引用操作:Java 13中对ZGC垃圾回收器进行了改进,支持并行处理引用操作,从而提高了GC效率。




  4. Reimplement the Legacy Socket API:Java 13中重新实现了Legacy Socket API,从而提高了网络编程的性能和可维护性。




  5. 其他改进:Java 13还包含一些其他的改进,例如对预览功能的改进,对嵌套访问控制的改进,以及对集合API的改进等等。




总的来说,JDK13主要关注于提高Java应用程序的性能和可维护性,通过引入一些新的特性和改进对JDK进行优化。其中,Text Blocks和Switch表达式增强是最值得关注的特性之一。


JDK14 新特性


JDK14是JDK的一个短期支持版本,于2020年3月发布。它的主要特性如下:




  1. Records:Java 14中引入了一种新的语法,称为Records,可以用于定义不可变的数据类,从而简化代码编写的复杂度。




  2. Switch 表达式增强:Java 14中对Switch表达式进行了增强,支持使用关键字 yield 返回值,从而可以更方便地进行流程控制。




  3. Text Blocks增强:Java 14中对Text Blocks进行了增强,支持在Text Blocks中嵌入表达式,从而可以更方便地生成动态字符串。




  4. Pattern Matching for instanceof:Java 14中引入了一种新的语法,称为Pattern Matching for instanceof,可以用于在判断对象类型时,同时对对象进行转换和赋值。




  5. 其他改进:Java 14还包含一些其他的改进,例如对垃圾回收器和JVM的改进,对预览功能的改进,以及对JFR的改进等等。




总的来说,JDK14主要关注于提高Java应用程序的可维护性和易用性,通过引入一些新的特性和改进对JDK进行优化。其中,Records和Pattern Matching for instanceof是最值得关注的特性之一。


JDK15 新特性


JDK15是JDK的一个短期支持版本,于2020年9月发布。它的主要特性如下:




  1. Sealed Classes:Java 15中引入了一种新的语法,称为Sealed Classes,可以用于限制某个类的子类的数量,从而提高代码的可维护性。




  2. Text Blocks增强:Java 15中对Text Blocks进行了增强,支持在Text Blocks中使用反斜杠和$符号来表示特殊字符,从而可以更方便地生成动态字符串。




  3. Hidden Classes:Java 15中引入了一种新的类,称为Hidden Classes,可以在运行时动态创建和卸载类,从而提高代码的灵活性和安全性。




  4. ZGC并发垃圾回收器增强:Java 15中对ZGC垃圾回收器进行了增强,支持在启动时指定内存大小,从而提高了GC效率。




  5. 其他改进:Java 15还包含一些其他的改进,例如对预览功能的改进,对JVM和垃圾回收器的改进,以及对集合API和I/O API的改进等等。




总的来说,JDK15主要关注于提高Java应用程序的可维护性和性能,通过引入一些新的特性和改进对JDK进行优化。其中,Sealed Classes和Hidden Classes是最值得关注的特性之一。


JDK16 新特性


JDK16是JDK的一个短期支持版本,于2021年3月发布。它的主要特性如下:




  1. Records增强:Java 16中对Records进行了增强,支持在Records中定义静态方法和构造方法,从而可以更方便地进行对象的创建和操作。




  2. Pattern Matching for instanceof增强:Java 16中对Pattern Matching for instanceof进行了增强,支持在判断对象类型时,同时对对象进行转换和赋值,并支持对switch语句进行模式匹配。




  3. Vector API:Java 16中引入了一种新的API,称为Vector API,可以用于进行SIMD(Single Instruction Multiple Data)向量计算,从而提高计算效率。




  4. JEP 388:Java 16中引入了一个新的JEP(JDK Enhancement Proposal),称为JEP 388,可以用于提高Java应用程序的性能和可维护性。




  5. 其他改进:Java 16还包含一些其他的改进,例如对垃圾回收器、JVM和JFR的改进,对预览功能的改进,以及对集合API和I/O API的改进等等。




总的来说,JDK16主要关注于提高Java应用程序的性能和可维护性,通过引入一些新的特性和改进对JDK进行优化。其中,Records增强和Pattern Matching for instanceof增强是最值得关注的特性之一。


JDK17 新特性


JDK17是JDK的一个长期支持版本,于2021年9月发布。它的主要特性如下:




  1. Sealed Classes增强:Java 17中对Sealed Classes进行了增强,支持在Sealed Classes中定义接口和枚举类型,从而提高代码的灵活性。




  2. Pattern Matching for switch增强:Java 17中对Pattern Matching for switch进行了增强,支持在switch语句中使用嵌套模式和or运算符,从而提高代码的可读性和灵活性。




  3. Foreign Function and Memory API:Java 17中引入了一种新的API,称为Foreign Function and Memory API,可以用于在Java中调用C和C++的函数和库,从而提高代码的可扩展性和互操作性。




  4. JEP 391:Java 17中引入了一个新的JEP(JDK Enhancement Proposal),称为JEP 391,可以用于提高Java应用程序的性能和可维护性。




  5. 其他改进:Java 17还包含一些其他的改进,例如对垃圾回收器、JVM和JFR的改进,对预览功能的改进,以及对集合API和I/O API的改进等等。




总的来说,JDK17主要关注于提高Java应用程序的灵活性、可扩展性和性能,通过引入一些新的特性和改进对JDK进行优化。其中,Sealed Classes增强和Foreign Function and Memory API是最值得关注的特性之一。


总结




  • JDK9:引入了模块化系统、JShell、私有接口方法、多版本兼容性等新特性




  • JDK10:引入了局部变量类型推断、垃圾回收器接口、并行全垃圾回收器等新特性




  • JDK11:引入了ZGC垃圾回收器、HTTP客户端API、VarHandles API等新特性




  • JDK12:引入了Switch表达式、新的字符串方法、HTTP/2客户端API等新特性




  • JDK13:引入了Text Blocks、Switch表达式增强、改进的ZGC性能等新特性




  • JDK14:引入了Records、Switch表达式增强、Pattern Matching for instanceof等新特性




  • JDK15:引入了Sealed Classes、Text Blocks增强、Hidden Classes等新特性




  • JDK16:引入了Records增强、Pattern Matching for instanceof增强、Vector API等新特性




  • JDK17:引入了Sealed Classes增强、Pattern Matching for switch增强、Foreign Function and Memory API等新特性




总的来说,JDK9到JDK17的更新涵盖了Java应用程序开发的各个方面,包括模块化、垃圾回收、性能优化、API增强等等,为Java开发者提供了更多的选择和工具,以提高代码的质量和效率


小记


Java作为一门长盛不衰的编程语言,未来的发展仍然有许多潜力和机会。




  • 云计算和大数据:随着云计算和大数据的发展,Java在这些领域的应用也越来越广泛。Java已经成为了许多云计算平台和大数据处理框架的首选语言之一。




  • 移动端和IoT:Java也逐渐开始在移动端和物联网领域崭露头角。Java的跨平台特性和安全性,使得它成为了许多移动应用和物联网设备的首选开发语言。




  • 前沿技术的应用:Java社区一直在积极探索和应用前沿技术,例如人工智能、机器学习、区块链等。Java在这些领域的应用和发展也将会是未来的趋势。




  • 开源社区的发展:Java开源社区的发展也将会对Java的未来产生重要影响。Java社区的开源项目和社区贡献者数量不断增加,将会为Java的发展提供更多的动力和资源。




  • 新的Java版本:Oracle已经宣布将在未来两年内发布两个新的Java版本,其中一个是短期支持版本,另一个是长期支持版本。这将会为Java开发者提供更多的新特性和改进,以满足不断变化的需求。




总的来说,Java作为一门优秀的编程语言,具有广泛的应用和发展前景。随着技术的不断创新和社区的不断发展,Java的未来将会更加光明。


更多内容:


image.png


作者:QIANGLU
来源:juejin.cn/post/7238795712569802809
收起阅读 »

对接了个三方支付,给俺气的呀

故事是这样的: 我们的商城需要在日本上线去赚小日子过得不错的日本人的钱,所以支付是首要的。就找了一家做日本本地支付的公司做对接,公司名字当然不能说,打我也不说。 第一天,很愉快,签了协议,给了开发文档。俺就准备开始撸代码了。 API文档 这开发文档,打开两秒钟...
继续阅读 »

故事是这样的:


我们的商城需要在日本上线去赚小日子过得不错的日本人的钱,所以支付是首要的。就找了一家做日本本地支付的公司做对接,公司名字当然不能说,打我也不说。


第一天,很愉快,签了协议,给了开发文档。俺就准备开始撸代码了。


API文档


这开发文档,打开两秒钟就自动挂掉了,我只能一次又一次的点击Reload


image.png


后来实在受不了了,我趁着那两三秒钟显示的时间,截图对照着看。结果就是所有的字段不能复制粘贴了,只能一个一个敲。


还是这个API文档,必输字段非必输字段乱的一塌糊涂,哪些是必输纯靠试,有的必输字段调试的时候有问题,对面直接甩过来一句,要么不传了吧。听得我懵懵的。


加密,验签


然后到了加密,验签。竟然能只给了node的demo,咱对接的大部分是后端程序员吧,没事,咱自己写。比较坑的是MD5加密完是大写的字母,这三方公司转小写了,也没有提示,折腾了一会,测了一会也就通了,还好还好。


场景支持


在之后就是支付的一个场景了,日本是没有微信支付宝这样的便捷支付的,要么信用卡,要么便利店支付



稍微说下便利店支付,就是说,客户下完单之后,会给到一个回执,是一串数字,我们且称之为支付码,他们便利店有一个类似于ATM机的柜员机,去这个机子上凭借这串支付码打印出来一个凭条,然后拿着这个凭条去找便利店的店员,现金支付



就是这个场景,就是这个数字,三方它就给客户显示一次,就一次,点击支付的时候显示一次。要是客户不截图,那么不好意思,您就重新下单吧。而且这个支付码我们拿不到,这个跳转了他们的页面,也不发个邮件告知。这明显没法用啊,我们的订单过期时间三天,客户这三天啥时候想去便利店支付都行,可是这只显示一次太扯了。

同样的请求再发一次显示的是支付进行中。这怎么玩,好说歹说他们排期开发了两周,把这个订单号重入解决了,就是说同一笔订单再次进入是可以看到那串支付码的。


测试环境不能测


最后,写完了,调通了,测一下支付,结果他们测试环境不支持日本的这个便利店支付测试,what? 测试环境不能测试?那我这么久在干什么,让我们上线上测,我的代码在测试环境还没搞完,让我上生产,最后上了,没测通,对方的问题,当天下午就把代码给回滚了。等着对方调试完继续测。


业务不完整


还有,不支持退款,作为一个支付公司,不支持退款,我们客户退款只能线下转账,闹呢。


以前对接三方的时候,遇到问题地想到的是我们是不是哪里做错了,再看看文档。对接这个公司,就跟他们公司的测试一样,找bug呢。


建议


这里是本文的重点,咱就是给点建议,作为一家提供服务的公司,不管是啥服务。



  1. 对外API文档应当可以正常显示,必输非必输定义正确,字段类型标注准确。

  2. 若是有验签的步骤,介绍步骤详细并配上各个语言的demo,并强调格式以及大小写。

  3. 牵扯到业务的,需要站在客户的角度思考,这个是否合情合理。

  4. 业务的完整性,有可能是尚未开发完全,但是得有备选的方案。


作者:奔跑的毛球
来源:juejin.cn/post/7127691522010529799
收起阅读 »

Go 开发短网址服务笔记

这篇文章是基于课程 Go 开发短地址服务 做的笔记 项目地址:github.com/astak16/sho… 错误处理 在处理业务逻辑时,如果出错误了,需要统一处理错误响应的格式,这样可以方便前端处理错误信息 所以需要定义一个 Error 接口,它包含了 er...
继续阅读 »

这篇文章是基于课程 Go 开发短地址服务 做的笔记


项目地址:github.com/astak16/sho…


错误处理


在处理业务逻辑时,如果出错误了,需要统一处理错误响应的格式,这样可以方便前端处理错误信息


所以需要定义一个 Error 接口,它包含了 error 接口,以及一个 Status() 方法,用来返回错误的状态码


type Error interface {
error
Status() int
}

这个接口用来判断错误类型,在 go 中可以通过 e.(type) 判断错误的类型


func respondWithError(w http.RespondWrite, err error) {
switch e.(type) {
case Error:
respondWithJSON(w, e.Status(), e.Error())
default:
respondWithJSON(w, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
}

go 中 实现 Error 接口,只需要实现 Error()Status() 方法即可


func () Error() string {
return ""
}
func () Status() int {
return 0
}

这样定义的方法,只能返回固定的文本和状态码,如果想要返回动态内容,可以定义一个结构体


然后 ErrorStatus 方法接受 StatusError 类型


这样只要满足 StatusError 类型的结构体,就可以返回动态内容


所以上面的代码可以修改为:


type StatusError struct {
Code int
Err error
}
func (se StatusError) Error() string {
return se.Err.Error()
}
func (se StatusError) Status() int {
return se.Code
}

middlerware


RecoverHandler


中间件 RecoverHandler 作用是通过 defer 来捕获 panic,然后返回 500 状态码


func RecoverHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Println("Recover from panic %+v", r)
http.Error(w, http.StatusText(500), 500)
}
}()
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

LoggingHandler


LoggingHandler 作用是记录请求耗时


func (m Middleware) LoggingHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
end := time.Now()
log.Printf("[%s] %q %v", r.Method, r.URL.Path, end.Sub(start))
}
return http.HandlerFunc(fn)
}

中间件使用


alicego 中的一个中间件库,可以通过 alice.New() 来添加中间件,具体使用如下:


m := alice.New(middleware.LoggingHandler, middleware.RecoverHandler)
mux.Router.HandleFunc("/api/v1/user", m.ThenFunc(controller)).Methods("POST")

生成短链接


redis 连接


func NewRedisCli(addr string, passwd string, db int) *RedisCli {
c := redis.NewClient(&redis.Options{
Addr: addr,
Password: passwd,
DB: db,
})

if _, err := c.Ping().Result(); err != nil {
panic(err)
}
return &RedisCli{Cli: c}
}

生成唯一 ID


redis 可以基于一个键名生成一个唯一的自增 ID,这个键名可以是任意的,这个方法是 Incr


代码如下:


err = r.Cli.Incr(URLIDKEY).Err()
if err != nil {
return "", err
}

id, err := r.Cli.Get(URLIDKEY).Int64()
if err != nil {
return "", err
}

fmt.Println(id) // 每次调用都会自增

存储和解析短链接


一个 ID 对应一个 url,也就是说当外面传入 id 时需要返回对应的 url


func Shorten() {
err := r.Cli.Set(fmt.Sprintf(ShortlinkKey, eid), url, time.Minute*time.Duration(exp)).Err()
if err != nil {
return "", err
}
}
func UnShorten() {
url, err := r.Cli.Get(fmt.Sprintf(ShortlinkKey, eid)).Result()
}

redis 注意事项


redis 返回的 error 有两种情况:



  1. redis.Nil 表示没有找到对应的值

  2. 其他错误,表示 redis 服务出错了


所以在使用 redis 时,需要判断返回的错误类型


if err == redis.Nil {
// 没有找到对应的值
} else if err != nil {
// redis 服务出错了
} else {
// 正确响应
}

测试


在测试用例中,如何发起一个请求,然后获取响应的数据呢?



  1. 构造请求


var jsonStr = []byte(`{"url":"https://www.baidu.com","expiration_in_minutes":60}`)
req, err := http.NewRequest("POST", "/api/shorten", bytes.NewBuffer(jsonStr))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")


  1. 捕获 http 响应


rw := httptest.NewRecorder()


  1. 模拟请求被处理


app.Router.ServeHTTP(rw, req)


  1. 解析响应


if rw.Code != http.ok {
t.Fatalf("Excepted status created, got %d", rw.Code)
}

resp := struct {
Shortlink string `json:"shortlink"`
}{}
if err := json.NewDecoder(rw.Body).Decode(&resp); err != nil {
t.Fatalf("should decode the response", err)
}

最终完整代码:


var jsonStr = []byte(`{"url":"https://www.baidu.com","expiration_in_minutes":60}`)
req, err := http.NewRequest("POST", "/api/shorten", bytes.NewBuffer(jsonStr))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")

rw := httptest.NewRecorder()
app.Router.ServeHTTP(rw, req)

if rw.Code != http.ok {
t.Fatalf("Excepted status created, got %d", rw.Code)
}
resp := struct {
Shortlink string `json:"shortlink"`
}{}

if err := json.NewDecoder(rw.Body).Decode(&resp); err != nil {
t.Fatalf("should decode the response")
}

代码


log.SetFlags(log.LstdFlags | log.Lshortfile)


作用是设置日志输出的标志


它们都是标志常量,用竖线 | 连接,这是位操作符,将他们合并为一个整数值,作为 log.SetFlags() 的参数



  • log.LstdFlags 是标准时间格式:2022-01-23 01:23:23

  • log.Lshortfile 是文件名和行号:main.go:23


当我们使用 log.Println 输出日志时,会自动带上时间、文件名、行号信息


recover 函数使用


recover 函数类似于其他语言的 try...catch,用来捕获 panic,做一些处理


使用方法:


func MyFunc() {
defer func() {
if r := recover(); r != nil {
// 处理 panic 情况
}
}
}

需要注意的是:



  1. recover 函数只能在 defer 中使用,如果在 defer 之外使用,会直接返回 nil

  2. recover 函数只有在 panic 之后调用才会生效,如果在 panic 之前调用,也会直接返回 nil

  3. recover 函数只能捕获当前 goroutinepanic,不能捕获其他 goroutinepanic


next.ServerHttp(w, r)


next.ServeHTTP(w, r),用于将 http 请求传递给下一个 handler


HandleFunc 和 Handle 区别


HandleFunc 接受一个普通类型的函数:


func myHandle(w http.ResponseWriter, r *http.Request) {}
http.HandleFunc("xxxx", myHandle)

Handle 接收一个实现 Handler 接口的函数:


func myHandler(w http.ResponseWriter, r *http.Request) {}
http.Handle("xxxx", http.HandlerFunc(myHandler))

他们的区别是:使用 Handle 需要自己进行包装,使用 HandleFunc 不需要


defer res.Body.Close()


为什么没有 res.Header.Close() 方法?


因为 header 不是资源,而 body 是资源,在 go 中,一般操作资源后,要及时关闭资源,所以 gobody 提供了 Close() 方法


res.Bodyio.ReadCloser 类型的接口,表示可以读取响应数据并关闭响应体的对象


w.Write()


代码在执行了 w.Writer(res) 后,还会继续往下执行,除非有显示的 returepanic 终止函数执行


func controller(w http.ResponseWriter, r *http.Request) {
if res, err := xxx; err != nil {
respondWithJSON(w, http.StatusOK, err)
}
// 这里如果有代码,会继续执行
}
func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
res, _ json.Marshal(payload)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(res)
}

需要注意的是,尽管执行了 w.Writer() 后,还会继续往下执行,但不会再对响应进行修改或写入任何内容了,因为 w.Write() 已经将响应写入到 http.ResponseWriter 中了


获取请求参数


路由 /api/info?shortlink=2


a.Router.Handle("/api/info", m.ThenFunc(a.getShortlinkInfo)).Methods("GET")

func getShortlinkInfo(w http.ResponseWriter, r *http.Request) {
vals := r.URL.Query()
s := vals.Get("shortlink")

fmt.Println(s) // 2
}

路由 /2


a.Router.Handle("/{shortlink:[a-zA-Z0-9]{1,11}}", m.ThenFunc(a.redirect)).Methods("GET")

func redirect(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
shortlink := vars["shortlink"]

fmt.Println(shortlink) // 2
}

获取请求体


json.NewDecoder(r.Body) 作用是将 http 请求的 body 内容解析为 json 格式


r.body 是一个 io.Reader 类型,它代表请求的原始数据


如果关联成功可以用 Decode() 方法来解析 json 数据


type User struct {
Name string `json:"name"`
Age int `json:"age"`
}

func controller(w http.ResponseWriter, r *http.Request){
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
fmt.Println(err)
}
fmt.Println(user)
}

new


用于创建一个新的零值对象,并返回该对象的指针


它接受一个类型作为参数,并返回一个指向该类型的指针


适用于任何可分配的类型,如基本类型、结构体、数组、切片、映射和接口等


// 创建一个新的 int 类型的零值对象,并返回指向它的指针
ptr := new(int) // 0

需要注意的是:new 只分配了内存,并初始化为零值,并不会对对象进行任何进一步的初始化。如果需要对对象进行自定义的初始化操作,可以使用结构体字面量或构造函数等方式


往期文章



  1. Go 项目ORM、测试、api文档搭建

  2. Go 实现统一加载资源的入口<
    作者:uccs
    来源:juejin.cn/post/7237702874880393274
    /a>

收起阅读 »

原型模式与享元模式

原型模式与享元模式 原型模式和享元模式,前者是在创建多个实例时,对创建过程的性能进行调优;后者是用减少创建实例的方式,来调优系统性能。这么看,你会不会觉得两个模式有点相互矛盾呢? 其实不然,它们的使用是分场景的。在有些场景下,我们需要重复创建多个实例,例如在循...
继续阅读 »

原型模式与享元模式


原型模式和享元模式,前者是在创建多个实例时,对创建过程的性能进行调优;后者是用减少创建实例的方式,来调优系统性能。这么看,你会不会觉得两个模式有点相互矛盾呢?


其实不然,它们的使用是分场景的。在有些场景下,我们需要重复创建多个实例,例如在循环体中赋值一个对象,此时我们就可以采用原型模式来优化对象的创建过程;而在有些场景下,我们则可以避免重复创建多个实例,在内存中共享对象就好了。


今天我们就来看看这两种模式的适用场景,看看如何使用它们来提升系统性能。


原型模式


原型模式是通过给出一个原型对象来指明所创建的对象的类型,然后使用自身实现的克隆接口来复制这个原型对象,该模式就是用这种方式来创建出更多同类型的对象。


使用这种方式创建新的对象的话,就无需再通过new实例化来创建对象了。这是因为Object类的clone方法是一个本地方法,它可以直接操作内存中的二进制流,所以性能相对new实例化来说,更佳。


实现原型模式


我们现在通过一个简单的例子来实现一个原型模式:


   //实现Cloneable 接口的原型抽象类Prototype
class Prototype implements Cloneable {
//重写clone方法
public Prototype clone(){
Prototype prototype = null;
try{
prototype = (Prototype)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return prototype;
}
}
//实现原型类
class ConcretePrototype extends Prototype{
public void show(){
System.out.println("原型模式实现类");
}
}

public class Client {
public static void main(String[] args){
ConcretePrototype cp = new ConcretePrototype();
for(int i=0; i< 10; i++){
ConcretePrototype clonecp = (ConcretePrototype)cp.clone();
clonecp.show();
}
}
}


要实现一个原型类,需要具备三个条件:



  • 实现Cloneable接口:Cloneable接口与序列化接口的作用类似,它只是告诉虚拟机可以安全地在实现了这个接口的类上使用clone方法。在JVM中,只有实现了Cloneable接口的类才可以被拷贝,否则会抛出CloneNotSupportedException异常。

  • 重写Object类中的clone方法:在Java中,所有类的父类都是Object类,而Object类中有一个clone方法,作用是返回对象的一个拷贝。

  • 在重写的clone方法中调用super.clone():默认情况下,类不具备复制对象的能力,需要调用super.clone()来实现。


从上面我们可以看出,原型模式的主要特征就是使用clone方法复制一个对象。通常,有些人会误以为 Object a=new Object();Object b=a; 这种形式就是一种对象复制的过程,然而这种复制只是对象引用的复制,也就是a和b对象指向了同一个内存地址,如果b修改了,a的值也就跟着被修改了。


我们可以通过一个简单的例子来看看普通的对象复制问题:


class Student {
private String name;

public String getName() {
return name;
}

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

}
public class Test {

public static void main(String args[]) {
Student stu1 = new Student();
stu1.setName("test1");

Student stu2 = stu1;
stu2.setName("test2");

System.out.println("学生1:" + stu1.getName());
System.out.println("学生2:" + stu2.getName());
}
}


如果是复制对象,此时打印的日志应该为:


学生1:test1
学生2:test2


然而,实际上是:


学生1:test2
学生2:test2


通过clone方法复制的对象才是真正的对象复制,clone方法赋值的对象完全是一个独立的对象。刚刚讲过了,Object类的clone方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。我们可以用 clone 方法再实现一遍以上例子。


//学生类实现Cloneable接口
class Student implements Cloneable{
private String name; //姓名

public String getName() {
return name;
}

public void setName(String name) {
this.name= name;
}
//重写clone方法
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}

}
public class Test {

public static void main(String args[]) {
Student stu1 = new Student(); //创建学生1
stu1.setName("test1");

Student stu2 = stu1.clone(); //通过克隆创建学生2
stu2.setName("test2");

System.out.println("学生1:" + stu1.getName());
System.out.println("学生2:" + stu2.getName());
}
}


运行结果:


学生1:test1
学生2:test2


深拷贝和浅拷贝


在调用super.clone()方法之后,首先会检查当前对象所属的类是否支持clone,也就是看该类是否实现了Cloneable接口。


如果支持,则创建当前对象所属类的一个新对象,并对该对象进行初始化,使得新对象的成员变量的值与当前对象的成员变量的值一模一样,但对于其它对象的引用以及List等类型的成员属性,则只能复制这些对象的引用了。所以简单调用super.clone()这种克隆对象方式,就是一种浅拷贝。


所以,当我们在使用clone()方法实现对象的克隆时,就需要注意浅拷贝带来的问题。我们再通过一个例子来看看浅拷贝。


//定义学生类
class Student implements Cloneable{
private String name; //学生姓名
private Teacher teacher; //定义老师类

public String getName() {
return name;
}

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

public Teacher getTeacher() {
return teacher;
}

public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
//重写克隆方法
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}

}

//定义老师类
class Teacher implements Cloneable{
private String name; //老师姓名

public String getName() {
return name;
}

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

//重写克隆方法,对老师类进行克隆
public Teacher clone() {
Teacher teacher= null;
try {
teacher= (Teacher) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}

}
public class Test {

public static void main(String args[]) {
Teacher teacher = new Teacher (); //定义老师1
teacher.setName("刘老师");
Student stu1 = new Student(); //定义学生1
stu1.setName("test1");
stu1.setTeacher(teacher);

Student stu2 = stu1.clone(); //定义学生2
stu2.setName("test2");
stu2.getTeacher().setName("王老师");//修改老师
System.out.println("学生" + stu1.getName + "的老师是:" + stu1.getTeacher().getName);
System.out.println("学生" + stu1.getName + "的老师是:" + stu2.getTeacher().getName);
}
}


运行结果:


学生test1的老师是:王老师
学生test2的老师是:王老师


观察以上运行结果,我们可以发现:在我们给学生2修改老师的时候,学生1的老师也跟着被修改了。这就是浅拷贝带来的问题。


我们可以通过深拷贝来解决这种问题,其实深拷贝就是基于浅拷贝来递归实现具体的每个对象,代码如下:


   public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
Teacher teacher = this.teacher.clone();//克隆teacher对象
student.setTeacher(teacher);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}


适用场景


前面我详述了原型模式的实现原理,那到底什么时候我们要用它呢?


在一些重复创建对象的场景下,我们就可以使用原型模式来提高对象的创建性能。例如,我在开头提到的,循环体内创建对象时,我们就可以考虑用clone的方式来实现。


例如:


for(int i=0; i<list.size(); i++){
Student stu = new Student();
...
}


我们可以优化为:


Student stu = new Student();
for(int i=0; i<list.size(); i++){
Student stu1 = (Student)stu.clone();
...
}


除此之外,原型模式在开源框架中的应用也非常广泛。例如Spring中,@Service默认都是单例的。用了私有全局变量,若不想影响下次注入或每次上下文获取bean,就需要用到原型模式,我们可以通过以下注解来实现,@Scope(“prototype”)。


享元模式


享元模式是运用共享技术有效地最大限度地复用细粒度对象的一种模式。该模式中,以对象的信息状态划分,可以分为内部数据和外部数据。内部数据是对象可以共享出来的信息,这些信息不会随着系统的运行而改变;外部数据则是在不同运行时被标记了不同的值。


享元模式一般可以分为三个角色,分别为 Flyweight(抽象享元类)、ConcreteFlyweight(具体享元类)和 FlyweightFactory(享元工厂类)。抽象享元类通常是一个接口或抽象类,向外界提供享元对象的内部数据或外部数据;具体享元类是指具体实现内部数据共享的类;享元工厂类则是主要用于创建和管理享元对象的工厂类。


实现享元模式


我们还是通过一个简单的例子来实现一个享元模式:


//抽象享元类
interface Flyweight {
//对外状态对象
void operation(String name);
//对内对象
String getType();
}


//具体享元类
class ConcreteFlyweight implements Flyweight {
private String type;

public ConcreteFlyweight(String type) {
this.type = type;
}

@Override
public void operation(String name) {
System.out.printf("[类型(内在状态)] - [%s] - [名字(外在状态)] - [%s]\n", type, name);
}

@Override
public String getType() {
return type;
}
}


//享元工厂类
class FlyweightFactory {
private static final Map<String, Flyweight> FLYWEIGHT_MAP = new HashMap<>();//享元池,用来存储享元对象

public static Flyweight getFlyweight(String type) {
if (FLYWEIGHT_MAP.containsKey(type)) {//如果在享元池中存在对象,则直接获取
return FLYWEIGHT_MAP.get(type);
} else {//在响应池不存在,则新创建对象,并放入到享元池
ConcreteFlyweight flyweight = new ConcreteFlyweight(type);
FLYWEIGHT_MAP.put(type, flyweight);
return flyweight;
}
}
}


public class Client {

public static void main(String[] args) {
Flyweight fw0 = FlyweightFactory.getFlyweight("a");
Flyweight fw1 = FlyweightFactory.getFlyweight("b");
Flyweight fw2 = FlyweightFactory.getFlyweight("a");
Flyweight fw3 = FlyweightFactory.getFlyweight("b");
fw1.operation("abc");
System.out.printf("[结果(对象对比)] - [%s]\n", fw0 == fw2);
System.out.printf("[结果(内在状态)] - [%s]\n", fw1.getType());
}
}


输出结果:


[类型(内在状态)] - [b] - [名字(外在状态)] - [abc]
[结果(对象对比)] - [true]
[结果(内在状态)] - [b]


观察以上代码运行结果,我们可以发现:如果对象已经存在于享元池中,则不会再创建该对象了,而是共用享元池中内部数据一致的对象。这样就减少了对象的创建,同时也节省了同样内部数据的对象所占用的内存空间。


适用场景


享元模式在实际开发中的应用也非常广泛。例如Java的String字符串,在一些字符串常量中,会共享常量池中字符串对象,从而减少重复创建相同值对象,占用内存空间。代码如下:


 String s1 = "hello";
String s2 = "hello";
System.out.println(s1==s2);//true


还有,在日常开发中的应用。例如,池化技术中的线程池就是享元模式的一种实现;将商品存储在应用服务的缓存中,那么每当用户获取商品信息时,则不需要每次都从redis缓存或者数据库中获取商品信息,并在内存中重复创建商品信息了。


总结


原型模式和享元模式,在开源框架,和实际开发中,应用都十分广泛。


在不得已需要重复创建大量同一对象时,我们可以使用原型模式,通过clone方法复制对象,这种方式比用new和序列化创建对象的效率要高;在创建对象时,如果我们可以共用对象的内部数据,那么通过享元模式共享相同的内部数据的对象,就可以减少对象的创建,实现系统调优。


本文由mdnic

e多平台发布

收起阅读 »

Java字符串常量池和intern方法解析

Java字符串常量池和intern方法解析 这篇文章,来讨论一下Java中的字符串常量池以及Intern方法.这里我们主要讨论的是jdk1.7,jdk1.8版本的实现. 字符串常量池 在日常开发中,我们使用字符串非常的频繁,我们经常会写下类似如下的代码: S...
继续阅读 »

Java字符串常量池和intern方法解析


这篇文章,来讨论一下Java中的字符串常量池以及Intern方法.这里我们主要讨论的是jdk1.7,jdk1.8版本的实现.


字符串常量池


在日常开发中,我们使用字符串非常的频繁,我们经常会写下类似如下的代码:



  • String s = "abc";

  • String str = s + "def";


通常,我们一般不会这么写:String s = new String("jkl"),但其实这么写和上面的写法还是有很多区别的.


先思考一个问题,为什么要有字符串常量池这种概念?原因是字符串常量既然是不变的,那么完全就可以复用它,而不用再重新去浪费空间存储一个完全相同的字符串.字符串常量池是用于存放字符串常量的地方,在Java7和Java8中字符串常量池是堆的一部分.


假如我们有如下代码:


String s = "abc";
String s1 = s + "def";    
String s2 = new String("abc");
String s3 = new String("def");

那么从内存分配的角度上看,最终会有哪些字符串生成呢,首先我先给出一张图来代表最终的结论,然后再分析一下具体的原因:


image.png


现在来依次分析上面的代码的执行流程:




  1. 执行String s = "abc",此时遇到"abc"这个字符串常量,会在字符串常量池中完成分配,并且会将引用赋值给s,因此这条语句会在字符串常量池中分配一个"abc".(这里其实没有空格,是因为生成文章时出现了空格,下文中如果出现同样情况,请忽略空格)




  2. 执行String s1 = s + "def",其实这个语句看似简单,实则另有玄机,它其实最终编译而成的代码是这样的:String s1 = new StringBuilder("abc").append("def").toString().首先在这个语句中有两个字符串常量:"abc"和"def",所以在字符串常量池中应该放置"abc"和"def",但是上个步骤已经有"abc"了,所以只会放置"def".另外,new StringBuilder("abc")这个语句相当于在堆上分配了一个对象,如果是new出来的,是在堆上分配字符串,是无法共享字符串常量池里面的字符串的,也就是说分配到堆上的字符串都会有新的内存空间. 最后toString()也是在堆中分配对象(可以从源码中看到这个动作),最终相当于执行了new String("abcdef");所以总结起来,这条语句分析起来还是挺麻烦的,它分配了以下对象:



    • 在字符串常量池分配"abc",但本来就有一个"abc"了,所以不需要分配

    • 在字符串常量池中分配“def"

    • 在堆中分配了"abc"

    • 在堆中分配了"abcdef"




  3. 执行String s2 = new String("abc").首先有个字符串常量"abc",需要分配到字符串常量池,但是字符串常量池中已经有"abc"了,所以无需分配.因此new String("abc")最终在堆上分配了一个"abc".所以总结起来就是,在堆中分配了一个"abc"




  4. 执行String s3 = new String("def");.首先有个字符串常量"def",需要分配到字符串常量池,但是字符串常量池中已经有"def"了,所以无需分配.因此new String("def")最终在堆上分配了一个"def".所以总结起来就是,在堆中分配了一个"def"。




总结起来,全部语句执行后分配的对象如下:



  • 在堆中分配了两个"abc",一个"abcdef",一个"def"

  • 在字符串常量池中分配了一个"abc",一个"def"


也就是图中所表示的这些对象,如果明白了对象是如何分配的,我们就可以分析以下代码的结果:


String s = "abc";
String s1 = s + "def";    
String s2 = new String("abc");
String s3 = new String("def");
String s4 = "abcdef";
String s5 = "abc";
System.out.println(s == s2); //false 前者引用的对象在字符串常量池 后者在堆上
System.out.println(s == s5);; //true 都引用了字符串常量池中的"abc"
System.out.println(s1 == s4); //false 前者引用的对象在字符串常量池,后者在堆上

intern方法


在字符串对象中,有一个intern方法.在jdk1.7,jdk1.8中,它的定义是如果调用这个方法时,在字符串常量池中有对应的字符串,那么返回字符串常量池中的引用,否则返回调用时相应对象的引用,也就是说intern方法在jdk1.7,jdk1.8中只会复用某个字符串的引用,这个引用可以是对堆内存中字符串中的引用,也可能是对字符串常量池中字符串的引用.这里通过一个例子来说明,假如我们有下面这段代码:


String str = new String("abc");
String str2 = str.intern();
String str3 = new StringBuilder("abc").append("def").toString();
String str4 = str3.intern();
System.out.println(str == str2);
System.out.println(str3 == str4);  

那么str2和str以及str3和str4是否相等呢?如果理解了上面对字符串常量池的分析,那么我们可以明白在这段代码中,字符串在内存中是这么分配的:



  • 在堆中分配两个"abc",一个“abcdef"

  • 在字符串常量池中分配一个"def",一个"abc"



  1. 当执行String str2 = str.intern();时,会先从字符串常量池中寻找是否有对应的字符串,此时在字符串常量池中有一个"abc",那么str2就指向字符串常量池中的"abc",而str是new出来的,指向的是堆中的"abc",所以str不等于str2;



  1. 当执行String str4 = str3.intern();会先从字符串常量池中寻找"abcdef",此时字符串常量池中并没有"abcdef",因此str4会指向堆中的"abcdef",因此str3等于str4,我们会发现一个有意思的地方:如果将第三句改成String str3 = new StringBuilder("abcdef").toString();,也就是把append后面的字符串和前面的字符串做一个拼接,那么结果就会变成str3不等于str4.所以这两种写法的区别还是挺大的.


要注意的是,在jdk1.6中intern的定义是如果字符串常量池中没有对应的字符串,那么就在字符串常量池中创建一个字符串,然后返回字符串常量池中的引用,也就是说在jdk1.6中,intern方法返回的对象始终都是指向字符串常量池的.如果上面的代码在jdk1.6中运行,那么就会得到两个false,原因如下:



  1. 当执行String str2 = str.intern();时,会先从字符串常量池中寻找是否有对应的字符串,此时在字符串常量池中有一个"abc",那么str2就指向字符串常量池中的"abc",而str是new出来的,指向的是堆中的"abc",所以str不等于str2;



  1. 当执行String str4 = str3.intern();会先从字符串常量池中寻找"abcdef",此时字符串常量池中并没有"abcdef",因此执行intern方法会在字符串常量池中分配"abcdef",然后str4最终等于这个字符串的引用,因此str3不等于str4,因为上面的str3指向堆,而str4指向字符串常量池,所以两者一定不会相等.


深入理解JVM虚拟机一书中,就有类似的代码:


String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1 == str1.intern());
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2 == str2.intern());

在jdk1.6中,两个判断都为false.因为str1和str2都指向堆,而intern方法得出来的引用都指向字符串常量池,所以不会相等,和上面叙述的结论是一样的.在jdk1.7中,第一个是true,第二个是false.道理其实也和上述所讲的是一样的,对于第一个语句,最终会在堆上创建一个"计算机软件"的字符串,执行str1.intern()方法时,先在字符串常量池中寻找字符串,但没有找到,所以会直接引用堆上的这个"计算机软件",因此第一个语句会返回true,因为最终都是指向堆.而对于第三个语句,因为和第一个语句差不多,按理说最终比较也应该返回true.但实际上,str2.intern方法执行的时候,在字符串常量池中是可以找到"java"这个字符串的,这是因为在Java初始化环境去加载类的时候(执行main方法之前),已经有一个叫做"java"的字符串进入了字符串常量池,因此str2.intern方法返回的引用是指向字符串常量池的,所以最终判断的结果是false,因为一个指向堆,一个指向字符串常量池.


总结


从上面的分析看来,字符串常量池并不像是那种很简单的概念,要深刻理解字符串常量池,至少需要理解以下几点:



  • 理解字符串会在哪个内存区域存放

  • 理解遇到字符串常量会发生什么

  • 理解new String或者是new StringBuilder产生的对象会在哪里存放

  • 理解字符串拼接操作+最终编译出来的语句是什么样子的

  • 理解toString方法会发生什么


这几点都在本文章中覆盖了,相信理解了这几点之后一定对字符串常量池有一个更深刻的理解.其实这篇文章的编写原因是因为阅读深入理解JVM虚拟机这本书的例子时突然发现作者所说的和我所想的是不一样的,但是书上可能对这方面没有展开叙述,所以我去查了点资料,然后写了一些代码来验证,最终决定写一篇文章来记录一下自己的理解,在编写代码过程中,还发现了一个分析对象内存地址的类库,我放在参考资料中了.


参考资料


http://www.baeldung.com/java-object… 查看java对象内存地址


作者:JL8
来源:juejin.cn/post/7237827233351073849
收起阅读 »

iOS加固保护新思路

之前有写过【如何给iOS APP加固】,但是经过一段时间的思考,我找到了更具有实践性的代码,具体可以看下面。 技术简介 iOS加固保护是基于虚机源码保护技术,针对iOS平台推出的下一代加固产品。可以对iOS APP中的可执行文件进行深度混淆、加固,并使用独创的...
继续阅读 »

之前有写过【如何给iOS APP加固】,但是经过一段时间的思考,我找到了更具有实践性的代码,具体可以看下面。


技术简介


iOS加固保护是基于虚机源码保护技术,针对iOS平台推出的下一代加固产品。可以对iOS APP中的可执行文件进行深度混淆、加固,并使用独创的虚拟机技术对代码进行加密保护,使用任何工具都无法直接进行逆向、破解。对APP进行完整性保护,防止应用程序中的代码及资源文件被恶意篡改。


技术功能


目前iOS加固主要包含逻辑混淆、字符串加密、代码虚拟化、防调试、防篡改以及完整性保护这三大类功能。通过对下面的代码片段进行保护来展示各个功能的效果:


- (void) test {
if (_flag) {
test_string(@"Hello, World!",@"你好,世界!","Hello, World!");
} else {
dispatch_async(dispatch_get_mian_queue(), ^{
do_something( );
});
}
int i=0;
while (i++ < 100) {
sleep(1);
do_something( );
}
}

将代码编译后拖入IDA Pro中进行分析,可以得到这样的控制流图,只有6个代码块,且跳转逻辑简单,可以很容易地判断出if-else以及while的特征:


1.png


将其反编译为伪代码,代码逻辑及源代码中使用的字符串均清晰可见,与源代码结构基本一致,效果如下


void __cdecl -[ViewController test](ViewController *self, SEL a2)
{
signed int v2; // w19
__int64 v3; //x0

if ( self->_flag )
sub_100006534 ( CFSTR("Hello, World!"),CFSTR("你好,世界!"), "Hello, World!" );
else
dispatch_async ( &_dispatch_main_q, &off_10000C308 );
v2 = 100;
do
{
v3 = sleep( 1u );
sub_100006584( v3 );
--v2;
}
while ( v2 );
}

1 代码逻辑混淆


通过将原始代码的控制流进行切分、打乱、隐藏,或在函数中插入花指令来实现对代码的混淆,使代码逻辑复杂化但不影响原始代码逻辑。


对代码进行逻辑混淆保护后,该函数的控制流图会变得十分复杂,且函数中穿插了大量不会被执行到的无用代码块,以及相互间的逻辑跳转,逆向分析的难度大大增强:


2.png
若开启防反编译功能,则控制流图会被完全隐藏,只剩下一个代码块,且无法反编译出有效代码(如下图所示),这对于对抗逆向分析工具来说非常有效,包括但不限于(IDA Pro, Hopper Disassembler, Binary Ninja, GHIDRA等)


3.png


void __cdecl -[ViewController test](ViewController *self , SEL a2)
{
JUMPOUT (__CS__, sub_100005A94(6LL, a2));
}

2 字符串加密


把所有静态常量字符串(支持C/C++/OC/Swift字符串)进行加密,运行时解密,防止攻击者通过字符串进行静态分析,猜测代码逻辑。


对代码中的字符串进行加密之后,所有的字符串都被替换为加密的引用,任何反编译手段均无法看到明文的字符串。你好,世界!,Hello, World!等字符串原本可以被轻易的反编译出来,但保护之后已经看不到了:


void __cdecl -[ViewController test](ViewController *self, SEL a2)
{
__int64 v2; //x0
__int64 v3; //x0
signed int v4; // w19
__int64 v5; //x0

if ( self->_flag )
{
v2 = sub_100008288();
v3 = sub_10000082E8(v2);
sub_100008228(v3);
sub_100007FB0( &stru_100010368, &stru_10001038, &unk_100011344);
}
else
dispatch_async ( &_dispatch_main_q, &off_100010308 );
}
v4 = 100;
do
{
v5 = sleep( 1u );
sub_100008004( v5 );
--v4;
}
while ( v4 );
}

3 代码虚拟化


将原始代码编译为动态的DX-VM虚拟机指令,运行在DX虚拟机之上,无法被反编译回可读的源代码,任何工具均无法直接反编译虚拟机指令。


采用代码虚拟化保护后,对函数进行反编译将无法看到任何与原代码相似的内容,函数体中只有对虚拟机子系统的调用:


void __cdecl -[ViewController test](ViewController *self, SEL a2)
{
SEL v2; // x19
__int64 v3; // x21

v2 = a2;
v3 = sub_10000C1EC;
(( void (* )(void))sub_10000C1D8)();
sub_10000C1D8( v3, v2);
sub_10000C180( v3, 17LL);
}

4 防调试


防止通过调试手段分析应用逻辑,开启防调试功能后,App进程可以有效地阻止各类调试器的调试行为:


4.png


5 防篡改,完整性保护


防止应用程序中的代码及资源文件被恶意篡改,杜绝盗版或植入广告等二次打包行为。


结语


以上内容是不限制ios版本的。不过,对于App类型,仅支持.xcarchive格式,不支持.ipa格式。并且,有以下注意事项:



  • Build Setting 中 Enable Bitcode 设置为 YES

  • 使用 Archive 模式编译以确保Bitcode成功启用,否则编译出的文件将只包含bitcode-marker

  • 若无法开启Bitcode,可使用辅助工具进行处理,详见五、iOS加固辅助工具 -> 5.2 启用 bitcode

  • 压缩后体积在 2048M 以内


以上是根据顶象的加固产品操作指南出具的流程,如需要更详细的说明,可以自行前往用户中心~


作者:昀和
来源:juejin.cn/post/7236634496765509692
收起阅读 »

夯实基础:彻底搞懂零拷贝

零拷贝 零拷贝我相信大家都听说过,Netty 也用到了零拷贝来大幅提升网络吞吐量,但是大多数人对零拷贝中的原理和过程却很难讲清楚,接下来我会给大家详细讲解这方面的内容。 首先,我们看看,没有零拷贝的时候,应用程序是如何从服务器的磁盘读数据并通过网卡发送到网络的...
继续阅读 »

零拷贝


零拷贝我相信大家都听说过,Netty 也用到了零拷贝来大幅提升网络吞吐量,但是大多数人对零拷贝中的原理和过程却很难讲清楚,接下来我会给大家详细讲解这方面的内容。
首先,我们看看,没有零拷贝的时候,应用程序是如何从服务器的磁盘读数据并通过网卡发送到网络的。


无零拷贝时,数据的发送流程


非DMA非零拷贝 (1).png


大家可以通过上图看到,应用程序把磁盘数据发送到网络的过程中会发生4次用户态和内核态之间的切换,同时会有4次数据拷贝。过程如下:



  1. 应用进程向系统申请读磁盘的数据,这时候程序从用户态切换成内核态。

  2. 系统也就是 linux 系统得知要读数据会通知 DMA 模块要读数据,这时 DMA 从磁盘拉取数据写到系统内存中。

  3. 系统收到 DMA 拷贝的数据后把数据拷贝到应用内存中,同时把程序从内核态变为用户态。

  4. 应用内存拿到从应用内存拿到数据后,会把数据拷贝到系统的 Socket 缓存,然后程序从用户态切换为内核态。

  5. 系统再次调用 DMA 模块,DMA 模块把 Socket 缓存的数据拷贝到网卡,从而完成数据的发送,最后程序从内核态切换为用户态。


如何提升文件传输的效率?


我们程序的目的是把磁盘数据发送到网络中,所以数据在用户内存和系统内存直接的拷贝根本没有意义,同时与数据拷贝同时进行的用户态和内核态之间的切换也没有意义。而上述常规方法出现了4次用户态和内核态之间的切换,以及4次数据拷贝。我们优化的方向无非就是减少用户态和内核态之间的切换次数,以及减少数据拷贝的次数



为什么要在用户态和内核态之间做切换?


因为用户态的进程没有访问磁盘上数据的权限,也没有把数据从网卡发送到网络的权限。只有内核态也就是操作系统才有操作硬件的权限,所以需要系统向用户进程提供相应的接口函数来实现数据的读写。
这里涉及了两个系统接口调用分别是:


read(file, tmp_buf, len);


write(socket, tmp_buf, len);



于是,零拷贝技术应运而生,系统为我们上层应用提供的零拷贝方法方法有下列两种:



  • mmap + write

  • sendfile


MMAP + write


这个方法主要是用 MMAP 替换了 read。
对应的系统方法为:
buf = mmap(file,length)
write(socket,buf,length)
所谓的 MMAP, 其实就是系统内存某段空间和用户内存某段空间保持一致,也就是说应用程序能通过访问用户内存访问系统内存。所以,读取数据的时候,不用通过把系统内存的数据拷贝到用户内存中再读取,而是直接从用户内存读出,这样就减少了一次拷贝。
我们还是先看图:


零拷贝之mmap+write.png
给大家简述一下步骤:



  1. 应用进程通过接口调用系统接口 MMAP,并且进程从用户态切换为内核态。

  2. 系统收到 MMAP 的调用后用 DMA 把数据从磁盘拷贝到系统内存,这时是第1次数据拷贝。由于这段数据在系统内存和应用内存是共享的,数据自然就到了应用内存中,这时程序从内核态切换为用户态。

  3. 程序从应用内存得到数据后,会调用 write 系统接口,这时第2次拷贝开始,具体是把数据拷贝到 Socket 缓存,而且用户态切换为内核态。

  4. 系统通过 DMA 把数据从 Socket 缓存拷贝到网卡。

  5. 最后,进程从内核态切换为用户态。


这样做到收益是减少了一次拷贝但是用户态和内核态仍然是4次切换


sendfile


这个系统方法可以实现系统内部不同设备之间的拷贝。具体逻辑我们还是先上图:


零拷贝之sendfile.png
大家可以看到,使用 sendfile 主要的收益是避免了数据在应用内存和系统内存或socket缓存直接的拷贝,同时这样会避免用户态和内核态之间的切换。
基本原理分为下面几步:



  1. 应用进程调用系统接口 sendfile,进程从用户态切换完内核态。

  2. 系统接收到 sendfile 指令后,通过 DMA 从磁盘把数据拷贝到系统内存。

  3. 数据到了系统内存后,CPU 会把数据从系统内存拷贝到 socket 缓存中。

  4. 通过 DMA 拷贝到网卡中。

  5. 最后,进程从内核态切换为用户态。


但是,这还不是零拷贝,所谓的零拷贝不会在内存层面去拷贝数据,也就是系统内存拷贝到 socket 缓存,下面给大家介绍一下真正的零拷贝。


真正的零拷贝


真正的零拷贝是基于 sendfile,当网卡支持 SG-DMA 时,系统内存的数据可以直接拷贝到网卡。如果这样实现的话,执行流程就会更简单,如下图所示:


真正的零拷贝.png


基本原理分为下面几步:



  1. 应用进程调用系统接口 sendfile,进程从用户态切换完内核态。

  2. 系统接收到 sendfile 指令后,通过 DMA 从磁盘把数据拷贝到系统内存。

  3. 数据到了系统内存后,CPU 会把文件描述符和数据长度返回到 socket 缓存中(注意这里没有拷贝数据)。

  4. 通过 SG-DMA 把数据从系统内存拷贝到网卡中。

  5. 最后,进程从内核态切换为用户态。


零拷贝在用户态和内核态之间的切换是2次,拷贝是2次,大大减少了切换次数和拷贝次数,而且全程没有 CPU 参与数据的拷贝。


零拷贝的重要帮手 PageCache


大家知道,从缓存中读取数据的速度一定要比从磁盘中读取数据的速度要快的多。那么,有没有一种技术能让我们从内存中读取磁盘的数据呢?
PageCache 的目的就是让磁盘的数据缓存化到系统内存。那么,PageCache会选取哪些磁盘数据作为缓存呢?具体步骤是这样的:



  • 首先,当用户进程要求方法哪些磁盘数据时,DMA会把这部分磁盘数据缓存到系统内存里,那么问题又来了,磁盘空间明显比内存空间大的多,不可能不限制的把磁盘的数据拷贝到系统内存中,所以必须要有一个淘汰机制来把访问概率低的内存数据删除掉。

  • 那么,用什么方法淘汰内存的数据呢?答案是 LRU (Least Recently Used),最近最少使用。原理的认为最近很少使用的数据,以后使用到的概率也会很低。


那么,这样就够了吗?我们设想一个场景,我们在操作数据的时候有没有顺序读写的场景?比如说消息队列的顺序读取,我们消费了 id 为1的 message 后,也会消费 id 为2的 message。而消息队列的文件一般是顺序存储的,如果我们事先把 id 为2的 message 读出到系统内存中,那么就会大大加快用户进程读取数据的速度。


这样的功能在现代操作系统中叫预读,也就是说如果你读某个文件的了32 KB 的字节,虽然你这次读取的仅仅是 0 ~ 32 KB 的字节,但系统内核会自动把其后面的 32~64 KB 也读取到 PageCache,如果在 32~64 KB 淘汰出 PageCache 前,用户进程读取到它了,那么就会大大加快数据的读取速度,因为我们是直接在缓存上读出的数据,而不是从磁盘中读取。


作者:肖恩Sean
来源:juejin.cn/post/7236988255073370167
收起阅读 »

聊聊「短信」渠道的设计与实现

有多久,没有发过短信了? 一、背景简介 在常规的分布式架构下,「消息中心」的服务里通常会集成「短信」的渠道,作为信息触达的重要手段,其他常用的手段还包括:「某微」、「某钉」、「邮件」等方式; 对于《消息中心》的设计和实现来说,在前面已经详细的总结过,本文重点...
继续阅读 »

有多久,没有发过短信了?



一、背景简介


在常规的分布式架构下,「消息中心」的服务里通常会集成「短信」的渠道,作为信息触达的重要手段,其他常用的手段还包括:「某微」、「某钉」、「邮件」等方式;


对于《消息中心》的设计和实现来说,在前面已经详细的总结过,本文重点来聊聊消息中心的短信渠道的方式;



短信在实现的逻辑上,也遵循消息中心的基础设计,即消息生产之后,通过消息中心进行投递和消费,属于典型的生产消费模型;


二、渠道方对接


在大部分的系统中,短信功能的实现都依赖第三方的短信推送,之前总结过《三方对接》的经验,这里不再赘述;


但是与常规第三方对接不同的是,短信的渠道通常会对接多个,从而应对各种消息投递的场景,比如常见的「验证码」场景,「通知提醒」场景,「营销推广」场景;



这里需要考虑的核心因素有好几个,比如成本问题,短信平台的稳定性,时效性,触达率,并发能力,需要进行不同场景的综合考量;


验证码:该场景通常是用户和产品的关键交互环节,十分依赖短信的时效性和稳定性,如果出问题直接影响用户体验;


通知提醒:该场景同样与业务联系密切,但是相对来说对短信触达的时效性依赖并不高,只要在一定的时间范围内最终触达用户即可;


营销推广:该场景的数据量比较大,并且从实际效果来看,具有很大的不确定性,会对短信渠道的成本和并发能力重点考量;


三、短信渠道


1、流程设计


从整体上来看短信的实现流程,可以分为三段:「1」短信需求的业务场景,「2」消息中心的短信集成能力,「3」对接的第三方短信渠道;



需求场景:在产品体系中,需要用到短信的场景很多,不过最主要的还是对用户方的信息触达,比如身份验证,通知,营销等,其次则是对内的重要消息通知;


消息中心:提供消息发送的统一接口方法,不同业务场景下的消息提交到消息中心,进行统一维护管理,并根据消息的来源和去向,适配相应的推送逻辑,短信只是作为其中的一种方式;


渠道对接:根据具体的需求场景来定,如果只有验证码的对接需求,可以只集成一个渠道,或者从成本方面统筹考虑,对接多个第三方短信渠道,建议设计时考虑一定的可扩展;


2、核心逻辑


单从短信这种方式的管理来看,逻辑复杂度并不算很高,但是很依赖细节的处理,很多不注意的细微点都可能导致推送失败的情况;



实际在整个逻辑中,除了「验证码」功能有时效性依赖之外,其他场景的短信触达都可以选择「MQ队列」进行解耦,在消息中心的设计上,也具备很高的流程复用性,图中只是重点描述短信场景;


3、使用场景


3.1 验证码


对于「短信」功能中的「验证码」场景来说,个人感觉在常规的应用中是最复杂的,这可能会涉及到「账户」和相关「业务」的集成问题;


验证码获取


这个流程相对来说路径还比较简短,只要完成手机号的校验后,按照短信推送逻辑正常执行即可;



这里需要说明的是,为了确保系统的安全性,通常会设定验证码的时效性,并且只能使用一次,但是偶尔可能因为延时问题,引起用户多次申请验证码,基于缓存可以很好的管理这种场景的数据结构;


验证码消费


验证码的使用是非常简单的,现在很多产品在设计上,都弱化了登录和注册的概念,只要通过验证码机制,会默认的新建帐户和执行相关业务流程;



无论是何种业务场景下的「验证码」依赖,在处理流程时都要先校验其「验证码」的正确与否,才能判断流程是否向下执行,在部分敏感的场景中,还会限制验证码的错误次数,防止出现账户安全问题;


3.2 短信触达


无论是「通知提醒」还是「营销推广」,其本质上是追求信息的最终触达即可,大部分短信运营商都可以提供这种能力,只是系统内部的处理方式有很大差异;



在部分业务流程中,需要向用户投递短信消息,在营销推广的需求中,更多的是批量发送短信,部分需求其内部逻辑上,还可能存在一个转化率统计的问题,需要监控相关短信的交互状态;


四、模型设计


由于短信是集成在消息中心的服务中,其相关的数据结构模型都是复用消息管理的,具体细节描述,参考《消息中心》的内容即可,此处不赘述;



从技术角度来看的话,涉及经典的生产消费模型,第三方平台对接,任务和状态机管理等,消息中心作为分布式架构的基础服务,在设计上还要考虑一定的复用性。


五、参考源码


编程文档:
https://gitee.com/cicadasmile/butte-java-note

应用仓库:
https:/
/gitee.com/cicadasmile/butte-flyer-parent

作者:知了一笑
来源:juejin.cn/post/7237082256480649271
收起阅读 »

iOS H5页面秒加载预研

背景 原生架构+H5页面的组合是很常见的项目开发模式了,H5的优势是跨平台、开发快、迭代快、热更新,很多大厂的App大部分业务代码都是H5来实现的,众所周知H5页面的体验是比原生差的,特别是网络环境差的时候,如果首屏页面是H5的话,那酸爽遇见过的都懂 白屏警告...
继续阅读 »

背景


原生架构+H5页面的组合是很常见的项目开发模式了,H5的优势是跨平台、开发快、迭代快、热更新,很多大厂的App大部分业务代码都是H5来实现的,众所周知H5页面的体验是比原生差的,特别是网络环境差的时候,如果首屏页面是H5的话,那酸爽遇见过的都懂


白屏警告




白屏主要就是下载页面资源失败或者慢导致的,下面可以看下H5加载过程


H5加载过程


这部分内容其实网上已经很多了,如下


初始化 webview -> 请求页面 -> 下载数据 -> 解析HTML -> 请求 js/css 资源 -> dom 渲染 -> 解析 JS 执行 -> JS 请求数据 -> 解析渲染 -> 下载渲染图片


主要要优化的就是下载到渲染这段时间,最简单的方案就是把整个web包放在App本地,通过本地路径去加载,做到这一步,再配合上H5页面做加载中占位图,基本上就不会有白屏的情况了,而且速度也有了很明显的提升


可参考以下测试数据(网络地址加载和本地路径加载)


WiFi网络打开H5页面100次耗时:(代表网络优秀时)


本地加载平均执行耗时:0.28秒


网络加载平均执行耗时:0.58秒


4G/5G手机网络打开H5页面100次耗时:(代表网络良好时)


本地加载平均执行耗时:0.43秒


网络加载平均执行耗时:2.09秒


3G手机网络打开H5页面100次耗时:(代表网络一般或者较差时)


本地加载平均执行耗时:1.48秒


网络加载平均执行耗时:19.09秒


ok,恭喜你,H5离线秒加载功能优化完毕,这么简单?


IMG_4941.JPG


进入正题


通过上述实现本地加载后速度虽然是快了不少,但H5页面的数据显示还是需要走接口请求的,如果网络环境不好的话,也是会很慢的,这个问题怎么解决呢?


下面开始上绝活,这里引入一个概念 WKURLSchemeHandler
在iOS11及以上系统中,可以通过WKURLSchemeHandler自定义拦截请求,有什么用?简单说就是可以利用原生的数据缓存去加载H5页面,可以无视网络环境的影响,在拦截到H5页面的网络请求后先判断本地是否有缓存,有缓存的话可以直接拼接一个成功的返回,没有的话直接放开继续走网络请求,成功后再缓存数据,对H5来说也是无侵入无感知的。


首先自定义一个SchemeHandler类,遵守WKURLSchemeHandler协议,实现协议方法


@protocol WKURLSchemeHandler <NSObject>

/*! @abstract Notifies your app to start loading the data for a particular resource
represented by the URL scheme handler task.
@param webView The web view invoking the method.
@param urlSchemeTask The task that your app should start loading data for.
*/

- (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;

/*! @abstract Notifies your app to stop handling a URL scheme handler task.
@param webView The web view invoking the method.
@param urlSchemeTask The task that your app should stop handling.
@discussion After your app is told to stop loading data for a URL scheme handler task
it must not perform any callbacks for that task.
An exception will be thrown if any callbacks are made on the URL scheme handler task
after your app has been told to stop loading for it.
*/

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)urlSchemeTask;

@end

直接上代码


#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface YXWKURLSchemeHandler : NSObject<WKURLSchemeHandler>

@end

NS_ASSUME_NONNULL_END

#import "CMWKURLSchemeHandler.h"
#import "YXNetworkManager.h"
#import <SDWebImage/SDWebImageManager.h>
#import <SDWebImage/SDImageCache.h>

@implementation YXWKURLSchemeHandler

- (void) webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
NSURLRequest * request = urlSchemeTask.request;
NSString * urlStr = request.URL.absoluteString;
NSString * method = urlSchemeTask.request.HTTPMethod;
NSData * bodyData = urlSchemeTask.request.HTTPBody;
NSDictionary * bodyDict = nil;

if (bodyData) {
bodyDict = [NSJSONSerialization JSONObjectWithData:bodyData options:kNilOptions error:nil];
}

NSLog(@"拦截urlStr=%@", urlStr);
NSLog(@"拦截method=%@", method);
NSLog(@"拦截bodyData=%@", bodyData);

// 图片加载
if ([urlStr hasSuffix:@".jpg"] || [urlStr hasSuffix:@".png"] || [urlStr hasSuffix:@".gif"]) {
SDImageCache * imageCache = [SDImageCache sharedImageCache];
NSString * cacheKey = [[SDWebImageManager sharedManager] cacheKeyForURL:request.URL];
BOOL isExist = [imageCache diskImageDataExistsWithKey:cacheKey];
if (isExist) {
NSData * imgData = [[SDImageCache sharedImageCache] diskImageDataForKey:cacheKey];
[urlSchemeTask didReceiveResponse:[[NSURLResponse alloc] initWithURL:request.URL MIMEType:[self createMIMETypeForExtension:[urlStr pathExtension]] expectedContentLength:-1 textEncodingName:nil]];
[urlSchemeTask didReceiveData:imgData];
[urlSchemeTask didFinish];
return;
}
[[SDWebImageManager sharedManager] loadImageWithURL:request.URL options:SDWebImageRetryFailed progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {}];
}

// 网络请求
NSData * cachedData = (NSData *)[YXNetworkCache httpCacheForURL:urlStr parameters:bodyDict];
if (cachedData) {
NSHTTPURLResponse * response = [self createHTTPURLResponseForRequest:urlSchemeTask.request];
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveData:cachedData];
[urlSchemeTask didFinish];
} else {
NSURLSessionDataTask * dataTask = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
[urlSchemeTask didFailWithError:error];
} else {
[YXNetworkCache setHttpCache:data URL:urlStr parameters:bodyDict];
[urlSchemeTask didReceiveResponse:response];
[urlSchemeTask didReceiveData:data];
[urlSchemeTask didFinish];
}
}];
[dataTask resume];
}
} /* webView */

- (NSHTTPURLResponse *) createHTTPURLResponseForRequest:(NSURLRequest *)request {
// Determine the content type based on the request
NSString * contentType;

if ([request.URL.pathExtension isEqualToString:@"css"]) {
contentType = @"text/css";
} else if ([[request valueForHTTPHeaderField:@"Accept"] isEqualToString:@"application/javascript"]) {
contentType = @"application/javascript;charset=UTF-8";
} else {
contentType = @"text/html;charset=UTF-8"; // default content type
}

// Create the HTTP URL response with the dynamic content type
NSHTTPURLResponse * response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{ @"Content-Type": contentType }];

return response;
}

- (NSString *) createMIMETypeForExtension:(NSString *)extension {
if (!extension || extension.length == 0) {
return @"";
}

NSDictionary * MIMEDict = @{
@"txt" : @"text/plain",
@"html" : @"text/html",
@"htm" : @"text/html",
@"css" : @"text/css",
@"js" : @"application/javascript",
@"json" : @"application/json",
@"xml" : @"application/xml",
@"swf" : @"application/x-shockwave-flash",
@"flv" : @"video/x-flv",
@"png" : @"image/png",
@"jpg" : @"image/jpeg",
@"jpeg" : @"image/jpeg",
@"gif" : @"image/gif",
@"bmp" : @"image/bmp",
@"ico" : @"image/vnd.microsoft.icon",
@"woff" : @"application/x-font-woff",
@"woff2": @"application/x-font-woff",
@"ttf" : @"application/x-font-ttf",
@"otf" : @"application/x-font-opentype"
};

NSString * MIMEType = MIMEDict[extension.lowercaseString];
if (!MIMEType) {
return @"";
}

return MIMEType;
} /* MIMETypeForExtension */

- (void) webView:(WKWebView *)webView stopURLSchemeTask:(id<WKURLSchemeTask>)urlSchemeTask {
NSLog(@"stop = %@", urlSchemeTask);
}

@end

重点来了,需要hook WKWebview 的 handlesURLScheme 方法来支持 http 和 https 请求的代理


直接上代码


#import "WKWebView+SchemeHandle.h"
#import <objc/runtime.h>

@implementation WKWebView (SchemeHandle)

+ (void) load {
static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{
Method originalMethod1 = class_getClassMethod(self, @selector(handlesURLScheme:));
Method swizzledMethod1 = class_getClassMethod(self, @selector(cmHandlesURLScheme:));
method_exchangeImplementations(originalMethod1, swizzledMethod1);
});
}

+ (BOOL) cmHandlesURLScheme:(NSString *)urlScheme {
if ([urlScheme isEqualToString:@"http"] || [urlScheme isEqualToString:@"https"]) {
return NO;
} else {
return [self handlesURLScheme:urlScheme];
}
}

@end

这里会有一个疑问了?为什么要这么用呢,这里主要是为了满足对H5端无侵入、无感知的要求
如果不hook http和https的话,就需要在H5端修改代码了,把scheme修改成自定义的customScheme,全部都要改,而且对安卓还不适用,所以别这么搞,信我!!!


老老实实hook,一步到位


如何使用


首先是WKWebView的初始化,直接上代码


- (WKWebView *) wkWebView {
if (!_wkWebView) {
WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc] init];
// 允许跨域访问
[config setValue:@(true) forKey:@"allowUniversalAccessFromFileURLs"];

// 自定义HTTPS请求拦截
YXWKURLSchemeHandler * handler = [YXWKURLSchemeHandler new];
[config setURLSchemeHandler:handler forURLScheme:@"https"];

_wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth, kScreenHeight) configuration:config];
_wkWebView.navigationDelegate = self;
}
return _wkWebView;
} /* wkWebView */

NSString * htmlPath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html" inDirectory:@"H5"];
NSURL * fileURL = [NSURL fileURLWithPath:htmlPath];
NSURLRequest * request = [NSURLRequest requestWithURL:url];
[self.wkWebView loadRequest:request];

ok,到了这一步基本上就能看到效果了,H5页面的接口请求和图片加载都会拦截到,直接使用原生的缓存数据,忽略网络环境的影响实现秒加载了


可参考以下测试数据(自定义WKURLSchemeHandler拦截和不拦截)


3G手机网络打开H5页面100次耗时:(代表网络一般或者较差时)

Figure_1.png


拦截后加载平均执行耗时:0.24秒


不拦截加载平均执行耗时:1.09秒


可以发现通过自定义WKURLSchemeHandler拦截后,加载速度非常平稳根本不受网络的影响,不拦截的话虽然整体加载速度并不算太慢,但是随网络波动比较明显。


不足


这套方案也还是有一些缺点的,比如



  1. App打包的时候需要预嵌入web模块的数据,会导致App包的大小增加(需要尽量缩小web模块的包大小,特别是资源文件的优化)

  2. App需要设计一套web模块包的更新机制,同时需要设计一个web包上传发布平台,后续版本管理更新比之前直接上传服务器替换相对麻烦一些

  3. 需要更新时下载全量web模块包会略大,也可以考虑用BSDiff差分算法来做增量更新解决,但是会增加程序复杂度


总结


综上就是本次H5页面秒加载全部的预研过程了,总的来说,基本上可以满足秒加载的需求。没有完美的解决方案,只有合适的方案,有舍有得,根据公司项目情况来。


作者:风轻云淡的搬砖
来源:juejin.cn/post/7236281103221096505
收起阅读 »

详解越权漏洞

1.1. 漏洞原理 越权漏洞是指应用程序未对当前用户操作的身份权限进行严格校验,导致用户可以操作超出自己管理权限范围的功能,从而操作一些非该用户可以操作的行为。简单来说,就是攻击者可以做一些本来不该他们做的事情(增删改查)。 1.2. 漏洞分类 主要分为 水...
继续阅读 »

1.1. 漏洞原理


越权漏洞是指应用程序未对当前用户操作的身份权限进行严格校验,导致用户可以操作超出自己管理权限范围的功能,从而操作一些非该用户可以操作的行为。简单来说,就是攻击者可以做一些本来不该他们做的事情(增删改查)


IDOR


1.2. 漏洞分类


主要分为 水平越权垂直越权 两大类


1.2.1. 水平越权


发生在具有相同权限级别的用户之间。攻击者通过利用这些漏洞,访问其他用户拥有的资源或执行与其权限级别不符的操作。


1.2.2. 垂直越权


发生在具有多个权限级别的系统中。攻击者通过利用这些漏洞,从一个低权限级别跳转到一个更高的权限级别。例如,攻击者从普通用户身份成功跃迁为管理员。


1.3. 漏洞举例


1.3.1. 水平越权


假设一个在线论坛应用程序,每个用户都有一个唯一的用户ID,并且用户可以通过URL访问他们自己的帖子。应用程序的某个页面的URL结构如下:


https://example.com/forum/posts?userId=<用户ID>

应用程序使用userId参数来标识要显示的用户的帖子。假设Alice的用户ID为1,Bob的用户ID为2。


Alice可以通过以下URL访问她自己的帖子:


https://example.com/forum/posts?userId=1

现在,如果Bob意识到URL参数是可变的,他可能尝试修改URL参数来访问Alice的帖子。他将尝试将URL参数修改为Alice的用户ID(1):


https://example.com/forum/posts?userId=1

如果应用程序没有正确实施访问控制机制,没有验证用户的身份和权限,那么Bob将成功地通过URL参数访问到Alice的帖子。


1.3.2. 垂直越权


假设一个电子商务网站,有两种用户角色:普通用户和管理员。普通用户有限的权限,只能查看和购买商品,而管理员则拥有更高的权限,可以添加、编辑和删除商品。


在正常情况下,只有管理员可以访问和执行与商品管理相关的操作。然而,如果应用程序没有正确实施访问控制和权限验证,那么普通用户可能尝试利用垂直越权漏洞提升为管理员角色,并执行未经授权的操作。


例如,普通用户Alice可能意识到应用程序的URL结构如下:


https://example.com/admin/manage-products

她可能尝试手动修改URL,将自己的用户角色从普通用户更改为管理员,如下所示:


https://example.com/admin/manage-products?role=admin

如果应用程序没有进行足够的验证和授权检查,就会错误地将Alice的角色更改为管理员,从而使她能够访问和执行与商品管理相关的操作。


1.4. 漏洞危害


具体以实际越权的功能为主,大多危害如下:



  1. 数据泄露:攻击者可以通过越权访问敏感数据,如个人信息、财务数据或其他敏感业务数据。这可能导致违反隐私法规、信用卡信息泄露或个人身份盗用等问题。

  2. 权限提升:攻击者可能利用越权漏洞提升其权限级别,获得系统管理员或其他高权限用户的特权。这可能导致对整个系统的完全控制,并进行更广泛的恶意活动。


1.5. 修复建议



  1. 实施严格的访问控制:确保在应用程序的各个层面上实施适当的访问控制机制,包括身份验证、会话管理和授权策略。对用户进行适当的身份验证和授权,仅允许其执行其所需的操作。

  2. 验证用户输入:应该对所有用户输入进行严格的验证和过滤,以防止攻击者通过构造恶意输入来利用越权漏洞。特别是对于涉及访问控制的操作,必须仔细验证用户请求的合法性。

  3. 最小权限原则:在分配用户权限时,采用最小权限原则,即给予用户所需的最低权限级别,以限制潜在的越权行为。用户只应具备完成其任务所需的最小权限。

  4. 安全审计和监控:建立安全审计和监控机制,对系统中的访问活动进行监视和记录。这可以帮助检测和响应越权行为,并提供对事件的审计跟踪。


作者:初始安全
来源:juejin.cn/post/7235801811525664825
收起阅读 »

Java常用JVM参数实战

在Java应用程序的部署和调优过程中,合理配置JVM参数是提升性能和稳定性的关键之一。本文将介绍一些常用的JVM参数,并给出具体的使用例子和作用的分析。 内存管理相关参数 -Xmx和-Xms -Xmx参数用于设置JVM的最大堆内存大小,而-Xms参数用于设置J...
继续阅读 »

在Java应用程序的部署和调优过程中,合理配置JVM参数是提升性能和稳定性的关键之一。本文将介绍一些常用的JVM参数,并给出具体的使用例子和作用的分析。


内存管理相关参数


-Xmx和-Xms


-Xmx参数用于设置JVM的最大堆内存大小,而-Xms参数用于设置JVM的初始堆内存大小。这两个参数可以在启动时通过命令行进行配置,例如:


java -Xmx2g -Xms512m MyApp

上述示例将JVM的最大堆内存设置为2GB,初始堆内存设置为512MB。


作用分析:



  • 较大的最大堆内存可以增加应用程序的可用内存空间,提高性能。但也需要考虑服务器硬件资源的限制。

  • 合理设置初始堆内存大小可以减少JVM的自动扩容和收缩开销。


-XX:NewRatio和-XX:SurvivorRatio


-XX:NewRatio参数用于设置新生代与老年代的比例,默认值为2。而-XX:SurvivorRatio参数用于设置Eden区与Survivor区的比例,默认值为8。


例如,我们可以使用以下参数配置:


java -XX:NewRatio=3 -XX:SurvivorRatio=4 MyApp

作用分析:



  • 调整新生代与老年代的比例可以根据应用程序的特点来优化内存分配。

  • 调整Eden区与Survivor区的比例可以控制对象在新生代中的存活时间。


-XX:MaxMetaspaceSize


在Java 8及之后的版本中,-XX:MaxMetaspaceSize参数用于设置元空间(Metaspace)的最大大小。例如:


java -XX:MaxMetaspaceSize=512m MyApp

作用分析:



  • 元空间用于存储类的元数据信息,包括类的结构、方法、字段等。

  • 调整元空间的最大大小可以避免元空间溢出的问题,提高应用程序的稳定性。


-Xmn


-Xmn参数用于设置新生代的大小。以下是一个例子:


java -Xmn256m MyApp


  • -Xmn256m将新生代的大小设置为256MB。


作用分析:



  • 新生代主要存放新创建的对象,设置合适的大小可以提高垃圾回收的效率。


垃圾回收相关参数


-XX:+UseG1GC


-XX:+UseG1GC参数用于启用G1垃圾回收器。例如:


java -XX:+UseG1GC MyApp

作用分析:



  • G1垃圾回收器是Java 9及之后版本的默认垃圾回收器,具有更好的垃圾回收性能和可预测的暂停时间。

  • 使用G1垃圾回收器可以减少垃圾回收的停顿时间,提高应用程序的吞吐量。


-XX:ParallelGCThreads和-XX:ConcGCThreads


-XX:ParallelGCThreads参数用于设置并行垃圾回收器的线程数量,而-XX:ConcGCThreads参数用于设置并发垃圾回收器的线程数量。例如:


java -XX:ParallelGCThreads=4 -XX:ConcGCThreads=2 MyApp

作用分析:



  • 并行垃圾回收器通过使用多个线程来并行执行垃圾回收操作,提高回收效率。

  • 并发垃圾回收器在应用程序运行的同时执行垃圾回收操作,减少停顿时间。


-XX:+ExplicitGCInvokesConcurrent


-XX:+ExplicitGCInvokesConcurrent参数用于允许主动触发并发垃圾回收。例如:


java -XX:+ExplicitGCInvokesConcurrent MyApp

作用分析:



  • 默认情况下,当调用System.gc()方法时,JVM会使用串行垃圾回收器执行垃圾回收操作。使用该参数可以改为使用并发垃圾回收器执行垃圾回收操作,减少停顿时间。


性能监控和调优参数


-XX:+PrintGCDetails和-XX:+PrintGCDateStamps


-XX:+PrintGCDetails参数用于打印详细的垃圾回收信息,-XX:+PrintGCDateStamps参数用于打印垃圾回收发生的时间戳。例如:


java -XX:+PrintGCDetails -XX:+PrintGCDateStamps MyApp

作用分析:



  • 打印垃圾回收的详细信息可以帮助我们了解垃圾回收器的工作情况,检测潜在的性能问题。

  • 打印垃圾回收发生的时间戳可以帮助我们分析应用程序的垃圾回收模式和频率。


-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath


-XX:+HeapDumpOnOutOfMemoryError参数用于在发生内存溢出错误时生成堆转储文件,-XX:HeapDumpPath参数用于指定堆转储文件的路径。例如:


java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump/file MyApp

作用分析:



  • 在发生内存溢出错误时生成堆转储文件可以帮助我们分析应用程序的内存使用情况,定位内存泄漏和性能瓶颈。


-XX:ThreadStackSize


-XX:ThreadStackSize参数用于设置线程栈的大小。以下是一个例子:


java -XX:ThreadStackSize=256k MyApp

作用分析:



  • 线程栈用于存储线程执行时的方法调用和局部变量等信息。

  • 通过调整线程栈的大小,可以控制应用程序中线程的数量和资源消耗。


-XX:MaxDirectMemorySize


-XX:MaxDirectMemorySize参数用于设置直接内存的最大大小。以下是一个例子:


java -XX:MaxDirectMemorySize=1g MyApp

作用分析:



  • 直接内存是Java堆外的内存,由ByteBuffer等类使用。

  • 合理设置直接内存的最大大小可以避免直接内存溢出的问题,提高应用程序的稳定性。


其他参数


除了上述介绍的常用JVM参数,还有一些其他参数可以根据具体需求进行配置,如:



  • -XX:+DisableExplicitGC:禁止主动调用System.gc()方法。

  • -XX:+UseCompressedOops:启用指针压缩以减小对象引用的内存占用。

  • -XX:OnOutOfMemoryError:在发生OutOfMemoryError时执行特定的命令或脚本。


这些参数可以根据应用程序的特点和需求进行调优和配置,以提升应用程序的性能和稳定性。


总结


本文介绍了一些常用的JVM参数,并给出了具体的使用例子和作用分析。合理配置这些参数可以优化内存管理、垃圾回收、性能监控等方面,提升Java应用程序的性能和稳定性。


在实际应用中,建议根据应用程序的需求和性能特点,综合考虑不同参数的使用。同时,使用工具进行性能监控和分析,以找出潜在的问题和瓶颈,并根据实际情况进行调优。



我是蚂蚁背大象,文章对你有帮助给项目点个❤关注我GitHub:mxsm,文章有不正确的地方请您斧正,创建ISSUE提交PR~谢谢!


作者:蚂蚁背大象
来源:juejin.cn/post/7235435351049781304

收起阅读 »

从前后端的角度分析options预检请求

本文分享自华为云社区《从前后端的角度分析options预检请求——打破前后端联调的理解障碍》,作者: 砖业洋__ 。 options预检请求是干嘛的?options请求一定会在post请求之前发送吗?前端或者后端开发需要手动干预这个预检请求吗?不用文档定义堆砌...
继续阅读 »

本文分享自华为云社区《从前后端的角度分析options预检请求——打破前后端联调的理解障碍》,作者: 砖业洋__ 。


options预检请求是干嘛的?options请求一定会在post请求之前发送吗?前端或者后端开发需要手动干预这个预检请求吗?不用文档定义堆砌名词,从前后端角度单独分析,大白话带你了解!


从前端的角度看options——post请求之前一定会有options请求?信口雌黄!


你是否经常看到这种跨域请求错误?


image.png


这是因为服务器不允许跨域请求,这里会深入讲一讲OPTIONS请求。


只有在满足一定条件的跨域请求中,浏览器才会发送OPTIONS请求(预检请求)。这些请求被称为“非简单请求”。反之,如果一个跨域请求被认为是“简单请求”,那么浏览器将不会发送OPTIONS请求。


简单请求需要满足以下条件:



  1. 只使用以下HTTP方法之一:GETHEADPOST

  2. 只使用以下HTTP头部:AcceptAccept-LanguageContent-LanguageContent-Type

  3. Content-Type的值仅限于:application/x-www-form-urlencodedmultipart/form-datatext/plain


如果一个跨域请求不满足以上所有条件,那么它被认为是非简单请求。对于非简单请求,浏览器会在实际请求(例如PUTDELETEPATCH或具有自定义头部和其他Content-TypePOST请求)之前发送OPTIONS请求(预检请求)。


举个例子吧,口嗨半天是看不懂的,让我们看看 POST请求在什么情况下不发送OPTIONS请求


提示:当一个跨域POST请求满足简单请求条件时,浏览器不会发送OPTIONS请求(预检请求)。以下是一个满足简单请求条件的POST请求示例:


// 使用Fetch API发送跨域POST请求
fetch("https://example.com/api/data", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "key1=value1&key2=value2"
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error:", error));

在这个示例中,我们使用Fetch API发送了一个跨域POST请求。请求满足以下简单请求条件:



  1. 使用POST方法。

  2. 使用的HTTP头部仅包括Content-Type

  3. Content-Type的值为"application/x-www-form-urlencoded",属于允许的三种类型之一(application/x-www-form-urlencoded、multipart/form-data或text/plain)。


因为这个请求满足了简单请求条件,所以浏览器不会发送OPTIONS请求(预检请求)。


我们再看看什么情况下POST请求之前会发送OPTIONS请求,同样用代码说明,进行对比


提示:在跨域请求中,如果POST请求不满足简单请求条件,浏览器会在实际POST请求之前发送OPTIONS请求(预检请求)。


// 使用Fetch API发送跨域POST请求
fetch("https://example.com/api/data", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Custom-Header": "custom-value"
},
body: JSON.stringify({
key1: "value1",
key2: "value2"
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error:", error));

在这个示例中,我们使用Fetch API发送了一个跨域POST请求。请求不满足简单请求条件,因为:



  1. 使用了非允许范围内的Content-Type值("application/json" 不属于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。

  2. 使用了一个自定义HTTP头部 “X-Custom-Header”,这不在允许的头部列表中。


因为这个请求不满足简单请求条件,所以在实际POST请求之前,浏览器会发送OPTIONS请求(预检请求)。


你可以按F12直接在Console输入查看Network,尽管这个网址不存在,但是不影响观察OPTIONS请求,对比一下我这两个例子。


总结:当进行非简单跨域POST请求时,浏览器会在实际POST请求之前发送OPTIONS预检请求,询问服务器是否允许跨域POST请求。如果服务器不允许跨域请求,浏览器控制台会显示跨域错误提示。如果服务器允许跨域请求,那么浏览器会继续发送实际的POST请求。而对于满足简单请求条件的跨域POST请求,浏览器不会发送OPTIONS预检请求。


后端可以通过设置Access-Control-Max-Age来控制OPTIONS请求的发送频率。OPTIONS请求没有响应数据(response data),这是因为OPTIONS请求的目的是为了获取服务器对于跨域请求的配置信息(如允许的请求方法、允许的请求头部等),而不是为了获取实际的业务数据,OPTIONS请求不会命中后端某个接口。因此,当服务器返回OPTIONS响应时,响应中主要包含跨域配置信息,而不会包含实际的业务数据


本地调试一下,前端发送POST请求,后端在POST方法里面打断点调试时,也不会阻碍OPTIONS请求的返回


image.png


2.从后端的角度看options——post请求之前一定会有options请求?胡说八道!


在配置跨域时,服务器需要处理OPTIONS请求,以便在响应头中返回跨域配置信息。这个过程通常是由服务器的跨域中间件(Node.jsExpress框架的cors中间件、PythonFlask框架的flask_cors扩展)或过滤器(JavaSpringBoot框架的跨域过滤器)自动完成的,而无需开发人员手动处理。


以下是使用Spring Boot的一个跨域过滤器,供参考


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {

public CorsConfig() {
}

@Bean
public CorsFilter corsFilter()
{
// 1. 添加cors配置信息
CorsConfiguration config = new CorsConfiguration();
// Response Headers里面的Access-Control-Allow-Origin: http://localhost:8080
config.addAllowedOrigin("http://localhost:8080");
// 其实不建议使用*,允许所有跨域
config.addAllowedOrigin("*");

// 设置是否发送cookie信息,在前端也可以设置axios.defaults.withCredentials = true;表示发送Cookie,
// 跨域请求要想带上cookie,必须要请求属性withCredentials=true,这是浏览器的同源策略导致的问题:不允许JS访问跨域的Cookie
/**
* withCredentials前后端都要设置,后端是setAllowCredentials来设置
* 如果后端设置为false而前端设置为true,前端带cookie就会报错
* 如果后端为true,前端为false,那么后端拿不到前端的cookie,cookie数组为null
* 前后端都设置withCredentials为true,表示允许前端传递cookie到后端。
* 前后端都为false,前端不会传递cookie到服务端,后端也不接受cookie
*/

// Response Headers里面的Access-Control-Allow-Credentials: true
config.setAllowCredentials(true);

// 设置允许请求的方式,比如get、post、put、delete,*表示全部
// Response Headers里面的Access-Control-Allow-Methods属性
config.addAllowedMethod("*");

// 设置允许的header
// Response Headers里面的Access-Control-Allow-Headers属性,这里是Access-Control-Allow-Headers: content-type, headeruserid, headerusertoken
config.addAllowedHeader("*");
// Response Headers里面的Access-Control-Max-Age:3600
// 表示下回同一个接口post请求,在3600s之内不会发送options请求,不管post请求成功还是失败,3600s之内不会再发送options请求
// 如果不设置这个,那么每次post请求之前必定有options请求
config.setMaxAge(3600L);
// 2. 为url添加映射路径
UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
// /**表示该config适用于所有路由
corsSource.registerCorsConfiguration("/**", config);

// 3. 返回重新定义好的corsSource
return new CorsFilter(corsSource);
}
}


这里setMaxAge方法来设置预检请求(OPTIONS请求)的有效期,当浏览器第一次发送非简单的跨域POST请求时,它会先发送一个OPTIONS请求。如果服务器允许跨域,并且设置了Access-Control-Max-Age头(设置了setMaxAge方法),那么浏览器会缓存这个预检请求的结果。在Access-Control-Max-Age头指定的时间范围内,浏览器不会再次发送OPTIONS请求,而是直接发送实际的POST请求,不管POST请求成功还是失败,在设置的时间范围内,同一个接口请求是绝对不会再次发送OPTIONS请求的。


后端需要注意的是,我这里设置允许请求的方法是config.addAllowedMethod("*")*表示允许所有HTTP请求方法。如果未设置,则默认只允许“GET”和“HEAD”。你可以设置的HTTPMethodGET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE


经过我的测试,OPTIONS无需手动设置,因为单纯只设置OPTIONS也无效。如果你设置了允许POST,代码为config.addAllowedMethod(HttpMethod.POST); 那么其实已经默认允许了OPTIONS,如果你只允许了GET,尝试发送POST请求就会报错。


举个例子,这里只允许了GET请求,当我们尝试发送一个POST非简单请求,预检请求返回403,服务器拒绝了OPTIONS类型的请求,因为你只允许了GET,未配置允许OPTIONS请求,那么浏览器将收到一个403 Forbidden响应,表示服务器拒绝了该OPTIONS请求,POST请求的状态显示CORS error



Spring Boot中,配置允许某个请求方法(如POSTPUTDELETE)时,OPTIONS请求通常会被自动允许。这意味着在大多数情况下,后端开发人员不需要特意考虑OPTIONS请求。这种自动允许OPTIONS请求的行为取决于使用的跨域处理库或配置,最好还是显式地允许OPTIONS请求。


点击关注,第一时间了解华为云新鲜技术~


作者:华为云开发者联盟
来源:juejin.cn/post/7233587643724234811
收起阅读 »

学了设计模式,我重构了原来写的垃圾代码

前言 最近笔者学习了一些设计模式,都记录在我的专栏 前端要掌握的设计模式 中,感兴趣的掘友可以移步看看。本着 学东西不能停留在用眼睛看,要动手实践 的理念,笔者今天带来的是一篇关于代码逻辑重构的文章,将学到的东西充分运用到实际的项目中。 重构代码的背景 要重构...
继续阅读 »

前言


最近笔者学习了一些设计模式,都记录在我的专栏 前端要掌握的设计模式 中,感兴趣的掘友可以移步看看。本着 学东西不能停留在用眼睛看,要动手实践 的理念,笔者今天带来的是一篇关于代码逻辑重构的文章,将学到的东西充分运用到实际的项目中。


重构代码的背景


要重构的代码是之前笔者的一篇文章——我是怎么开发一个Babel插件来实现项目需求的?,大概的逻辑就是实现 JS 代码的一些转换需求:



  1. 去掉箭头函数的第一个参数(如果是ctx(ctx, argu1) => {}转换为(argu1) => {}

  2. 函数调用加上this.: sss(ctx) 转换为 this.sss()

  3. ctx.get('one').$xxx() 转换为 this.$xxxOne()

  4. const crud = ctx.get('two'); crud.$xxx();转换为this.$xxxTwo()


  5. /**
    * 处理批量的按钮显示隐藏
    * ctx.setVisible('code1,code2,code3', true)
    * 转化为
    * this.$refs.code1.setVisible(true)
    * this.$refs.code2.setVisible(true)
    * this.$refs.code3.setVisible(true)
    */


  6. 函数调用把部分 API 第一参数为ctx的变为arguments

  7. 函数调用去掉第一个参数(如果是ctx

  8. 函数声明去掉第一个参数(如果是ctx

  9. 普通函数 转为 () => {}

  10. 标识符ctx 转为 this

  11. ctx.data 转为 this

  12. xp.message(options) 转换为 this.$message(options)

  13. const obj = { get() {} } 转化为 const obj = { get: () => {} }


具体的实现可参考之前的文章,本文主要分享一下重构的实现。


重构前


所有的逻辑全写在一个 JS 文件中:
image.png
还有一段逻辑很长:
image.png


为啥要重构?


虽然主体部分被我折叠起来了,依然可以看到上面截图的代码存在很多问题,而且主体内容只会比这更糟:



  1. 难以维护,虽然写了注释,但是让别人来改根本看不明白,改不动,甚至不想看

  2. 如果新增了转换需求,不得不来改上面的代码,违反开放封闭原则。因为你无法保证你改动的代码不会造成原有逻辑的 bug。

  3. 代码没有章法,乱的一批,里面还有一堆 if/elsetry/catch

  4. 如果某个转换逻辑,我不想启用,按照现有的只能把对应的代码注释,依然是具有破坏性的


基于以上的这些问题,我决定重构一下。


重构后


先来看下重构后的样子:
image.png
统一将代码放到一个新的文件夹code-transform下:



  • transfuncs文件夹用来放具体的转换逻辑

  • util.js中有几个工具函数

  • trans_config.js用于配置transfuncs中转换逻辑是否生效

  • index.js 导出访问者对象 visitor(可以理解为我们根据配置动态组装一个 visitor 出来)


transfuncs下面的文件格式


如下图所示,该文件夹下的每个 JS 文件都默认导出一个函数,是真正的转换逻辑。
image.png
文件名命名规则:js ast树中节点的type_执行转换的大概内容


其余三个文件内容概览


image.png
其中笔者主要说明一下index.js


import config from './trans_config'

const visitor = {}
/**
* 导出获取访问者对象的函数
*/

export function generateVisitorByConfig() {
if (Object.keys(visitor).length !== 0) {
return visitor
}
// 过滤掉 trans_config.js 中不启用的转换规则
const transKeys = Object.keys(config).filter(key => config[key])
// 导入 ./transfuncs 下的转换规则
const context = require.context('./transfuncs', false, /\.js$/)
const types = new Set()
// 统计我们定义的转换函数,是哪些 ast 节点执行转换逻辑
// 别忘了文件名命名规则:js ast树中节点的type_执行转换的大概内容
// 注意去重,因为我们可能在同一种节点类型,会执行多种转换规则。
// 比如 transfuncs 下有多个 CallExpression_ 开头的文件。
context.keys().forEach(path => {
const fileName = path.substring(path.lastIndexOf('/') + 1).replace('.js', '')
const type = fileName.split('_')[0]
types.add(type)
})

const arrTypes = [...types]
// 到此 arrTypes 可能是这样的:
// ['CallExpression', 'FunctionDeclaration', 'MemberExpression', ...]
// 接着遍历每种节点 type

arrTypes.forEach(type => {
const typeFuncs = context.keys()
// 在 transfuncs 文件夹下找出以 对应 type 开头
// 并且 trans_config 中启用了的的文件
.filter(path => path.includes(type) && transKeys.find(key => path.includes(key)))
// 得到文件导出的 function
.map(path => context(path).default)
// 如果 typeFuncs.length > 0,就给 visitor 设置该节点执行的转换逻辑
typeFuncs.length > 0 && (visitor[type] = path => {
typeFuncs.forEach(func => func(path, attribute))
})
})
// 导出 visitor
return visitor
}

最后调用:


import { generateVisitorByConfig } from '../code-transform'

const transed = babel.transform(code, {
presets: ['es2016'],
sourceType: 'module',
plugins: [
{
visitor: generateVisitorByConfig()
}
]
}).code

有些掘友可能对babel的代码转换能力、babel插件不是很了解, 看完可能还处于懵的状态,对此建议各位先去我的上一篇我是怎么开发一个Babel插件来实现项目需求的? 大致看下逻辑,或者阅读一下Babel插件手册,看完之后自然就通了。


总结


到此呢,该部分的代码重构就完成了,能够明显看出:



  1. 文件变多了,但是每个文件做的事情更专一了

  2. 可以很轻松启用、禁用转换规则了,trans_config中配置一下即可,再也不用注释代码了

  3. 可以很轻松的新增转换逻辑,你只需要关注你在哪个节点处理你的逻辑,注意下文件名即可,你甚至不需要关心引入文件,因为会自动引入。

  4. 更容易维护了,就算离职了你的同事也能改的动你的代码,不会骂人了

  5. 逻辑更清晰了

  6. 对个人来说,代码组织能力提升了😃


👊🏼感谢观看!如果对你有帮助,别忘了 点赞 ➕ 评论 + 收藏 哦!


作者:Lvzl
来源:juejin.cn/post/7224205585125556284
收起阅读 »

如何避免旧代码成包袱?5步教你接手别人的系统

👉腾小云导读 老系统的代码,是每一个程序员都不想去触碰的领域,秉着能跑就行的原则,任由其自生自灭。本期就给大家讲讲,接手一套故障频发的复杂老系统需要从哪些地方着手。内容包括:代码串讲、监控建设和告警治理、代码缺陷修复、研发流程建设。在细节上,结合腾讯研发生态,...
继续阅读 »

👉腾小云导读


老系统的代码,是每一个程序员都不想去触碰的领域,秉着能跑就行的原则,任由其自生自灭。本期就给大家讲讲,接手一套故障频发的复杂老系统需要从哪些地方着手。内容包括:代码串讲、监控建设和告警治理、代码缺陷修复、研发流程建设。在细节上,结合腾讯研发生态,介绍有哪些工具可以使用,也介绍一些告警治理、代码 bug 修复的经验、研发流程建设等。欢迎阅读。


👉看目录,点收藏


1 项目背景


2 服务监控


2.1 平台自带监控


2.2 业务定制监控


3 串讲文档


3.1 串讲文档是什么


3.2 为什么需要串讲文档


3.3 怎么输出串讲文档


4 代码质量


4.1 业务逻辑 bug


4.2 防御编程


4.3 Go-python 内存泄露问题


4.4 正确使用外部库


4.5 避免无限重试导致雪崩


4.6 真实初始化


4.7 资源隔离


4.8 数据库压力优化


4.9 互斥资源管理


5 警告治理


5.1 全链路超时时间合理设置


5.2 基于业务分 set 进行资源隔离


5.3 高耗时计算使用线程池


6 研发流程


7 优化效果总结


7.1 健全的 CICD


7.2 更完备的可观测性


7.3 代码 bug 修复


7.4 服务被调成功率优化


7.5 外部存储使用优化


7.6 CPU 用量下降


7.7 代码质量提升


7.8 其他优化效果


01、项目背景


内容架构为 QB 搜索提供内容接入、计算、分发的全套服务。经过多年的快速迭代,内容架构包括 93 个服务,光接入主链路就涉及 7 个服务,支持多种接口类型。其分支定制策略多且散,数据流向混杂,且有众多 bug。


项目组接手这套架构的早期,每天收到大量的业务故障反馈以及服务自身告警。即便投入小组一半的人力做运维支持,依旧忙得焦头烂额。无法根治系统稳定性缺陷的同时,项目组还需要继续承接新业务,而新业务又继续暴露系统缺陷,陷入不断恶化的负循环,漏洞百出。没有人有信心去承诺系统稳定性,团队口碑、开发者的信心都处于崩溃的边缘。


在此严峻的形势下,项目组全员投入启动稳定性治理专项,让团队进入系统稳定的正循环


02、服务监控


监控可以帮助我们掌握服务运行时状态,发现、感知服务异常情况。通过看到问题 - 定位问题 - 修复问题来更快的熟悉模块架构和代码实现细节。下面分两部分介绍,如何利用监控达成稳定性优化。


2.1 平台自带监控


若服务的部署、发布、运行托管于公共平台,则这些平台可能会提供容器资源使用情况的监控,包括:CPU 利用率监控、内存使用率监控、磁盘使用率监控等服务通常应该关注的核心指标。我们的服务部署在123 平台(司内平台),有如下常用的监控。


平台自带监控:


监控类型解析
服务运行监控123 平台的 tRPC 自定义监控:event_alarm 可监控服务异常退出。
节点服务资源使用监控内存使用率监控:检查是否存在内存泄露,会不会不定期 OOM。
磁盘使用率监控:检查日志是否打印的过多,日志滚动配置是否合理。
CPU 使用率监控
如存在定期/不定期的 CPU 毛刺,则检查对应时段请求/定期任务是否实现不合理。如CPU 配额高而使用率低,则检查是配置不合理导致资源浪费,还是服务实现不佳导致 CPU 打不上去。

外部资源平台监控:


数据库连接数监控:检查服务使用 DB 是否全是长连接,使用完没有及时 disconnect 。
数据库慢查询监控:SQL 命令是否不合理,DB 表是否索引设置不合理。数据库 CPU 监控:检查服务是否全部连的 DB 主机,对于只读的场景可选择用只读账号优先读备机,降低 DB 压力。其他诸如腾讯云 Redis 等外部资源也有相关的慢查询监控,亦是对应检查。

2.2 业务定制监控


平台自带的监控让我们掌控服务基本运行状态。我们还需在业务代码中增加业务自定义监控,以掌控业务层面的运转情况。


下面介绍常见的监控完善方法,以让各位对于业务运行状态尽在掌握之中。


2.2.1 在主/被调监控中增加业务错误码


一般来说,后台服务如果无法正常完成业务逻辑,会将错误码和错误详情写入到业务层的回包结构体中,然后在框架层返回成功。


这种实现导致我们无法从平台自带的主/被调监控中直观看出有多少请求是没有正常结果的。一个服务可能看似运行平稳,基于框架层判断的被调成功率 100%,但其中却有大量没有正常返回结果的请求,在我们的监控之外。


此时如果我们想要对这些业务错误做监控,需要上报这些维度:请求来源(主调服务、主调容器、主调 IP、主调 SET)、被调容器、错误码、错误数,这和被调监控有极大重合,存在资源浪费,并且主、被调服务都有开发成本。


若服务框架支持,我们应该尽可能使用框架层状态包来返回业务自定义的错误码。以tRPC框架为例服务的<被调监控 - 返回码>中,会上报框架级错误码和业务错误码:框架错误码是纯数字,业务错误码是<业务协议_业务码>。如此一来,就可以在主/被调服务的平台自带监控中看到业务维度的返回码监控。


2.2.2 在主/被调监控中注入业务标识


有时候一个接口会承担多个功能,用请求参数区分执行逻辑A / 逻辑B。在监控中,我们看到这个接口失败率高,需要进一步下钻是 A 失败高还是 B 失败高、A/B 分别占了多少请求量等等。


对这种诉求,我们可以在业务层上报一个带有各种主调属性的多维监控以及接口参数维度用于下钻分析。但这种方式会造成自定义监控和被调监控字段的重叠浪费。


更好的做法是:将业务维度注入到框架的被调监控中,复用被调监控中的主调服务、节点、SET、被调接口、IP、容器等信息。


2.2.3 单维属性上报升级成多维属性上报


单维属性监控指的是上报单个字符串(维度)、一个浮点数值以及这个浮点数值对应的统计策略。多维监控指的是上报多个字符串(维度)、多个浮点数值以及每个浮点数值对应的统计策略。


下面以错误信息上报场景为例,说明单维监控的缺点以及如何切换为多维上报。


作为后台服务,处理逻辑环节中我们需要监控上报各类关键错误,以便及时感知错误、介入处理。内容架构负责多个不同业务的接入处理,每个业务具有全局唯一的资源标识,下面称为 ResID。当错误出现时,做异常上报时,我们需要上报 “哪个业务”,出现了“什么错误”。


使用单维上报时,我们只能将二者拼接在一起,上报 string (ResID + "." + ErrorMsg)。对不同的 ResID 和不同的 ErrorMsg,监控图是铺平展开的,只能逐个查看,无法对 ResID 下钻查看每类错误分别出现了多少次或者对 ErrorMsg 下钻,查看此错误分别在哪些 ID 上出现。


除了查看上的不便,多维属性监控配置告警也比单维监控便利。对于单维监控,我们需要对每一个可能出现的组合配置条件:维度值 = XXX,错误数 > N 则告警。一旦出现了新的组合,比如新接入了一个业务,我们就需要再去加告警配置,十分麻烦且难以维护。


而对于多维监控表,我们只需设置一条告警配置:对 ResID && 错误维度 下钻,错误数 > N 则告警。新增 ResID 或新增错误类型时,均会自动覆盖在此条告警配置内,维护成本低,配置简单。


03、串讲文档


监控可以帮助我们了解服务运行的表现,想要“深度清理”服务潜在问题,我们还需要对项目做代码级接手。在稳定性治理专项中,我们要求每个核心模块都产出一份串讲文档,而后交叉学习,使得开发者不单熟悉自己负责的模块,也对完整系统链路各模块功能有大概的理解,避免窥豹一斑。


3.1 串讲文档是什么


代码串讲指的是接手同学在阅读并理解模块代码后,系统的向他人介绍对该模块的掌握情况。代码串讲文档则是贯穿串讲过程中的分享材料,需要对架构设计、代码实现、部署、监控、理想化思考等方面进行详细介绍,既帮助其他同学更好的理解整个模块,也便于评估接手同学对项目的理解程度。


代码串讲文档通常包括以下内容:模块主要功能,上下游关系、整体架构、子模块的详细介绍、模块研发和上线流程、模块的关键指标等等。在写串讲文档的时候也通常需要思考很多问题:这个功能为什么需要有?设计思路是这样的?技术上如何实现?最后是怎么应用的?


3.2 为什么需要串讲文档


原因
解析
确保代码走读的质量串讲文档涵盖了模块最重要的几个部分,要求开发人员编写串讲文档的这些章节,可保障他在走读此模块时高度关注到这些部分,并总结输出成文档,确保了代码学习的深度和质量。
强化理解编写串讲文档的过程,也是对模块各方面的提炼总结。在编写的过程中,开发人员可能会发现走读中未想到的问题。通过编写代码串讲文档,开发人员可以更好地理解整个系统的结构和实现,加强对代码和系统的理解,为后续接手、维护该系统提供了帮助。
团队知识沉淀和积累串讲文档是团队内部进行知识分享和沟通的载体,也是团队知识不断沉淀积累的过程,可以作为后续新人加入团队时了解系统的第一手材料,也可以作为其他同学后续多次翻阅、了解该系统的材料。

3.3 怎么输出串讲文档


增代码串讲文档的时候,需要从2个方面进行考虑——读者角度和作者角度。


作者角度: 需要阐述作者对系统和代码的理解和把握,同时也需要思考各项细节:这个功能为什么需要有、设计思路是怎样的、技术上如何实现、最后是怎么应用的等等。


读者角度: 需要考虑目标受众是哪些,尽可能地把读者当成技术小白,思考读者需要了解什么信息,如何才能更好地理解代码的实现和作用。


通常,代码串讲文档可以包含以下几个部分:


文档构成
信息
模块主要功能首先需要明确模块的主要功能,并在后续的串讲中更加准确地介绍代码的实现。
上下游关系在介绍每个模块时,需要明确它与其他模块之间的上下游关系,即模块之间的调用关系,这有助于了解模块之间的依赖关系,从而更好地理解整个系统的结构和实现。
名词解释对一些关键词、专业术语和缩写词等专业术语进行解释,不同团队使用的术语和缩写可能不同,名词解释可以减少听众的阅读难度,提高沟通效率。
整体架构介绍整个系统的设计思路、实现方案和架构选择的原因以及优缺点。
子模块的详细解读在介绍每个子模块时,需要对其进行详细的解读,包括该模块的具体功能、实现方式、代码实现细节等方面。
模块研发和上线流程介绍模块的研发和上线流程,包括需求分析、设计、开发、测试、上线等环节。这有助于了解模块的开发过程和上线流程,以及研发团队的工作流程。
模块的关键指标介绍模块的关键指标,包括不限于业务指标、服务监控、成本指标等。
模块当前存在问题及可能的解决思路主要用于分析该模块目前存在的问题,并提出可能的解决思路和优化方案。
理想化的思考对该模块或整个系统的未来发展和优化方向的思考和展望,可以对模块的长期目标、技术选型、创新探索、稳定性建设等方面进行思考。
中长线工作安排结合系统现状和理想化思考,做出可行的中长线工作计划。
串讲中的问题这部分用于记录串讲中的问题及解答,通常是Q&A的形式,串讲中的问题也可能是系统设计相关的问题,后续将作为todo项加入到工作安排中。

04、代码质量


代码质量很大程度上决定服务的稳定性。在对代码中业务逻辑 bug 进行修复的同时,我们也对服务的启动、数据库压力及互斥资源管理做优化。


4.1 业务逻辑bug


4.1.1 内存泄漏


如下图代码所示,使用 malloc 分配内存,但没有 free,导致内存泄露。该代码为 C 语言风格,现代 C++ 使用智能指针可以避免该问题。


图片


4.1.2 空指针访问成员变量


如下图所示的代码,如果一个非虚成员函数没有使用成员变量,因编译期的静态绑定,空指针也可以成功调用该成员函数。但如果该成员函数使用了成员变量,那么空指针调用该函数时则会 core。该类问题在接入系统仓库中比较普遍,建议所有指针都要进行合理的初始化。


图片


4.2 防御编程


4.2.1 输入防御


如下图所示,如果发生了错误且没有提前返回,request 将引发 panic。针对输入,在没有约定的情况下,建议加上常见的空指针判断及异常判断。


图片


4.2.2 数组长度防御-1


如下图所示,当 url 长度超过 512 时,将会被截断,导致产出错误的url。建议针对字符串数组的长度进行合理的初始化,亦或者使用string来替代字符数组。


图片


4.2.3 数组长度防御-2


如下图所示,老代码不判断数组长度,直接取值。当出现异常数据时,该段代码则会core。建议在每次取值时,基于上下文做防御判断。


图片


4.2.4 野指针问题


下图中的ts指针指向内容和 create_time 一致。当 create_time 被 free 之后,ts 指针就变成了野指针。


该代码为 C 语言风格代码,很容易出现内存方面的问题。建议修改为现代 C++风格。


图片


下图中,临时变量存储的是 queue 中的值的引用。当 queue pop 后,此值会被析构;而变量引用的存储空间也随之释放,访问此临时变量可能出现未定义的行为。


图片


4.2.5 全局资源写防护


同时读写全局共有资源,尤其生成唯一 id,要考虑并发的安全性。


这里直接通过查询 DB 获取最大的 res_id,转成 int 后加一,作为新增资源的唯一 id。如果并发度超过 1,很可能会出现 id 重复,影响后续操作逻辑。


图片


4.2.6 lua 添加 json 解析防御


如下图所示的 lua 脚本中,使用 cjson 将字符串转换 json_object。当 data_obj 不是合法的 json 格式字符串时,decode 接口会返回 nil。修复前的脚本未防御返回值为空的情况,一旦传入非法字符串,lua 脚本就会引发 coredump。


图片


4.3 Go-python 内存泄露问题


如下图 85-86 行代码所示,使用 Go-python 调用 python 脚本,将 Go 的 string 转为PyString,此时 kv 为 PyObject。部分 PyObject 需要在函数结束时调用 DecRef,减少引用计数,以确保资源释放,否则会造成内存泄露。


判定依据是直接看 python sdk 的源码注释,如果有 New Reference , 那么必须在使用完毕时释放内存,Borrowed Reference 不需要手动释放。


图片


4.4 正确使用外部库


4.4.1 Kafka Message 结束字符


当生产者为 Go 服务时,写入 kafka 的消息字符串不会带有结束字符 '\0'。当生产者为 C++ 服务时,写入 kafka 的消息字符串会带有结束字符 '\0'。


如下图 481 行代码所示,C++中使用 librdkafka 获取消费数据时,需传入消息长度,而不是依赖程序自行寻找 '\0' 结束符。


图片


4.5 避免无限重试导致雪崩


如下图所示代码所示,失败之后立马重试。当出现问题时,不断立即重试,会导致雪崩。给重试加上 sleep,减缓下游雪崩的速度,留下缓冲的时间。


图片


4.6 真实初始化


如果每次服务启动都存在一定的成功率抖动,需要检查服务注册前的初始化流程,看看是不是存在异步初始化,导致未完成初始化即提供对外服务。如下图 48 行所示注释,原代码中初始化代码为异步函数,服务对外提供服务时可能还没有完成初始化。


图片


4.7 资源隔离


时延/成功率要求不同的服务接口,建议使用不同的处理线程。如下图中几个 service,之前均共用同一个处理线程池。其中 secure_review_service 处理耗时长,流量不稳定,其他接口流量大且时延敏感,线上出现过 secure_review_service 瞬时流量波峰,将处理线程全部占住且队列积压,导致其他 service 超时失败。


图片


4.8 数据库压力优化


4.8.1 分批拉取


当某个表数据很多或单次传输量很大,拉取节点也很多的时候,数据库的压力就会很大。这个时候,可以使用分批读取。下图 308-343 行代码,修改了 sql 语句,从一批拉取改为分批拉取。


图片


4.8.2 读备机


如果业务场景为只读不写数据,且对一致性要求不高,可以将读配置修改为从备机读取。mysql 集群一般只会有单个主机、多个备机,备机压力较小。如下图 44 行代码所示,使用readuser,主机压力得到改善。


图片


4.8.3 控制长链接个数


需要使用 mysql 长链接的业务,需要合理配置长链接个数,尤其是服务节点数很多的情况下。连接数过多会导致 mysql 实例内存使用量大,甚至 OOM;此外 mysql 的连接数是刚性限制,超过阈值后,客户端无法正常建立 mysql 连接,程序逻辑可能无法正常运转。


4.8.4 建好索引


使用 mysql 做大表检索时,应该建立与查询条件对应的索引。本次优化中,我们根据 DB 慢查询统计,找到有大表未建查询适用的索引,导致 db 负载高,查询速度慢。


4.8.5 实例拆分


非分布式数据库 (如 mariaDB) 存储空间是有物理上限的,需要预估好数据量,数据量过多及时进行合理的拆库。


4.8.6 分布式数据库负载均衡


分布式数据库一般应用在海量数据场景,使用中需要留意节点间负载均衡,否则可能出现单机瓶颈,拖垮整个集群性能。如下图是内容架构使用到的 hbase,图中倒数两列分别为请求数和region 数,从截图可看出集群的 region 分布较均衡,但部分节点请求量是其他节点几倍。造成图中请求不均衡的原因是集群中有一张表,有废弃数据占用大量 region,导致使用中的 region 在节点间分布不均,由此导致请求不均。解决方法是清理废弃数据,合并空数据 region。


图片


4.9 互斥资源管理


4.9.1 避免连接占用


接入系统服务的 mysql 连接全部使用了连接池管理。每一个连接在使用完之后,需要及时释放,否则该连接就会被占住,最终连接池无资源可用。下图所示的 117 行连接释放为无效代码,因为提前 return。有趣的是,这个 bug 和另外一个 bug 组合起来,解决了没有连接可用的问题:当没有连接可用时,获取的连接则会为空。该服务不会对连接判空,导致服务 core 重启,连接池重新初始化,又有可用的连接了。针对互斥的资源,要进行及时释放。


图片


4.9.2 使用 RAII 释放资源


下图所示的 225 行代码,该任务为互斥资源,只能由一个节点获得该任务并执行该任务。GetAllValueText 执行完该任务之后,应该释放该任务。然而在 240 行提前 return 时,没有及时释放该资源。


优化后,我们使用 ScopedDeferred 确保函数执行完成,退出之前一定会执行资源释放。


图片


05、告警治理


告警轰炸是接手服务初期常见的问题。除了前述的代码质量优化,我们还解决下述几类告警:


全链路超时配置不合理。下游服务的超时时间,大于上游调用它的超时时间,导致多个服务超时告警轰炸、中间链路服务无效等待等。
业务未隔离。某个业务流量突增引起全链路队列阻塞,影响到其他业务。请求阻塞。请求线程中的处理耗时过长,导致请求队列拥堵,请求消息得不到及时处理。

5.1 全链路超时时间合理设置


未经治理的长链路服务,因为超时设置不合理导致的异常现象:




  • 超时告警轰炸: A 调用 B,B 调用 C,当 C 异常时,C 有超时告警,B 也有超时告警。在稳定性治理分析过程中,C 是错误根源,因而 B 的超时告警没有价值,当链路较长时,会因某一个底层服务的错误,导致海量的告警轰炸。




  • 服务无效等待: A 调用 B,B 调用 C,当 A->B 超时的时候,B 还在等 C 的回包,此时 B 的等待是无价值的。




这两种现象是因为超时时间配置不合理导致的,对此我们制定了“超时不扩散原则”,某个服务的超时不应该通过框架扩散传递到它的间接调用者,此原则要求某个服务调用下游的超时必须小于上游调用它的超时时间。


5.2 基于业务分 set 进行资源隔离


针对某个业务的流量突增影响其他业务的问题,我们可将重点业务基于 set 做隔离部署。确保特殊业务只运行于独立节点组上,当他流量暴涨时,不干扰其他业务平稳运行,降低损失范围。


5.3 高耗时计算使用线程池


如下图红色部分 372 行所示,在请求响应线程中进行长耗时的处理,占住请求响应线程,导致请求队列阻塞,后续请求得不到及时处理。如下图绿色部分 368 行所示,我们将耗时处理逻辑转到线程池中异步处理,从而不占住请求响应线程。


图片


06、研发流程


在研发流程上,我们沿用司内其他技术产品积累的 CICD 建设经验,包括以下措施:


研发方式具体流程
建设统一镜像统一开发镜像,任意服务都可以在统一镜像下开发编译
配置工蜂仓库保护 master 分支,代码经过评审才可合入
规范分支和 tag 命名,便于后续信息追溯
规范化 commit 信息,确保信息可读,有条理,同样便于后续信息追溯
建设蓝盾流水线MR 流水线提交 MR 时执行的流水线,涵盖代码静态检查、单元测试、接口测试,确保合入 master 的代码质量达到基本水平
提交构建流水线:MR 合入后执行的流水线,同样涵盖代码检查,单元测试,接口测试,确保 master 代码随时可发布
XAC 发布流水线:发布上线的流水线,执行固化的灰度->全量流程,避免人工误操作
落实代码评审机制确保代码合入时,经过了至少一位同事的检查和评审,保障代码质量、安全

07、优化效果总结


7.1 健全的CICD


7.1.1 代码合入


在稳定性专项优化前,内容架构的服务没有合理的代码合入机制,导致主干代码出现违背编码规范、安全漏洞、圈复杂度高、bug等代码问题。


优化后,我们使用统一的蓝盾流水线模板给所有服务加上流水线,以保证上述问题能被自动化工具、人工代码评审拦截。


7.1.2 服务发布


在稳定性优化前,内容架构服务发布均为人工操作,没有 checklist 机制、审批机制、自动回滚机制,有很大安全隐患。


优化后,我们使用统一的蓝盾流水线模板给所有服务加上 XAC 流水线,实现了提示发布人在发布前自检查、double_check 后审批通过、线上出问题时一键回滚。


7.2 更完备的可观测性


7.2.1 多维度监控


在稳定性优化前,内容架构服务的监控覆盖不全,自定义监控均使用一维的属性监控,这导致多维拼接的监控项泛滥、无法自由组合多个维度,给监控查看带来不便。


优化后,我们用更少的监控项覆盖更多的监控场景。


7.2.2 业务监控和负责人制度


在稳定性优化前,内容架构服务几乎没有业务维度监控。优化后,我们加上了重要模块的多个业务维度监控,譬如:数据断流、消息组件消息挤压等,同时还建立值班 owner、服务 owner 制度,确保有告警、有跟进。


7.2.3 trace 完善与断流排查文档建设


在稳定性优化前,虽然已有上报鹰眼 trace 日志,但上报不完整、上报有误、缺乏排查手册等问题,导致对数据处理全流程的跟踪调查非常困难。


优化后,修正并补全了 trace 日志,建设配套排查文档,case 处理从不可调查变成可高效率调查。


7.3代码bug修复


7.3.1 内存泄露修复


在稳定性优化前,我们观察到有3个服务存在内存泄露,例如代码质量章节中描述内存泄露问题。


图片


7.3.2 coredump 修复 & 功能 bug 修复


在稳定性优化前,历史代码中存在诸多 bug 与可能导致 coredump 的隐患代码。


我们在稳定性优化时解决了如下 coredump 与 bug:


  • JSON 解析前未严格检查,导致 coredump 。
  • 服务还未初始化完成即接流,导致服务重启时被调成功率猛跌。
  • 服务初始化时没有同步加载配置,导致服务启动后缺失配置而调用失败。
  • Kafka 消费完立刻 Commit,导致服务重启时,消息未实际处理完,消息可能丢失/
  • 日志参数类型错误,导致启动日志疯狂报错写满磁盘。

7.4 服务被调成功率优化


在稳定性优化前,部分内容架构服务的被调成功率不及 99.5% ,且个别服务存在严重的毛刺问题。优化后,我们确保了服务运行稳定,调用成功率保持在 99.9%以上。


7.5 外部存储使用优化


7.5.1 MDB 性能优化


在稳定性优化前,内容架构各服务对MDB的使用存在以下问题:低效/全表SQL查询、所有服务都读主库、数据库连接未释放等问题。造成MDB主库的CPU负载过高、慢查询过多等问题。优化后,主库CPU使用率、慢查询数都有大幅下降。


图片


7.5.2 HBase 性能优化


在稳定性优化前,内容架构服务使用的 HBase 存在单节点拖垮集群性能的问题。


优化后,对废弃数据进行了清理,合并了空数据 region,使得 HBase 调用的 P99 耗时有所下降。


图片


7.6 CPU 用量下降


在稳定性优化前,内容架构服务使用的线程模型是老旧的 spp 协程。spp 协程在在高吞吐低延迟场景下性能有所不足,且未来 trpc 会废弃 spp。


我们将重要服务的线程模型升级成了 Fiber 协程,带来更高性能和稳定性的同时,服务CPU利用率下降了 15%。


7.7 代码质量提升


在稳定性优化前,内容架构服务存在很多不规范代码。例如不规范命名、魔术数字和常量、超长函数等。每个服务都存在几十个代码扫描问题,最大圈复杂度逼近 100。


优化后,我们对不规范代码进行了清扫,修复规范问题,优化代码命名,拆分超长函数。并在统一清理后,通过 MR 流水线拦截,保护后续合入不会引入新的规范类问题。


7.8 其他优化效果


经过本轮治理,我们的服务告警量大幅度下降。以系统中的核心处理服务为例,告警数量从159条/天降低到了0条/天。业务 case 数从 22 年 12 月接手以来的 18 个/月下降到 4 个/月。值班投入从最初的 4+ 人力,降低到 0.8 人力。


我们项目组在完成稳定性接手之后,下一步将对全系统做理想化重构,进一步提升迭代效率、运维效率。希望这些经验也对你接管/优化旧系统有帮助。如果觉得内容有用,欢迎分享。

作者:腾讯云开发者
来源:juejin.cn/post/7233564835044524092
收起阅读 »