2024年1月

开心一刻

昨晚和媳妇坐在沙发上刷视频

我用手肘轻轻推了推媳妇:你看这渣男,玩完女的都不娶人家

媳妇:哎哟我天,哎呀妈,我这也没好哪去呀

我疑惑的看向媳妇:啥意思啊

媳妇看向自己的手机:啥意思啊,特么有些人,娶完了也不玩呀

背景介绍

我负责的系统需要同步上游系统的数据

同步机制分两步

1、上游系统数据变动了,会下发消息,通知下游系统:我这边数据更新了,你们爱咋办咋办啊

2、下游系统收到消息后,会调上游系统提供的数据查询接口:请给我最新的数据

你情我愿,没有强买强卖,简直就是天作之合!

问题复现

我先模拟下两个系统,免得你们说我:光说不练假把式

环境准备

消息组件:
RabbitMQ 3.9.11

数据库:
MySQL 8.0.30

上游系统:
spring-boot-front
,源码地址:
spring-boot-front

下游系统:
spring-boot-after
,源码地址:
spring-boot-after

假设目前一致状态是:

front
端将 张三 密码调整成
zhangsan1

我们来看下效果

after
很快就成功同步了 张三 的密码
zhangsan1

一切有条不紊的进行着,平静的就像你的女神回复你的消息一样,简直是轮回!

突然的热情

当你以为一切尘埃落定,开始放下过往,准备面向未来的时候

你的女神发来了一个消息

此刻的你无比纠结,是继续舔还是果断断?

我们来模拟下她突然的消息

调整下
front
的代码

在发消息之后睡眠
100
毫秒

将 李四 的密码调整成
lisi666666

李四的密码竟然没同步成功!

打开女神的消息一看,特喵的竟然不是关心,是借钱!

问题修复

已经有女神折磨你们了,我就不折磨你们了

front
的这段代码

是有问题的!

我给你们分析下

front
事务未提交,消息就发给下游了

after
收到消息后,查询
front
接口的时候,
front
的事务若还未提交,
front
又当如何应对?

还能怎么应对,只能给旧数据了呗,是不是懂了?

既然找到原因了,就很好处理了

把所有她的联系方式都删了,问题就完美解决了

我好像偏题了,回到主题,问题同样很好处理

把消息发送从事务中拎出来就好了,也就是等事务提交后,再发消息

或者直接把要同步的数据以消息的方式直接同步给下游系统,下游系统拿到消息直接更新数据,而不用再调用上游系统的接口来查询数据(但要考虑消息体的大小)

通篇看下来是不是很精彩,此处应该有掌声

总结

1、日志很重要,很重要,很重要!

楼主这次排查这个问题还是很快的,因为日志打印的比较全,根据日志很快就能定位到接口查到的是旧数据

这就好比借钱:一定保留转账记录,现金的话要打借条

2、圈子不同,不要强融

好好的消息发送,为什么非要写到事务中?

事务尽量缩小

jQuery.i18n.properties 是 jQuery 老项目的国际化框架,其实国际化方案本质上都大同小异,都是需要用翻译函数包裹词条,然后根据词条文件来进行翻译

就是使用上与其他框架不太一样而已

但由于我们已经基于 vue 框架实现了一个国际化自动处理脚本,脚本会自动用全局函数包裹词条,自动提取到 json 文件中

因此,为了让这个老项目也能够用脚本来进行维护,又考虑到最小的改造成本和最小的影响,我们决定扩展 jQuery.i18n.properties 的能力,让它的国际化行为跟 vue-i18n 一致,也就能够用脚本来进行维护了

而且对于团队内的其他新人而言,也没必要去了解 jQuery.i18n.properties,毕竟扩展后的使用方式跟 vue-i18n 基本一样

那么扩展之前,先来看下 jQuery.i18n.properties 这个框架:

全局函数

$.i18n.properties(settings)

国际化初始化函数,用来设置当前语言,资源文件路径

$.i18n.prop(key, ...args)

国际化全局函数,根据 key 值去资源文件里找翻译,后续参数支持占位符替换

当根据 key 值找不到翻译时,会直接返回 key 值

类似 vue-i18n 的
$t

词条文件

.properties 格式文件

也是个键值对的配置文件,只是格式与 json 不一样

【en.properties】

string_login=Login
string_username=username
string_password=password

【zh.properties】

string_login=登录
string_username=账号
string_password=密码

扩展

增加支持 .json 格式的词条文件

重写 $.i18n 的全局函数,内部增加支持 json 词条的挂载,以及在原本翻译行为结束后,如果没翻译成功,则走入 json 词条里进行匹配,查看是否能翻译成功

这样能保证不改动原本项目里已有的国际化代码和行为,保持原样

新增或新改动到的代码就可以用新的方式去进行维护

所以才叫做扩展,而不是改造,毕竟扩展是以不影响原样为前提,不然谁知道老项目的屎山会由于什么修改而突然崩塌

// i18n.expand.js

/**
 * 劫持 jQuery.i18n.properties 的 api,扩展国际化能力:
 * 1. 支持 .json 格式的资源文件
 */
function init(i18n) {
  // 注意,初始化需要在 jQuery.i18n.properties.js 文件加载后才能正常劫持到 i18n 的 api
  if (!i18n) return;
  wrapFnProperties(i18n);
  wrapFnProp(i18n);
  wrapMap(i18n);
}

/**
 * 挂载新的 map 对象来存储从 .json 资源文件里读取的国际化翻译信息
 */
function wrapMap(i18n) {
  i18n.mapFromJson = i18n.mapFromJson || {};
}

/**
 * 劫持原 prop,如果原 prop 翻译失败,则再去 mapFromJson 里尝试翻译
 */
function wrapFnProp(i18n) {
  let oldFn = i18n.prop;
  i18n.prop = function (key, ...args) {
    let value = oldFn.call(i18n, key, ...args);
    // 如果原翻译行为未翻译成功,则尝试从 json 词条里去寻找翻译
    if (value === key) {
      // 这里把原 jquery.i18n.properties#prop 代码拷贝过来就行,然后把词条来源改成从json词条里寻找翻译
      value = $.i18n.mapFromJson[key];
      // ... 省略拷贝过来的代码
    }
    return value;
  };
}

/**
 * 劫持原 properties,获取国际化相关配置信息
 * 如:当前语言 language,【新增】json的国际化翻译信息
 */
function wrapFnProperties(i18n) {
  let oldFn = i18n.properties;
  i18n.properties = function (settings, ...args) {
    if (settings.jsonResource) {
      i18n.mapFromJson = settings.jsonResource;
    }
    return oldFn.call(i18n, settings, ...args);
  };
}

// 如果当前已经加载完jquery.i18n.properties.js文件,就直接扩展它的能力
if ($.i18n) {
  // 扩展 jquery.i18n.properties 的能力
  init($.i18n);
}

export default {
  init: init,
};

然后在引入 jquery.i18n.properties.js 的 html 下面增加:

<script type="text/javascript" src="/lib/jquery.i18n.properties.js"></script>
<script type="text/javascript" src="/lib/i18n.expand.js"></script>

如果你们多页应用没有基类 html 文件的话,那如果有基类 js 的话,也可以在基类 js 里去初始化
尽量只在一个地方去初始化,省得需要每个 html 里去加代码

import i18nExpand from "./i18n.expand";
if ($.i18n) {
  // 扩展 jquery.i18n.properties 的能力
  i18nExpand.init($.i18n);
}

给 Vue 挂载个全局函数 $t 指向 $.i18n.prop

我们老项目里有引入 Vue 框架,但也仅仅引入 Vue,没有引入其他全家桶系列,只用来在有新改动时,可以局部性使用 Vue 的响应式编程

而这里老项目里没必要再引入个 Vue-i18n 框架了,直接给挂载个全局函数 $t 指向原本的国际化方案的翻译函数即可

if (Vue) {
  // 给 Vue 挂载全局函数
  Vue.prototype.$t = $.i18n.prop;
}

properties 转 json 的脚本

如果嫌弃 properties 格式的文件不好维护词条,可以写个脚本来转换:

/**
 * 将 properties 文件的国际化资源文件转成 json 格式文件
 * 脚本命令挂在 package.json 文件里
 * 注:由于 properties 文件的中文经过编码,该脚本会进行解码处理,以便中文可正常显示
 */
const vfs = require("vinyl-fs");
const map = require("map-stream");
const path = require("path");
const fs = require("fs");

const ROOT_DIR = path.resolve(__dirname, "./");
const fileRules = ["**/*.+(properties)"];
const jsonFile = "properties2json.json";

function ascii2native(value) {
  var character = value.split("\\u");
  var native1 = character[0];
  for (var i = 1; i < character.length; i++) {
    var code = character[i];
    native1 += String.fromCharCode(parseInt("0x" + code.substring(0, 4)));
    if (code.length > 4) {
      native1 += code.substring(4, code.length);
    }
  }
  return native1;
}

function run() {
  console.log("================================>start");
  let zhProperties = {};
  let enProperties = {};
  let curProperties = {};
  let res = {};
  const exist = fs.existsSync(path.resolve(ROOT_DIR, jsonFile));
  if (exist) {
    res = fs.readFileSync(path.resolve(ROOT_DIR, jsonFile), "utf-8");
    res = JSON.parse(res);
  }
  vfs
    .src(fileRules.map((item) => path.resolve(ROOT_DIR, item)))
    .pipe(
      map((file, cb) => {
        console.log("开始解析 =========================>", file.path);
        let count = 0;
        if (file.path.indexOf("_zh") > -1) {
          curProperties = zhProperties;
        } else {
          curProperties = enProperties;
        }
        let fileContent = file.contents.toString();
        fileContent.split("\n").map((line) => {
          if (line.indexOf("=") > -1) {
            count++;
            line = ascii2native(line);
            const [key, ...value] = line.split("=");
            // console.log(key, value);
            curProperties[key.trim()] = value.join("=").trim();
          }
        });
        console.log("词条数量:", count);
        console.log("解析结束 =========================>", file.path);
        cb();
      })
    )
    .on("end", () => {
      console.log("================================>end");
      //   console.log(zhProperties);
      //   console.log(enProperties);
      let unTranslate = {};
      Object.keys(zhProperties).map((key) => {
        if (enProperties[key]) {
          res[zhProperties[key]] = enProperties[key];
        } else {
          unTranslate[key] = zhProperties[key].trim();
          console.log("==>翻译丢失", key, zhProperties[key].trim());
        }
      });
      fs.writeFileSync(
        path.resolve(ROOT_DIR, jsonFile),
        JSON.stringify(res, " ", 2)
      );
      // fs.writeFileSync(
      //   path.resolve(ROOT_DIR, "unTranslate.json"),
      //   JSON.stringify(unTranslate, " ", 2)
      // );
    });
}

run();


总之,老项目的国际化原则就是控制影响面,降低维护成本,包括需要考虑交给新人去维护的情况

因此,能不改动到原方案就不改动,保持原方案不变的情况下,扩展支持跟 vue 项目一致的使用方式,以便国际化自动处理脚本也能够直接用来维护老项目

什么是布隆过滤器?

布隆过滤器是一种数据结构,具有快速插入和查找的特性,能确定某个字符串一定存在或者可能存在。布隆过滤器有着高效的空间利用率,它不存储具体数据,只存储数据的关键标识,所以占用的空间较小。它的查询结果可能会存在一定误差,但是误差总体可控,同时不支持删除操作。布隆过滤器的应用场景丰富,在任何仅需要知道数据是否存在,并不关心具体数据内容的场景都可以使用布隆过滤器,例如在网页爬虫中URL去重防止重爬、可以应用在缓存系统中,避免缓存穿透等问题、在安全领域,也可以使用它来快速判断一个请求是否属于黑名单ip,防止恶意攻击等。
布隆过滤器拥有的快速插入和查找的特性是否很像散列表?普通散列表一般依赖数组实现,而布隆过滤器为了节省空间,使用的
Bitmap
结构,即位图。

位图

位图本质上其实也是散列表的一种实现,不同的是位图节省空间体现在它利用二进制位存储数据状态。我们知道在ASCII编码中,一个英文字符占用一个字节(Byte),也就是8位(bit)。若是UTF8编码,中文或者特殊字符可能会占用更多字节。例如存在一个数组如下:

  Integer[] array = new Integer[5]{0,1,3,5,7};

以上的数组中,不考虑数组对象本身占用的空间,只计算元素空间,每个元素若只占8位,存储这5个元素就要占用40位。假如用二进制位存储这5个元素,则只需要8位即可。
oDhLBr.png
如上图,对应槽位的二进制位中,0代表不存在元素,1表示存在元素,当我们需要查询是否存在某个数字时,只需要看对应槽位的值是不是1即可,这样只需要一个8位空间即可表示0,1,3,5,7这几个元素了。

布隆过滤器的原理

上面说过,布隆过滤器是依赖位图实现的,它的原理是在位图的基础之上,定义了若干个散列函数。当需要向布隆过滤器中插入数据时,首先利用这几个散列函数分别计算出散列值,并且将位图上对应槽位的值设置成1,如果已经是1的话就不做任何操作。需要检查某个数据是否存在时也是同理,计算出要检查的数据的多个散列值,并且检查位图中对应槽位是否全都为1,但凡有一位不为1就可以断定值不存在,都为1的话值可能存在。
oDyU0t.png
为什么是可能存在呢?随着计算的数据越来越多,查询的结果也会慢慢出现一定几率误差。因为散列函数存在结果碰撞的问题,两个不同的值通过散列函数计算出的结果有概率相同。所以当某个值即便从来没有被插入过,通过这多个散列函数计算的出的槽位也可能和之前值相同,所以会误判为已存在。为了降低误差几率,需要按照具体需求调整散列函数的算法\个数和位图大小,确保通过散列函数计算出来的槽位足够均匀分布到位图上。
布隆过滤器不支持删除操作,是因为在位图中每个槽位只存在两种状态,即0和1。一个槽位被设置为1,但无法确定它被多少个关键字,通过哪些散列函数设置了多少次1,只知道它被置为了1,所以无法删除。

布隆过滤器的应用

Google提供的guava工具里面包含了布隆过滤器的实现。

<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>30.0-jre</version>
		</dependency>
  public static void main(String[] args) {
        Integer size = 1000000;
        BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), size, new Double(0.001));

        for (Integer i = 0; i < size; i++) {
            bloomFilter.put(String.valueOf(i));
        }

        Integer count = 0;
        for (Integer i = size; i < size * 2; i++) {
            if (bloomFilter.mightContain(String.valueOf(i))) {
                count++;
            }
        }

        System.out.println("误判率:" + count / new Double(size));
    }
误判率:0.00101

Process finished with exit code 0

测试结果和我们配置的参数大致吻合。

缓存穿透问题的产生,是当有大量的恶意流量去请求系统中不存在的数据时,缓存中没有对应数据,导致请求直接去查数据库,使数据库承受压力。而将布隆过滤器放在缓存之前则可以避免此问题,每当有流量来请求数据时会先在布隆过滤器中查询,请求是否是系统中的正常数据,如果是则放行流量去查缓存或数据库,否则直接忽略请求或者执行其他处理策略。

本文介绍在
Python
中,
numpy
库出现报错
module 'numpy' has no attribute '_no_nep50_warning'
的解决方法。

一次,在运行一个
Python
代码时,发现出现报错
module 'numpy' has no attribute '_no_nep50_warning'
,如下图所示。

其中,这一代码本来在
Windows
平台下运行是没有问题的,而换到
Ubuntu
平台后,就出现了这样的报错;由于这两个平台中,我的其他一些
Python
库配置的版本不一致,因此考虑到这一问题大概率就是版本不一致导致的。于是,对比了两个平台中
numpy
库的版本,发现确实不一致,如下图所示。其中,上图是
Windows
平台下的版本,而下图则是
Ubuntu
平台中的版本。可以看到,
Windows
平台下的版本确实是高于
Ubuntu
平台中的版本的。

因此,我们首先在
Ubuntu
的终端中,输入如下的代码,从而更新
numpy
库至
Windows
平台的版本
1.24.2

conda install numpy=1.24.2

随后,得到如下所示的界面,其中可以看到,如果用
1.24.2
版本的
numpy
库,我们的
Python
包是有一些冲突的。

但是,我这里暂时没有管这个冲突,而是直接继续安装;如下图所示。

可是,经过了上述的安装后,发现
Ubuntu
平台中的版本依然没有发生变化,且运行原本的
Python
代码还会出现如上图所示的报错代码。

随后,进一步发现,
Ubuntu
平台中
numpy
库的下载来源是
pypi
,而我此时用的是
Anaconda
环境;因此,考虑重新用
conda
安装一下
numpy
库。这里我们输入如下的代码。

conda install numpy

在这里,我就没有指定
numpy
库的版本,而是直接下载。随后,得到如下图所示的界面。

随后,运行原本的
Python
代码,发现就没有这一报错内容了。

至此,大功告成。

转载:
DNS中CNAME和MX记录的冲突

在DNS解析中,CNAME记录与其他记录往往是互斥的。最常见的是CNAME记录和MX记录的互斥。例如我们在http://example.com部署官网,通过CNAME解析到后端网关的IP地址。但是http://example.com往往也是我们的邮件地址,需要添加MX解析记录和SPF-TXT记录。如果有CNAME记录的存在,可能会导致他们失效(有时候也不会,要看实际访问的主机是否缓存了CNAME记录)。

那么你可能会问了,为什么http://example.com使用CNAME记录而不是A记录呢?在实务中,我确实更喜欢使用CNAME记录。我一般使用k8s集群,通过Ingress网关暴露到公网,此时所有服务都有统一的一组网关地址,我将一个统一的网关域名解析到该组地址,然后各个服务的域名分别解析到网关域名。如此一来,当网关的负载均衡IP发生改变时,只需要变更网关DNS的解析记录即可。又或者使用CDN等技术需要CNAME接入。

为什么CNAME和其他解析记录互斥?

图片

如何解决?

方法一

最简单的方法就是不用CNAME,改用A记录。

方法二

使用更现代的ALIAS记录替代CNAME记录,前提是DNS供应商需要支持ALIAS记录。目前,阿里云貌似不支持。

方法三

阿里云推出了显性URL解析记录。严格来说,这个不是记录,他其实也是A记录。它通过A记录,把域名解析到阿里云的服务器,然后阿里云通过301或者302的方式将请求转发到别的地方去。

可以把官网部署在http://www.example.com,然后http://example.com配置此解析,通过301转发到目标的http://www.example.com域名上。

当然,你也可以自己配置一台服务器,添加A记录,进行转发。但是这又涉及到需要自己维护IP变更的问题了,可以做个动态DNS解析。

参考:
1.DNS各类型互斥关系说明
https://messay.me/2019/07/14/DN