分类 编程相关 下的文章

dstat是一个Linux监控工具。可定制采集数据,可设置采集频率,可输出字符界面和导出CSV。默认一秒一条监测数据。其中以top开头的参数,可以记录检测类型最大值的进程。比如--top-cpu记录CPU占用最大的命令,--top-cpu-adv还会记录CPU占用最大的进程ID等。参数--time的时间格式,需要通过环境变量DSTAT_TIMEFMT进行定义。

示例操作命令:

# 设置时间格式
export DSTAT_TIMEFMT='%Y-%m-%d %H:%M:%S'

# 执行监测,并导出CSV文件
dstat --time --cpu --mem --disk --io --net --sys --top-cpu-adv --top-mem --top-bio-adv --top-io-adv --output /opt/dstat_log/dstat_$(date +%Y%m%d).csv

使用时,可结合tmux,随时查看其采集数据,即时输出在终端。导出的CSV文件,需要下载到本地,并使用第三方工具生成图表。

在众多监控方案中,dstat不算优秀的解决方案,而且只有采集数据的功能。其记录数据,采用CSV格式。如果终端不够宽时,不能完整显示每行的采集数据。而且CSV格式不好扩展,比如--top-cpu-adv记录的数据,不适合机器理解。这里记录一下相关经验。

1 正式版的bug

安装过0.7.3和0.7.4两个版本,并使用Python3运行,都存在以下两个Bug。幸好是使用Python开发,可以直接修复。其安装路径为/usr/bin/dstat

a)在Debian 10以上使用Python3运行时,出现以下Bug:

/usr/bin/dstat:2619: DeprecationWarning: the imp module is deprecated in favour of importlib and slated for removal in Python 3.12; see the module's documentation for alternative uses
  import imp
Terminal width too small, trimming output.
Traceback (most recent call last):
  File "/usr/bin/dstat", line 2847, in <module>
    main()
  File "/usr/bin/dstat", line 2687, in main
    scheduler.run()
  File "/usr/lib/python3.10/sched.py", line 151, in run
    action(*argument, **kwargs)
  File "/usr/bin/dstat", line 2806, in perform
    oline = oline + o.showcsv() + o.showcsvend(totlist, vislist)
  File "/usr/bin/dstat", line 547, in showcsv
    if isinstance(self.val[name], types.ListType) or isinstance(self.val[name], types.TupleType):
NameError: name 'types' is not defined. Did you mean: 'type'?

解决办法,参考以下文档:

简单来说,改两行代码。如下:

# 第547行,改为:
if isinstance(self.val[name], (tuple, list)):

# 第552行,改为:
elif isinstance(self.val[name], str):

b)--top-mem参数统计错误的bug

参考文章:

修改方法def proc_splitline(filename, sep=None),改为:

if filename.startswith("/proc/") and filename.endswith("/stat") and filename != "/proc/stat":
    tmp = linecache.getline(filename, 1).split(sep)
    it = [i for i,c in enumerate(tmp) if c.endswith(')')]
    it = 2 if not it else it[-1]+1
    return tmp[0:1] + [' '.join(tmp[1:it])] + tmp[it:]
else:
    return linecache.getline(filename, 1).split(sep)

2 应用场景

感觉比较适合单机版,或者指定采集一些系统数据。不适合生产机大规模部署。如果非要用dstat不可,可以考虑 dstat + Fluentd + Influxdb + Grafana 这种组合方案。

最近因工作需要,研究了一下Linux服务器的监控方案,收获颇丰。

1 监控需求

服务器需要监控什么?可以分为硬件和软件,或者系统数据和业务数据。一般的监控解决方案,都是针对硬件、操作系统和常用软件(比如数据库、Docker之类)。涉及业务数据,需要二次开发。

2 监控方案设计

一般的监控方案,分为采集、存储、展示、告警,这四大模块或者功能。针对被监控服务器的数量,可灵活实施四个模块的部署方式。比较完善的整体解决方案,还包括“控制”模块,实现服务器集群的统一管理。

2.1 采集

数据采集程序,或者叫“探针”,一般是部署在被监控服务器上的程序。用于采集相关数据,要求占用系统资源小(主要是CPU、内存、磁盘、网络等),对系统影响小。

数据采集的方式,可以采用“推”(push)和“拉”(pull)模式。

“推”是数据采集程序主动把数据从采集端发到存储端。数据具有良好的实时性,方便内网部署并推送到外网服务器。但采集端太多,或者采集的数据比较大,需要考虑存储端的承受能力。遇到存储端没有收到数据时,不能确定是网络问题还是采集端问题。

“拉”是数据采集程序暴露出来,例如开放HTTP服务的端口,存储端去访问并获取数据。存储端也有更多的主动权,决定拉取频率,甚至决定采集样本,能降低带宽、减轻存储端的压力。采集不到数据时,能区分网络问题和采集端问题。如果对数据有实时需求,采集端需要保存未拉走的数据。

针对采集端有时只能部署在内网的情况,有的解决方案会提供“代理”或者“跳板”功能,实现采集端与存储断之间的数据连接。

2.2 存储

数据存储,就是把监控数据持久化,可以是文件(比如CSV文件),也可以是数据库(比如MySQL、InfluxDB)。目前主流的方案,基本采用时序数据库,例如InfluxDB。专门针对这种大量连续时间的数据,提供存储、查询、统计等功能。

2.3 展示

数据展示,一般是把监控数据生成图表,以便更直观地查看和分析。简单的方案是用Microsoft Excel之类的软件,根据导出CSV文件的数据,生成各种图表。主流和灵活的方案是使用可视化软件,例如Grafana,连接时序数据库并生成各种图表。Grafana能够实现实时展示和历史分析。

2.4 告警

通过检测采集的数据,发现超过指定危险指标时,向相关人员发送消息,就是告警。由于相关人员一般不会24小时盯着服务器,所以需要机器进行告警。InfluxDB、Prometheus、Grafana等都有告警功能。一般开源系统只提供邮件或Web Hook(调用钉钉接口)通知,商业系统(例如:阿里云的云监控)会有短信或电话通知。

3 解决方案

服务器监控的解决方案,像编程语言一样,没有一个万能方案,需要根据情况进行选择。这里列举一些相关软件或方案。

3.1 dstat

基于Python。默认一秒采集一条数据,数据定制性高。数据可显示在终端,也可导出CSV文件。需要使用第三方软件,例如Microsoft Excel之类,生成图表和分析数据。高级玩法是,搭配Fluentd,保存数据到InfluxDB,再用Grafana展示、分析。

优点是占用资源小,数据简单。缺点是由于使用CSV格式,复杂数据记录得不够好。而且当前版本(0.7.4)有bug,部署时需要自己修正。

3.2 njmon

C语言开发。nmon的升级版,可设置不收集的数据,数据格式采用JSON。不支持终端显示,原生支持导出文件和发送到InfluxDB。有官方工具处理保存的JSON文件。推荐的玩法是njmon + InfluxDB + Grafana。

优点是占用资源小,作者对整个监控方案考虑比较全面。缺点是对AIX较好但对Linux支持不足。比如Debian 11需要自己编译。Grafana的njmon模板大多针对AIX,而且仅有的Linux模板是针对InfluxDB v1,即使用InfluxQL而不是Flux。

3.3 glances

基于Python,开源跨平台,界面优秀。支持三种模式:单独运行、C/S、Web。提供XML-RPC服务、RESTful JSON接口,也可把数据保存到其它系统,包括InfluxDB。本身支持配置“Actions”,根据事件触发相应脚本,实现告警。

占用资源较大(包括CPU和磁盘空间),界面优秀,玩法多。适合桌面系统的监控。

3.4 InfluxDB

InfluxDB只是个时序数据库,但是该公司开发了Telegraf作为数据收集工具(采用“推”模式),并且InfluxDB新增了告警和图形化展示,形成一个完整的数据收集方案。

3.5 Prometheus

基于Go,监控、告警工具,使用“拉”模式采集数据。

3.6 Zabbix

企业级的开源的服务器监控管理系统,是完整的解决方案,基本可以替代阿里云的云监控之类的系统。Web控制台基于PHP,支持中文显示;采集端基于C,升级版改为Go;数据存储使用MySQL,未支持时序数据库是最突出的缺点。另外,告警功能不支持电话和短信通知。

非常适合企业内部管理服务器集群,便于运维人员使用。

3.7 阿里云的云监控

一般云主机的服务商都提供云监控功能,且基本监控免费,高级功能收费。阿里云的云监控,还能监控非阿里云的主机。使用这些云监控前,要确定是否可以把服务器监控数据发给云厂商,甚至安装云厂商的采集软件。

4 方案选型

根据不同的情况,总结一下各个方案的选型。

4.1 云主机

如果是购买云主机,可以考虑云服务提供商的云监控,一般免费提供基础监控功能,例如阿里云的云监控。但前提是,云主机可以安装云监控的采集端软件,并且接受相关数据上传到云服务提供商那边。另外,高级监控功能,需要付额使用。

4.2 运维管理的服务器集群

企业内部,有专门的运维人员管理服务器,针对硬件或操作系统相关数据的监控,则可以考虑Zabbix。

4.3 业务数据

node_exporter + Prometheus + Grafana,或者 Telegraf + InfluxDB + Grafana,这种方案适合收集业务日志。部署了InfluxDB,还能存储其它数据,个人觉得比较好玩。

近来项目遇到要显示SVG图像文件。本来SVG已不是新鲜事物,应该很好使用,现实并非如此。

设计说用AI(Adobe Illustrator)做出来的图,用Chrome打开会出现错误,比如图形位置不对、该显示的图形没显示等。用文本文件打开那图,会显示xml标签开头,而不是svg标签开头。

图片显示有误的问题,只能让设计去修正了。至于文件格式的问题,找到svgcleaner这个工具,转换一下就好了,体积还有所减少。虽然svgcleaner貌似不更新了,但工作正常。

以前一直用Inkscape代替曾经的Flash,设计矢量图,比如图标之类,还不错。不知道为什么大厂出品的AI会是这么多问题。

近来遇到关于库存与并发的问题。由于一直接触的系统都是没有考虑过商品库存的并发,加上解决过的并发问题,也只是简单直接地采用锁表的方式。所以导致踩坑。

1 问题1,商品基础数据与库存数量,设计在同一个表。

商品基础数据,包括库存数量,主要用于查询。但库存数量,还要解决经常变化,且可能出现并发的情况。如果简单使用锁,即使只锁一行数据,也会导致正在进行下单(涉及扣减库存)的商品不能被浏览(因为锁住,不能查询)。

为了减轻这个情况,下单时,检查库存数量是否足够购买时,不锁数据,等到保存订单数据,真正扣减库存时才加锁。本想着通过减少锁数据的时间,减少商品数据不能查询的情况。但是系统采用Java开发,使用了Spring + Hibernate框架。而Hibernate在事务内使用了一级缓存,即事务内未提交时,查询到的业务数据都放到一级缓存。事务内查询时,会先查询一级缓存,若命中,则不再查询数据库。就导致了检查库存时已获取了商品数据,扣减库存时(从一级缓存获取)不能获取到最新库存(特别是两个客户同时下单同一个商品的情况),最后在并发情况下扣减库存,就出现库存扣少1了的问题。

解决方案很简单,把商品基础数据与库存数据分开两个表存放。库存数据在扣减时,不影响商品浏览。

2 问题2,库存数量,需减少锁定时间。

由于客户浏览商品,或者添加商品到购物车,都需要查询库存数据。如果使用悲观锁,即锁表或锁数据后不能查询,会导致客户不能浏览。参考了以下文章,决定使用乐观锁,即不使用数据库锁。

目前系统规模比较小,且没有涉及分布式,于是决定在扣减库存时直接更新数据的方式。即使用update语句扣减库存时,用where条件判断是否足够扣减,并返回是否扣减成功。

由于使用MySQL,update语句不能返回指定数据(但是,sql server可以使用update...output,PostgreSQL可用update...returning)。加上Hibernate不能同时执行update和select两个语句,最后采用存储过程。参考以下网址:

3 解决方案

总的来说,使用乐观锁(即没有使用数据库的锁),并利用MySQL存储过程实现扣减库存后返回结果。

1)库存表

create table `product_stock` (
    `productId` bigint not null comment '商品ID',
    `instock` int not null default '0' comment '库存数量',
    `createTime` datetime(3) default null comment '创建时间',
    `updateTime` datetime(3) default null comment '更新时间',
    primary key ( productId )
) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_0900_ai_ci comment='商品库存';

2)扣减库存的存储过程

利用存储过程的out参数,返回扣减结果。当outUpdateQty返回的值大于零,扣减成功,否则失败。扣减成功,outStockAfter的值才是正确。

delimiter //
create procedure `product_reduce_instock`(
    in inProductId bigint, /*传入参数:商品ID*/
    in inReduceQty int, /*传入参数:扣减数量*/
    out outUpdateQty int, /*传出参数:实际扣减数量*/
    out outStockAfter int /*传出参数:更新后库存数量*/
)
begin
    -- 初始化返回的值
    set @updateQty=0;
    set @stockAfter=0;
    
    -- 执行扣减库存
    update product_stock 
    set instock = (@stockAfter := instock - (@updateQty := inReduceQty)), updateTime = now() 
    where productId = inProductId and instock >= inReduceQty;
    
    -- 传出参数赋值,即返回扣减结果
    set outUpdateStock=@updateQty;
    set outStockAfter=@updateQty;
end //

本文记录了从jQurey转到原生JavaScript开发的相关处理。

一 历史

二十一世纪初,IE 6还在统治浏览器的时代,出现了一批JavaScript框架。除了提高前端开发效率,还屏蔽了各个浏览器的JavaScript接口差异。那时有3个产品印象比较深刻:

  1. prototype,http://prototypejs.org/
    其特点是在原生JavaScript基础上做扩展,定义通用的方法或接口,屏蔽各个浏览器的差异。很轻量,个人比较喜欢。
  2. Ext JS,https://www.sencha.com/products/extjs/
    数据与界面分离,提供丰富的UI组建,便于页面开发。当时浏览器JavaScript性能不高,用起来不够流畅,不适合简单排版布局的页面。但是对于开发一些管理系统,确实很方便。
  3. jQuery,https://jquery.com/
    最大特别是查找HTML元素很方便(前提是熟悉其搜索语法),有点函数式编程的味道。在那个需要手工修改HTML界面的年代,确实很方便。

二 当前

看看当前的浏览器,已经是Webkit内核的天下,加上IE已亡,ECMAScript 6普及……各个浏览器的JavaScript兼容性大大提高。所以,我们可以直接采用浏览器原生JavaScript,替代jQuery这类用于遍历或搜索DOM的框架。当然,复杂的界面,主要是响应式前端框架(AngularJS、React、VUE)的世界。

三 实现方法

主要参考这个文章,从jQuery转到原生JavaScript。

另外,对于页面上的异步请求(ajax),该文章没有提出timeout的处理。以下整理一个示例:

// 请求错误的类,用于传递错误信息
let RespError = class {
  constructor(code, msg, respJson) {
    this.code = code;
    this.msg = msg;
    this.respJson = respJson;
  }
};

// POST提交Json数据。调用ajaxJson方法前加上async就是同步调用,直接调用就是异步调用
// 默认超时10秒
let ajaxJson = async (url, formParam={}, onSuccess=(respJson)=>{}, 
    onFailed=(respError)=>{}, timeoutSec=10) => {
  let controller = new AbortController();
  let timeoutId = setTimeout(() => {
    // 超时后停止请求
    controller.abort();
    // 抛出超时的错误
    onFailed(new RespError(-1, 'TIMEOUT', null));
  }, timeoutSec * 1000);
  try {
    // 发起请求
    let response = await fetch(url, {
        signal: controller.signal, // 用于接收中断请求信号
        method: 'POST',
        cache: 'no-cache',
        headers: {
          // 声明请求的参数是JSON
          'Content-Type': 'application/json; charset=UTF-8'
        },
        body: JSON.stringify(formParam)
    });
    // 注意,响应的数据只能获取一次,包括response.json()和response.text()
    let respJson = await response.json();
    if(!response.ok) {
      // 请求失败,抛出自定义的错误对象
      throw new RespError(response.status, response.statusText, respJson);
    }
    onSuccess(respJson);
  } catch(err) {
    onFailed(err instanceof RespError ? err : new RespError(-1, err.message, null));
  } finally {
    // 请求结束,停止执行定时函数。避免相应成功后,抛出超时的错误。
    clearTimeout(timeoutId);
  }
};

关于Fetch的使用,参考:

  1. Fetch API 教程
    https://www.ruanyifeng.com/blog/2020/12/fetch-tutorial.html
  2. mdn web docs - 使用 Fetch
    https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch

四 后记

推荐搭配 petite-vue ,实现数据与页面元素的绑定。

用过Flutter,对数据的响应式,有进一步了解。于是,回过头来再看看VueJS,自然而然地理解了。

找到一个比较好的Vue3示例代码,展示了使用Vue3的主要代码,涉及Vue3的模板语言、组件等。

Vue3 起步简单示例
https://blog.csdn.net/thankseveryday/article/details/124741733

我在代码加上注释,更容易理解:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://unpkg.com/vue@next"></script>
<title>Learn Vue3</title>
</head>

<body>
<!-- 界面模板 -->
<div id="app">
    <!-- 两个大括号,获取data()方法返回对象的属性的值,给文本绑定值 -->
    <p>{{counter}}</p>
    <p>
        <!-- :title是v-bind:title的缩写,给HTML标签的属性绑定值 -->
        <span :title="message">{{message}}</span>
    </p>
    
    <!-- @click 是 v-on:click 的缩写,指定事件的执行方法 -->
    <p><button @click="reverseMessage">翻转文字</button></p>
    
    <!-- “v-”前缀的特殊属性,是Vue的指令 -->
    <!-- 使用 v-model 指令来实现双向数据绑定 -->
    <p><input type="text" v-model="message"></p>
    
    <!-- v-if ,当值为true时,才显示对应HTML -->
    <p v-if="seen">你能看到我吗?</p>
    
    <p><button @click="seenYN">显示/隐藏</button></p>
    <h4>我爱吃的水果:</h4>
    <ul>
        <!-- v-for,循环输出HTML -->
        <li v-for="fruit in fruits">{{fruit}}</li>
    </ul>
    <h4>周末计划:</h4>
    <ol>
        <!-- todo-item对应自定义组件TodoItem -->
        <todo-item v-for="todo in todos" :todo="todo" :key="todo"></todo-item>
    </ol>
</div>
<script>
// 定义自定义组件
const TodoItem = {
    props: ['todo'],
    template: '<li>{{todo}}</li>'
}

// 数据对象,有点像Flutter的State或者Provider
const dataObj = {
    // 声明用到的自定义组件
    components: {
        TodoItem
    },
    
    // 定义数据对象,有用的属性可以先设置null,用于占位
    data() {
        console.log("vue3 demo, data()");
        return {
            counter: 0,
            message: "hello vue",
            seen: true,
            fruits: ["apple", "orange", "bananas"],
            todos: ["钢琴课", "绘画课", "看电影"]
        }
    },
    
    // 在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图
    created() {
        console.log("vue3 demo, created()");
    },
    
    // 在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
    mounted() {
        console.log("vue3 demo, mounted()");
        setInterval(() => {
            this.counter++
        }, 1000);
    },
    
    // 操作数据的方法,一般用于页面元素的事件处理
    methods: {
        reverseMessage: function () {
            // 操作数据,让Vue更新界面
            this.message = this.message.split('').reverse().join('');
        },
        seenYN: function () {
            this.seen = !this.seen;
        }
    }
}

// 数据与模板绑定
Vue.createApp(dataObj).mount('#app')
</script>
</body>
</html>

更详细的介绍和相关文档,可以访问Vue3的官网:
https://v3.cn.vuejs.org/guide/introduction.html#vue-js-%E6%98%AF%E4%BB%80%E4%B9%88

近来工作中用上了Flutter,并且使用了Provider作为状态管理,确实爽,但是也踩了一下坑。

一 概述

Provider是基于InheritedWidget组件,使用观察者模式 + 生产者消费者模式,实现状态共享,简直就是为了取代StatefulWidget而存在。相关资料:

Provider的Flutter插件网址:
https://pub.dev/packages/provider

Provider的官方中文说明:
https://github.com/rrousselGit/provider/blob/master/resources/translations/zh-CN/README.md

二 总结

  1. Provider可以定义在任意地方,其状态只提供给其子Widget访问。例如,定义在App之上可实现全局的状态共享的状态,定义在页面之上可实现页面内的状态共享。
  2. Provider是利用BuildContext对象传递,实现状态共享,也因此只能实现子级可访问。
  3. Provider的子Widget(即child参数)不能赋予创建好的Widget对象,否则后面的所有孙Widget不能通过BuildContext对象获取其状态。解决方案是使用builder参数,传入构建子Widget的函数,或者child参数设置带有builder函数的Widget,例如Builder对象。简单来说,实现BuildContext对象传递,就要父级创建后,利用创建子级的builder方法,把带有父级Provider状态的BuildContext对象传递给子级。
  4. 数据变化,必然导致重绘。所以不要过于担心是否重绘,而重点关注重绘的点在哪里,如何减少重绘的Widget。重绘Widget,会向上找到最近的builder方法并执行。所以需要重绘的Widget,最好放在其builder方法内。需要变化的StatelessWidget对象,用Builder类的builder方法包裹,是个很好的做法。
  5. Provider是以类型区分数据的。如果是多个相同数据类型(例如int类型)的状态,则需要定义不同的类,且都含有该数据类型(例如int类型)的属性。
  6. 定义多个Provider,可以使用MultiProvider。
  7. 组合多个Provider对象,可以使用ProxyProvider。

三 Provider类型

一般使用ChangeNotifierProvider就可以,更多的Provider类型如下:

类型描述
Provider最基础的 provider 组成,接收一个任意值并暴露它。
ListenableProvider供可监听对象使用的特殊 provider。ListenableProvider 会监听对象,并在监听器被调用时更新依赖此对象的 widgets。
ChangeNotifierProvider为 ChangeNotifier 提供的 ListenableProvider 规范,会在需要时自动调用 ChangeNotifier.dispose。
ValueListenableProvider监听 ValueListenable,并且只暴露出 ValueListenable.value。
StreamProvider监听流,并暴露出当前的最新值。
FutureProvider接收一个 Future,并在其进入 complete 状态时更新依赖它的组件。

四 监听方式

获取Provider的状态,有以下三种方式:

  1. read,即只读。只获取状态,不进行监听。示例代码:
// 使用Provider.of,需要加上参数“listen: false”
T t = Provider.of<T>(context,listen: false));

// 使用context.read方法最简单
T t = context.read<T>();
  1. select,即只监听指定数据。指定数据有变化,才会执行重绘。注意:如果监听对象(包括List对象),只有对象的内存地址变化了,才会执行重绘。对象的属性(包括List对象的元素)变化,不会引起重绘。
// 使用Selector类,可以定义builder方法
Selector<T, R>(
  selector: (_, t) {return t.r;},
  builder: (_, r, __) {return Text('${r}');}
);
    
// 使用context.select方法最简单。如果取出的数据需要重绘,则最好用Builder类包裹一下
R r = context.select<T,R>(R cb(T value));

// Flutter Provider Selector数据更新问题优化 
// [https://blog.csdn.net/Code1994/article/details/124720388][3]
  1. watch,即监听状态的变化。状态有任何变化,都会执行重绘。
// 使用Consumer类,可以定义builder方法
Consumer<T>(
  builder: (_, t, __) {return Text('${t.r}');}
);

// 使用Provider.of方法。如果取出的数据需要重绘,则最好用Builder类包裹一下
T t = Provider.of<T>(context);

// 使用context.watch方法最简单。如果取出的数据需要重绘,则最好用Builder类包裹一下
T t = context.watch<T>();

最近完成了一个小项目的数据库迁移,从微软SQL Server 2016迁移到MySQL 8。过程没什么复杂,只是需要注意一下数据类型和SQL语法的转换。

1 环境

原数据库是SQL Server 2016。迁移的目标环境,操作系统为Debian 11,安装了MySQL 8。

2 还原SQL Server数据库备份

拿到手的是SQL Server数据库备份,需要还原出来再迁移。幸好微软推出了SQL Server的Linux版,而且官方提供了可用于开发测试的Docker镜像,几个步骤就部署并还原好SQL Server数据库。

参考资料:

1)在Debian上安装Docker的官方教程:
Install Docker Engine on Debian
https://docs.docker.com/engine/install/debian/

2)运行SQL Server 2019 Docker镜像的官方教程:
Quickstart: Run SQL Server container images with Docker
https://docs.microsoft.com/en-us/sql/linux/quickstart-install-connect-docker?view=sql-server-ver15&pivots=cs1-bash

3)SQL Server 2019的微软官方Docker镜像:
dockerhub - Microsoft SQL Server
https://hub.docker.com/_/microsoft-mssql-server

3 MySQL的准备

由于SQL Server的数据库表名不区分大小写,MySQL为了兼容相关SQL语句,也需要设置表名不区分大小写。即设置MySQL的参数lower_case_table_names=1,MySQL在存储和查询时,都把表名转为小写后再执行处理。

这里最麻烦的是,如果MySQL原来设置了lower_case_table_names=0(一般Linux上安装MySQL的默认值),需要把data文件夹清空,更新设置后重新初始化MySQL的数据。如果直接更改该值,MySQL重启后会报错。

关键的操作步骤:

1)修改MySQL的配置文件(Debian的默认路径为:/etc/mysql/mysql.conf.d/mysql.cnf),在[mysqld]节点下,加入一行lower_case_table_names=1

2)重新初始化MySQL(已有数据库的话,先做好备份,初始化后再还原),先清空数据文件夹(Debian的默认路径:/var/lib/mysql),然后执行以下命令:

mysqld --user=root --initialize --lower-case-table-names=1

初始化成功后,root用户的密码会记录在/var/log/mysql/error.log

4 迁移数据库定义

即导出原数据库表的create语句。一般推荐使用MySQL Workbench的Migration功能,官方文档如下:

MySQL Workbench - Using the MySQL Workbench Migration Wizard
https://dev.mysql.com/doc/workbench/en/wb-migration-wizard.html

但是我所安装的MySQL Workbench不能连接到Docker部署的SQL Server,所以使用了已安装的HeidiSQL,导出原数据库表的create table语句,然后手工修正为MySQL的语法。一些修改操作如下:

  • 修正字符编码,特别是设置了COLLATE的,需求改为COLLATE utf8mb4_0900_ai_ci
  • 修正默认值设置,例如DEFAULT '(0)'改为DEFAULT '0'DEFAULT getDate()改为DEFAULT CURRENT_TIMESTAMP
  • 自增型字段会被忽略,需要加上AUTO_INCREMENT
  • 字段类型转换,例如NVARCHAR改为VARCHARBIT改为TININY(1)MONEY改为DECIMAL(19,4)
  • 需要补上索引设置。

5 迁移数据库的数据

即导出所有数据的insert语句,然后在目标数据库利用source命令进行导入。一般也是推荐使用MySQL Workbench操作,不用担心语法和数据类型的问题。

我使用了DBeaver导出所有表的insert语句,然后手工修正为MySQL语法。需要注意:

  • 一般一条insert语句包含10000行数据,已提高导入时的效率。
  • 所有表名以数据库名.dbo开头的,都改为以数据库名开头。
  • 列名以中括号“[]”括住的,要改为“\`”。

6 修改程序的SQL语句

主要是把SQL Server的语法,改为MySQL的语法。总结如下:

  • TOP改为LIMIT
  • getDate()改为CURRENT_TIMESTAMP
  • 去掉表名前的dbo.
  • WITH(NOLOCK)的处理。SQL Server加了WITH(NOLOCK)的语句,如果MySQL的InnoDB设置innodb_autoinc_lock_mode=0,需要特殊处理该语句,否则直接去掉WITH(NOLOCK)。关于InnoDB的设置说明如下:

MySQL innodb_autoinc_lock_mode 详解
https://www.cnblogs.com/JiangLe/p/6362770.html

当Service方法被内部调用时,Spring注解会失效。就是Spring的Service类,如果public方法加上注解,类内部的其它方法使用this调用该方法,会导致注解失效。

例如Spring的Service实现类如下:

@Service("userService")
class UserServiceImpl implements UserService{
    
    @Override
    @Cacheable(CacheNames="USER_CACHE", key="#userId")
    public User getUser(String userId){
        // do something
    }

    @Override
    public String getUserName(String userId) {
        User u = this.getUser(userId); // getUser方法的@Cacheable注解失效了
        return u == null ? "" : u.getName();
    }
}

原因是this引用的对象没有被Spring代理,调用该对象的public方法时,Spring不能处理相关注解。

解决方法很简单,就是使用Spring代理过的对象,代替this。然后只需解决如果获取该Service类被Spring代理过的对象。


1 循环依赖

就是自己注入自己。在Service类定义一个自身对象的属性,让Spring装配时把自己注入到自己。虽然Spring 5(具体版本待查证)声称解决了循环依赖的问题,但是Spring Boot 2.6.0开始默认设置不允许循环依赖。循环依赖是一个古老的问题,一样认为要避免。所以此方法不推荐

1)先设置

# Spring Boot 2.6.0之后,允许循环依赖
spring.main.allow-circular-references = true

2)上面的例子改为

@Service("userService")
class UserServiceImpl implements UserService{
    
    @Autowired
    private UserService self; // 自己注入自己
    
    @Override
    @Cacheable(CacheNames="USER_CACHE", key="#userId")
    public User getUser(String userId){
        // do something
    }

    @Override
    public String getUserName(String userId) {
        User u = self.getUser(userId); // 用self代替this,注解生效
        return u == null ? "" : u.getName();
    }
}

2 获取装配后的自己

避免Bean的循环依赖,主要思路是,在Bean装配完成后,再获取被Spring代理的自己。至于怎样获取,实现方法是多种多样的。

方法1,从Bean容器中获取自己。即:

UserService self = applicationContext.getBean("userService");

至于怎么获取Bean容器applicationContext,方法也是多样的。

方法2,开启AspectJ自动代理来获取自己。

详细参考:一个Spring AOP的坑!很多人都犯过!

要注意,AspectJ自动代理不只是解决本文档的问题,需考虑是否会带来未知的问题。

开启AspectJ自动代理的方法有多种,这里列出三种:

1)在启动类添加注解:

@EnableAspectJAutoProxy(proxyTargetClass=true, exposeProxy=true)

2)Spring增加配置:

<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true" />

3)Spring Boot的配置文件增加配置:

# 开启AspectJ自动代理
spring.aop.auto=true
# 开启CGLIB代理
spring.aop.proxy-target-class=true

然后就可以在当前Service类的方法中,通过类似的代码调用自身的方法,且能保证该方法的注解正常执行:

User u = ((UserService) AopContext.currentProxy()).getUser(userId);

方法3,延迟执行自己注入自己。

很简单,就是使用@Lazy注解,达到Bean初始化不执行自己注入自己,避免循环依赖的错误。我记得解决Spring 2.x循环依赖的问题时,也是采用延迟注入的配置。此方法写的代码最少,目前倾向采用这种方法。于是,上面的代码改为:

@Service("userService")
class UserServiceImpl implements UserService{
    
    @Lazy
    @Autowired
    private UserService self; // 延迟自己注入自己
    
    @Override
    @Cacheable(CacheNames="USER_CACHE", key="#userId")
    public User getUser(String userId){
        // do something
    }

    @Override
    public String getUserName(String userId) {
        User u = self.getUser(userId); // 用self代替this,注解生效
        return u == null ? "" : u.getName();
    }
}

XDA论坛上看了很久,Android 11就只有crDroid有官方支持。加上Resurrection Remix OS用得真不爽,主要是自动杀进程,而且不分前后台。于是五一假期狠下心刷上crDroid,用了一段时间,很爽!流畅、不杀进程、支持国产支付app的指纹,续航还行。

1. 相关下载

1.1. crDroid ROM

当前版本是7.5。注:目前此ROM版本处于无人维护状态!
官网不仅可以下载ROM,还提供小米官方固件(欧版MIUI)、Recovery、OpenGapps等的链接:
https://crdroid.net/picasso/7

1.2. 欧版MIUI

目前最新版是12.1.8,据说今年6月会推出12.5适配。当然,这不是重点。下载地址:
xiaomi.eu_multi_HMK305G_V12.1.8.0.RGICNXM_v12-11.zip
https://sourceforge.net/projects/xiaomi-eu-multilang-miui-roms/files/xiaomi.eu/MIUI-STABLE-RELEASES/MIUIv12/

1.3. Recovery

crDroid推荐的Recovery是OrangeFox。界面跟Twrp非常不同,感觉更像是文件管理器,但功能还是一样。
OrangeFox-picasso-stable@R11.1_1.7.zip
https://orangefox.download/zh-CN/device/picasso

1.4. Margisk

目前几乎是获取root权限的唯一方案。下载地址:
https://github.com/topjohnwu/Magisk/releases

要注意,从Margisk 22开始,root安装包和root管理工具,都合并为一个apk文件。root系统时,只需用Recovery刷入该apk文件。

1.5. Gapps

由于OpenGapps的Android 11版还没正式发布(当前处于测试),所以我选用NikGApps。下载链接:
https://nikgapps.com/downloads.html#downloads

一般选在Core版就可以了。值得注意的是,如果要开启Google Assistant(Google助手),需要把Google Velvet(即Google搜索)安装为系统App。NikGApps提供了对应的Addons(文件名为:NikGapps-Addon-11-Velvet-signed.zip),可以通过Recovery直接刷入系统。

2. 刷机过程

2.1. 解锁及刷Recovery

都是常识性操作。刷Recovery后,最好先备份当前系统。

2.2. 备份数据

钛备份不再更新了,推荐使用OAndBackupX备份App及数据。可通过F-Droid安装:
https://f-droid.org/en/packages/com.machiav3lli.backup/

还有联系人和短信的备份,不再赘述。

2.3. 刷机步骤

按刷机顺序排列,crDroid的方案是:OrangeFox -> 欧版MIUI -> crDroid -> Margisk -> OpenGapps。我的方案只是用NikGApps替换OpenGapps。另外,最好先“双清”再开刷。

3. 初始设置

3.1. 去掉WiFi和信号出现“x”符号

需要使用adb执行以下命令:

adb shell settings put global captive_portal_mode 0
adb shell settings put global captive_portal_https_url https://www.google.cn/generate_204

命令执行成功后,手机开启飞行模式,然后关闭飞行模式,即可解决。
参考:https://www.uso.cn/post/view/47990

3.2. 开启Google Assistant(可选)

初次进入系统后,不插入SIM卡,语言选English(US)。安装Google Assistant并设置开启。成功后,再插入SIM卡,并设语言为中文。


更新 2023-06-22

1. 针对此机的crDroid 9(Android 13)终于有人维护。

详见:https://crdroid.net/picasso/9

刷机时注意:

  • 需要“双清”数据后全新刷入。
  • 需要更新对应的Firmware和Recovery。

2. GMS耗电的问题。

原因:可能是Android的安全补丁一直没更新,Google Play服务会不断尝试后台下载更新,但非Pixel手机会下载失败。

3. ROOT方案。

旧的ROOT方案是使用Magisk。隐藏ROOT是开启Zygisk,再安装Shamiko模块(Shamiko发布地址)。缺点是容易被应用检测到,导致不能适应,例如国内的各大银行app。

新的ROOT方案是使用KernelSU。内核集成ROOT功能,只需安装KernelSU管理应用即可使用(KernelSU发布地址)。缺点是需要内核支持,可玩模块也没那么多。优点是隐蔽性强,几乎不被检测到。

4. system_server高占用CPU。

在crdroid 9.10到9.12都出现,直接导致手机电量消耗过快,而且偶然出现。目前只能通过重启系统解决。

5. 卡屏。

一般是切换应用时“卡屏”,就是屏幕画面不变,且不接收任何输入。一般等几秒就恢复。“阿里系”的应用,例如“支付宝”、“淘宝”,切换时最容易出现。