最近开发个APP
项目用到了Github
上的Golang
开源的第三方库。虽然项目有一直有在更新,但是提交审核时,系统提示说引用了苹果未公开(私有)或者已经废弃的API
)。
Guideline 2.5.1 - Performance - Software Requirements
Your app uses or references the following non-public or deprecated APIs:
…
打开项目工程,全局搜索了关键字,结果什么都没有搜到 XD
系统也很贴心,不但帮你找出了问题还教你怎么去解决这个问题,上述提到了两个命令工具:
strings
检测otool
工具(xcode
自带,可以直接在终端中使用)otool -L appName # 查看可执行程序都链接了那些库
otool -ov appName # 输出Object-C类结构以及定义的方法
另外还有一种更简单的方法:
grep
全局搜索grep -r KeyWord . (r 后面一个空格,然后键入关键字,注意名称后面敲一个空格,然后键入.)
例子:
1 | # grep -r KeyWord . |
一行命令就能找到工程里面引用未公开(私有)或者已经废弃的API
,但是没有提示具体是什么库,你学废了么。
接下来就可以根据经验来使用排除法定位了XD。
过程忽略不计,接下来就需要来升级第三方库或者删除未用到的库就行了(强行回到主题)。
怎样在改动最小的情况下,替换成自己二次开发的包呢?
具体步骤如下:
Fork
原有的Package
,例如github
就直接star
+fork
(你的代码是我的了!)Release
)go mod edit -replace github.com/original/gopackage@version=github.com/you/go-git@version
在第三步时,可以看到go.mod
下面最后会多出来一行:
1 | module PROJECT_NAME |
go.mod
文件,是一个非常重要的文件。
该文件中,通过4个指令来声明module信息。用于控制版本的选择。
一共有4条指令,如下所示:
module
: 声明module
名称,go mod init
指定的名称require
: 声明依赖以及其版本号,go mod tidy
执行后会自动生成replace
: 替换require
中声明的依赖,使用另外的依赖及其版本号exclude
: 禁用指定的依赖;
require
用于指定依赖,如require github.com/sakishum/mckee v1.1.1
该指令相当于告诉go build
使用github.com/sakishum/mckee
的v1.1.1
版本进行编译。
最后再执行go mod tidy
命令,如无意外就替换好了。
golang使用github上fork出来的项目
与AppStore 审核的爱恨情仇
以上。
]]>支持简体、繁体和日文
Tiny Clicker
是一款小巧实用的键鼠辅工具软件。
它能根据指定的时间间隔,自动为鼠标按下和释放左键/中键/右键,实现自动连续点击的效果(最高每秒约120+次点击)。
它还能录制键盘鼠标的所有操作并进行回放,并自动保存成脚本,并可以设置回放次数,模拟操作帮助用户快速完成一些固定操作。
现代化的应用程序,为用户提供自动鼠标点击和录放键盘鼠标事件的功能,方便用户简化操作流程,提高工作效率。
无需编写复杂的脚本,只需几步操作即可实现自动化点击和录制键盘鼠标事件,让工作变得轻松高效。
对于需要进行重复性操作的用户来说,这款工具软件将是节省时间和精力的利器,让他们可以更专注于核心工作任务。
让自动化成为您的工作新伙伴!
使用中有任何问题或建议,欢迎邮件联系:sakishum1118@gmail.com
本App
不进行任何隐私信息收集或上传。
OS Version: mac 14.2.1 (23C71)
Wails Version: v2.7.1
M1 MAC
环境下打包的 wails app
在另一台M1 MAC
下打不开,报错 “APP”已损坏,无法打开。 你应该将它移到废纸篓。
在终端执行下面命令就可以了。
1 | sudo xattr -d com.apple.quarantine /Applications/APP_NAME.app |
以上。
]]>OS: mac OS 14.2.1 (23C71)
Golang: go version go1.21.0 darwin/arm64
Protoc: libprotoc 3.20.3
1 | mkdir -p go-grpc/helloworld |
创建 helloworld/helloworld.proto
文件并编辑:
1 | // gRPC: helloworld demo |
使用以下命令将helloworld/helloworld.proto
转换成go
语言源代码文件:
1 | protoc -I . --go_out=. --go-grpc_out=require_unimplemented_servers=false:. helloworld/helloworld.proto |
执行完成后会生成一个:helloworld.pb.go
、helloworld.proto
文件, 目录结构如下:
1 | helloworld |
在工程目录下创建并编辑 server/main.go
文件,内容为:
1 | /* |
在工程目录下创建并编辑client/main.go
文件,内容为:
1 | /* |
在工程目录下执行以下命令运行服务端:
1 | > go mod tidy |
开启一个新的终端并启动客户端,如果没有报错可以看到有调用成功的结果输出:
1 | > go run client/main.go |
gRPC
协议旨在支持随时间变化的服务。 通常,gRPC
服务和方法会不中断地新增内容。
给 helloworld/helloworld.proto
再添加一个方法:SayHelloAgain
, request
和 response
类型同 SayHello
函数。
1 | ... |
重新将变更后的helloworld/helloworld.proto
转换成go
语言源代码文件:
1 | protoc -I . --go_out=. --go-grpc_out=require_unimplemented_servers=false:. helloworld/helloworld.proto |
编辑 server/main.go
文件,添加以下代码:
1 | func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { |
编辑 client/main.go
文件,添加以下代码:
1 | r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: name}) |
执行以下命令重跑服务端:
1 | > go run server/main.go |
启动客户端,可以看到新方法调用成功的结果输出:
1 | > go run client/main.go |
以上。
]]>需要在项目中自动生成和展示QR
码,在github
上翻到了名为qrcode.vue
的开源项目,这是一款 Vue.js
二维码组件,同时支持 Vue 2
和 Vue 3
。
快速添加 qrcode.vue
组件到项目中:
1 | npm install --save qrcode.vue |
在单个 *.vue
文件中当组件使用:
1 | <script setup> |
以上。
]]>1 | package main |
以上。
]]>今天,新建个了vue 3
个项目打算撸起袖子大干一场, 在安装下载依赖naive ui
包时居然报错了,出师未捷身先死啊!
1 | > npm i -D naive-ui |
照着错误码(node:11149)
, 面向搜索引擎CV
了半天, 情况依旧。。。
心中暗念九字真言, 起手敲出 npm doctor
命令看看npm
环境抽了什么风:
1 | > npm doctor |
等了一刻多钟, 才显示npm ping
不通,反手使出npm config get registry
看看源指向哪里:
1 | > npm config get registry |
灵光一闪,该不会是代理有问题吧, 先把代理设置给关闭了:
1 | > npm config set proxy false |
结果,没有效果。。。。
死马当作活马医, 不是ping
不通官方源么, 我换成淘宝源试试呢,毕竟是大公司,兴许能成。
说时迟那时快, 迅雷不及掩耳盗铃之势,替换了源:
1 | npm config set registry https://registry.npmmirror.com/ |
招式未老,再来一招:
1 | > npm doctor |
成了!
以上。
]]>google
搜索的时候排除特定的网站, 可以在搜索关键词后面加上-site:
,这样做可以排除特定网站的干扰,找到更准确的信息。sakishum.com
时, 查看有没其他网站有引用时, 就可以使用以下搜索关键词:1 | sakishum.com -site:sakishum.com |
搜索出来的结果中就不会有sakishum.com
的内容了。
以上。
]]>使用scss
写样式,拖动页面会不停的晃动。
1 | body { |
这是我解决vue3
+ naive-ui
项目中的页面抖动的方式,希望可以帮到大家!
以上。
]]>1 | npm i unplugin-auto-import -D |
修改vite.config.js
的配置文件(在 AutoImport({ imports: [ ] }
)中添加需要的插件)
1 | import { defineConfig } from 'vite' |
以上。
]]>在第一周,将通过做一些关于数组和字符串的简单和中等问题来热身。数组和字符串是最常见的问题类型;熟悉它们将有助于奠定坚实的基础,以更好地处理更棘手的问题。
Question | Difficulty | LeetCode |
---|---|---|
☑ Two Sum | Easy | Link |
☑ Contains Duplicate | Easy | Link |
☑ Best Time to Buy and Sell Stock | Easy | Link |
☑ Valid Anagram | Easy | Link |
☑ Valid Parentheses | Easy | Link |
☑ Maximum Subarray | Easy | Link |
☐ Product of Array Except Self | Medium | Link |
☐ 3Sum | Medium | Link |
☐ Merge Intervals | Medium | Link |
☐ Group Anagrams | Medium | Link |
Question | Difficulty | LeetCode |
---|---|---|
☐ Maximum Product Subarray | Medium | Link |
☐ Search in Rotated Sorted Array | Medium | Link |
以上。
]]>ES
和Kibana
从7.10.1
升级到8.x
, 记录一下遇到的问题。启动服务,ES
正常,但在浏览器访问http://127.0.0.1:5601
提示未准备就绪
,日志提示:
1 | [FATAL][root] Error: [config validation of [elasticsearch].username]: value of "elastic" is forbidden. This is a superuser account that cannot write to system indices that Kibana needs to function. Use a service account token instead. Learn more: https://www.elastic.co/guide/en/elasticsearch/reference/8.0/service-accounts.html |
翻译:elastic
这是一个超级用户帐户,不能写入Kibana
需要运行的系统索引,使用服务账户令牌代替。
修改docker-compose.yaml
(容器编排脚本), 不要直接使用- ELASTICSEARCH_PASSWORD=xxxx
,改用-ELASTICSEARCH_SERVICEACCOUNTTOKEN=MY_TOKEN
:
1 | ... |
使用命令docker-compose up -d
拉起ES
、Kibana
后, 生成token
:
1 | curl -X POST -u elastic:password "localhost:29200/_security/service/elastic/kibana/credential/token/token1?pretty" |
回填token
替换docker-compose.yaml
中的MY_TOKEN
,再重新执行命令docker-compose up -d
更新容器, 问题解决。
以上。
]]>OS
: macOS
14.1.1 (23B81)Docker
: 24.0.5Image
: mysql
(8.0.21)
Linux
服务器下查看网络连接的状态, 通过 netstat
命令查看了当前tcp
链接的情况(本地测试,线上实际值大的多)
1 | # netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' |
常用的三个状态是:ESTABLISHED
表示正在通信,TIME_WAIT
表示主动关闭,CLOSE_WAIT
表示被动关闭。
socket
连接未被使用socket
连接的关闭socket
连接关闭,正在关闭连接socket
连接,然后关闭远程socket
连接,最后等待确认信息(特殊的状态,也比较少见,正常情况下不会出现)socket
连接关闭后,等待来自远程计算器的关闭信号可以使用 netstat -an
命令查看链接状态,进行了检查。
例如,查看所有 CLOSE_WAIT
的链:
1 | # netstat -an | grep TIME_WAIT |
Linux
系统有很多块网卡,可以用 ifconfig -a
(显示或设置网络设备) 打出来,自己写的服务器一定要监听 lo
(本地环路接口)这块网卡,不然监听上网的那块网卡是监听不到的。
eth0
,eth1
,eth2
…代表网卡一,网卡二,网卡三…lo
代表127.0.0.1
,即localhost
查找本地 lo
网卡:
1 | # ifconfig -a | grep lo |
另外也可以使用nmcli dev status
命令查看网卡状态:
1 | # nmcli dev status |
tcpdump
是对网络上的数据包进行截获的包分析工具,它支持针对网络层、协议、主机、网络或端口的过滤,并提供 and
、or
、not
等逻辑语句来去掉无用的信息。
例如,监听本地Docker
运行的MySQL
服务(端口 13306
)的数据包:
1 | sudo tcpdump -i lo0 host 127.0.0.1 and port 13306 |
参数说明:
X: 包的内容也可以查看的到
i: 指定要监听的网卡
-n: 指定将每个监听到数据包中的域名转换成IP
地址后显示,不把网络地址转换成名字
-nn: 指定将每个监听到的数据包中的域名转换成IP
、端口从应用名称转换成端口号后显示
-v: 输出一个稍微详细的信息,例如在IP
包中可以包括TTL
和服务类型的信息
-S: 打印绝对时间戳
-w mysql.pcap: 将抓到的包写入文件,生成的 pcap 文件可以用 tcpdump 或者 wireshark 之类的网络流量分析工具打开
host: 是IP
地址
port: 是端口号
抓包结果:
1 | 21:29:40.148304 IP localhost.59994 > localhost.13306: Flags [S], seq 1224152147, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 2485129604 ecr 0,sackOK,eol], length 0 |
FLAG[S]
意思是包中的标志是SYN
,这是一个请求包。FLAG[.]
一般是指明这是一个单一的 ack
确认包。
NOTE:Flags are some combination of S (SYN), F (FIN), P (PUSH), R (RST), W (ECN CWR) or E (ECN-Echo), or a single ‘.’ (no flags)
options 表示选项
mss 表示是发送端通告的最大报文长度
sackOK 表示发送端支持 SACK 选项,SACK 选项是为了更好的确定数据的准确接收的
TS val 发送端的时间戳 ecr 接收端的时间戳
wscale 表示窗口因子大小
如果 tcpdump
无法抓到数据包,可以考虑以下几种原因:
libpcap
库: tcpdump
需要使用libpcap
库来抓取数据包,如果没有安装,请先安装。tcpdump
需要使用root
权限才能抓取数据包,使用sudo
命令运行tcpdump
。tcpdump
可能无法抓取数据包。请检查网络配置是否正确。tcpdump
也无法抓取数据包。如果以上原因都已经排除,请尝试使用其他工具来检测网络是否正常。
1 | 21:29:40.148304 IP localhost.59994 > localhost.13306: Flags [S], seq 1224152147, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 2485129604 ecr 0,sackOK,eol], length 0 |
connect()
发出 SYN
请求建立 TCP
连接,此时客户端的 TCP
端口状态为 SYN_SENT
(表示请求连接)。1 | 21:29:40.148352 IP localhost.13306 > localhost.59994: Flags [S.], seq 2879060989, ack 1224152148, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 1613321693 ecr 2485129604,sackOK,eol], length 0 |
listen()
监听到 SYN
请求,TCP
端口状态从 LISTEN
转为 SYN_RCVD
并第一次响应 ACK
。1 | 21:29:40.148358 IP localhost.59994 > localhost.13306: Flags [.], ack 1, win 6379, options [nop,nop,TS val 2485129604 ecr 1613321693], length 0 |
ACK
响应之后,TCP
端口状态变成 ESTABLISHED
(已建立连接,表示通信双方正在通信),再给服务端发送 ACK
。服务端接收到 ACK
之后,服务端的 TCP
端口状态为 ESTABLISHED
。为什么要握手要三次?
防止失效的连接请求突然传到服务器端,让服务器端误认为要建立连接。
双方建立通信之后,Clien
向 Server
正式发出请求:
1 | 22:56:02.009399 IP localhost.13306 > localhost.58189: Flags [P.], seq 1:79, ack 1, win 6379, options [nop,nop,TS val 1886650872 ecr 1513261871], length 78 |
Client
=> Server
:seq
= x+1, ack
= y+1 继承了第三次连接的 seq
和 ack number
请求长度 length
: 78,seq
1:79 == seq
1:[1+length
]
TCP
协议规定,对于已经建立的连接,收发双方要进行四次挥手才能成功断开连接,如果缺少了其中某个步骤,都会使连接处于假死状态,连接本身所占用的资源不会被释放。
step 1:
1 | 21:32:25.591866 IP localhost.59994 > localhost.13306: Flags [F.], seq 27, ack 1, win 6334, options [nop,nop,TS val 2485295047 ecr 1613321738], length 0 |
由 Client
提出断开连接请求 FIN
(Flags [F.]
),Client
的 TCP
端口进入 FIN_WAIT_1
状态。
step 2,3:
1 | 21:32:25.591885 IP localhost.13306 > localhost.59994: Flags [.], ack 28, win 6368, options [nop,nop,TS val 1613487136 ecr 2485295047], length 0 |
Server
发送 ACK
应答,Server
的 TCP
端口进入 CLOSE_WAIT
状态,Client
的 TCP
端口进入 FIN_WAIT_2
状态。几乎同时 Server
还发送了一个 FIN
端口连接请求,进入 LAST_ACK
状态。
step 4:
1 | 21:32:25.592247 IP localhost.59994 > localhost.13306: Flags [.], ack 2, win 6334, options [nop,nop,TS val 2485295047 ecr 1613487136], length 0 |
Client
进行 ACK
应答(LAST ACK
),进入 TIME_WAIT
(两倍的分段最大生存期),并最终变成 CLOSED
状态。
为什么进入
TIME_WAIT
状态后必须等待2MSL
后才能返回CLOSED
状态?
- 保证发送的最后一个
ACK
报文段能达到- 防止失效的报文段出现在连接中
以上。
]]>OS
: macOS 14.1.1 (23B81)Docker
: 24.0.5Image
: mysql:8.0.21
定位慢查询问题,可以通过如下几个步骤进行:
1、查看慢查询日志是否开启
因为开启慢查询日志是有代价的(跟bin log
、optimizer-trace
一样),所以它默认是关闭的。
开启慢日志,执行命令:
1 | show variables like 'slow%'; |
可以看到slow_query_log
属性是OFF
,处于关闭状态,那么我们需要先开启慢查询。slow_query_log_file
表示慢查询日志文件的存放路径,我们可以自定义文件路径(mysql
安装、运行在docker
):
1 | set global slow_query_log_file = '路径' |
执行命令:
1 | set global slow_query_log = on |
然后再查询一下,发现slow_query_log
处于开启状态:
1 | show variables like 'slow%'; |
开启了之后,是不是所有的查询都会记录在文件里呢?
当然不是,慢查询日志,顾名思义是只记录查询比较慢的语句,那问题又来了,怎么才算查询比较慢的语句呢?
实际上,这里会有一个标准值,而且这个标准值是可以由我们自己设定的。
首先查看一下默认的临界值。
执行命令:
1 | show variables like '%long%'; |
其中有一个long_query_time
属性,它的值为10.000000
。它表示的意思是,只记录查询时间在10s
以上的语句。
显然10s
我们是不可接受的,所以需要自己设定一下这个值。因为我的测试表中没有多少条数据XD
,查询很快,所以这里把慢查询的临界值设置为1.00
:
1 | set long_query_time=1.00; |
可以看到现在临界值是1.00
秒了。
1 | +----------------------------------------------------------+----------+ |
现在来模拟手动触发一个大于 1.00s
的慢查询(测试表中没有多少条数据,就直接用sleep
测试了),看是否都记录在了慢查询日志中。
1 | SELECT sleep(3); |
查看慢查询日志当中有多少条记录:
1 | show global status like '%Slow_queries%'; |
看下慢查询日志(mysql
安装、运行在docker
):
1 | docker exec -it d1673994303e cat /var/lib/mysql/d1673994303e-slow.log |
可以看到刚才模拟查询的日志,从该日志中能看到如下几个信息(根据不同的MySQL
版本或者配置,这些信息可能有增减):
产生时间(Time
):2021-03-12T08:52:54.227174Z
来源(User@Host
): df-test[df-test] @ [192.168.65.1]
,即用户 df-test
在192.168.65.1
这个机器上执行了这个查询
查询统计(Query_time
):如消耗的时间,发送/接收的行数
具体的SQL
语句
需要注意的是,上面的操作是在命令行中进行的,如果数据库进行重启,这些设置都会失效。
一般不建议长期开启慢查询日志,如果要永久生效,需要修改配置文件:
1 | vim /etc/mysql/my.cnf |
在配置文件中加上这三行就可以了,但需要重启MySQL
才能生效!
有了慢查询日志,怎么去分析统计呢?比如SQL
语句的出现的慢查询次数最多,平均每次执行了多久?人工肉眼分析显然不可能。
可以通过MySQL
官方自带的mysqldumpslow
工具进行分析,执行命令(mysql
安装、运行在docker
):
1 | docker exec -it d1673994303e mysqldumpslow --help |
例如:查询用时最多的10
条慢SQL
语句:
1 | docker exec -it d1673994303e mysqldumpslow -s t -t 10 -g 'select' /var/lib/mysql/d1673994303e-slow.log |
Count
: 代表这个 SQL 执行了多少次;Time
: 代表执行的时间,括号里面是累计时间;Lock
: 表示锁定的时间,括号是累计;Rows
: 表示返回的记录数,括号是累计。
官方文档地址。
通过慢查询日志,针对性的去找到这些慢查询日志去进行explain
分析 然后去调优创建索引。
执行命令:
1 | explain (SQL语句)。 |
把上面执行的两条语句放一起对比解析一下:
1 | explain SELECT sleep(3); |
需要重点关注possible_keys
、key
、rows
这几个属性值。possible_keys
表示该语句可能会用到的索引key
表示该语句实际用到的索引rows
表示该语句扫描的行数
通过这些属性,可以大致的分析定位到慢查询的原因,然后针对性的去解决。
如果通过explain
语句解析没有定位到问题,该加的索引也加了,但是还是比较慢,那就可以通过性能详情来进一步的探究一下原因。
性能详情可以追踪查询语句的整个生命周期的状态,有了这些状态值,就可以从更深层次找出具体是哪个环节慢了,从而能针对性的进行改善。
执行命令:
1 | show variables like '%profiling%'; |
可以看到profiling
属性值为OFF
,表示关闭。
开启性能详情,执行命令:
1 | set profiling = on; |
当我们开启了之后,在数据库上执行的任何的SQL
语句都会被记录来。
我们执行一条SQL
语句:
1 | SELECT sleep(3); |
然后查看性能记录:
1 | show profiles; |
可以看到开启后的所有查询语句的记录。我们想查看一下第二条执行语句的整个执行周期的状态详情(SQL
慢要么是CPU
计算复杂,要么是IO
频繁开销所以只看CPU
和block io
即可):
执行命令:
1 | show profile cpu,block io for query 2; |
可以看到整条SQL
语句执行过程每个状态的耗时情况。然后定位到具体是哪个状态最耗时,然后针对性的排查原因。
官方也给出了每个状态的解释,具体可查看官方文档地址。
以上是定位MySQL
慢日志查询问题的步骤总结。在实际应用过程中,要多尝试不同维度的解决方案,并结合自身所处行业、业务等特点,挑选适合自己和团队使用的数据库分析工具,保障系统和业务的稳定。
以上。
]]>在基于vue3+vite+typescript
的UI
组件库时设置了别名会有typescript
提示
找不到模块“XXX”或其相应的类型声明ts(2307)
检查vite.config.ts
里面有没有配置alias
别名路径,没有就添加上。
1 | export default defineConfig({ |
以上。
]]>slice
当做参数传递到函数,给这个slice
做一些修改的情况。想到slice
是引用传递,可以直接传递slice
用作修改,于是可能出现下面这种情况:1 | package main |
我们利用modifySlice
函数对传入的arr
进行修改和添加的操作,想到slice
是引用传递,在modifySlice
中的修改必然会生效。
但是实际上呢,我们看一下运行情况:
1 | # go run main |
运行结果表明我们对s[0]
做的修改没有生效,添加新元素的操作也未生效,这是为什么呢?
原因就在于添加新元素是用的append
,并将原先的引用重新赋值了!
我们把引用当成指针来看,指针指向的内容就是slice
的数据内容。
指针本身是一种变量类型,指针类型在传递时,是值传递!
也就是说我们在外面调用modifySlice
时,传入arr
是一个指针变量,当进入到modifySlice
函数时,形参arr
跟外面传入的arr
并不是一个值,是arr
的值拷贝!但是他们的值是一样的,即指向的数据内容都是slice
中的数据。
然而我们用这种方式:arr = append(arr, 2)
,这是在修改指针,并不是在修改指针指向的数据!
append
可能会因为arr
的cap
不足,重新分配空间(扩容过的数组),所以append
有一个返回参数。
如果append超过设定的区间,那么Slice底层就会扩容了
我们对modifySlice
中的arr
进行了修改,但是arr
是指针,仅仅是外层传入的slice
指针的拷贝,说明我们并没有对外层的slice
重新赋值!
这就是添加新元素失败的原因。
我们再执行arr[0] = 0
,实际上是修改的是指针指向的数据的内容(扩容过的数组)!这里肯定不会生效!
如果把函数modifySlice
中的语句调换下顺序,修改为:
1 | func modifySlice(arr []int) { |
则运行情况则会改变:
1 | # go run main |
这里用arr[0] = 0
做了修改,实际上是修改指针指向的数据的内容(未扩容的数组)!这里就会修改到外部传入的arr
。
以上。
]]>
github
: wailsapp/wails
C/S模式,一个后端服务,一个前端页面作为UI。前端可以使用 Vue / React / Angular,可以说很适合偏前端的选手。
Go
语言的桌面解决方案来了——Wails
,一个能让你使用 Go
语言创建漂亮桌面应用的项目。从其官网的描述来看,它提供的解决方案如 Tauri
类似,只是后端语言换作了 Go
语言,前端开发依旧采用 HTML/CSS/Javascrip
t,采用 Web
渲染引擎进行界面绘制。
桌面级别的应用开发依然是各个语言争相角逐的领域。 C#
有微软推出的 MAUI
,以及 WinForm
和 WPF
,依旧占据桌面应用开发的绝对主导地位。Node.js
将Javascript
语言带入火热的后端应用开发领域,然而 Electron
项目却让 Javascript
语言在桌面应用开发中也占有了一席之地,依靠强大的前后端 Web
生态,在桌面开发中也玩得风生水起。
多端跨平台解决方案也是层次不穷,最值得关注的就是 Google
所推出的 Flutter
项目,目前可以说是真正的多端(移动端/ Web
端/桌面端),多平台( Windows/Linux/Android/iOS/macOS
)的解决方案,其对桌面应用开发也已经正式发布。
目前势头发展很猛的 Rust
语言最近也推出了桌面应用开发框架 Tauri 1.0
版本,也标志着 Rust
语言的桌面级应用开发也准备就绪了。
安装(Installing Wails
):
1 | go install github.com/wailsapp/wails/v2/cmd/wails@latest |
系统检查(System Check
):
检查是否安装了正确的依赖项
1 | wails doctor |
安装 *upx
1 | brew install --build-from-source upx |
安装 *nsis
makensis System to create Windows installers.
1 | brew install makensis |
自动更新:
1 | # Wails update |
现在 CLI
已安装,您可以使用该 wails init
命令生成一个新项目。
为了快速启动和运行,您可以通过运行 wails init -n myproject
生成一个默认项目。这将创建一个名为 myproject
的目录,并使用默认模板填充它。
1 | wails init -d . -n 项目名称 -t vue # 其中-d .代表当前文件夹下创建,不加的话会创建一个-n名称的文件夹 |
其他可用的项目模板可以使用 wails init -l
列出。这里也有提供不同功能和框架的社区模板。
1 | #wails init -l |
Wails
项目具有以下布局:
1 | . |
项目结构概要
frontend
目录没有特定于 Wails
的内容,可以是您选择的任何前端项目。build
目录在构建过程中使用。这些文件可以修改以自定义您的构建。如果文件从构建目录中删除,将重新生成默认版本。go.mod
中的默认模块名称是 changeme
。您应该将其更改为更合适的内容。
在当前目录下用命令行开启开发者热加载工具查看效果:
1 | wails dev |
从项目目录,运行 wails build
。 这将编译您的项目并将构建的可用于生产的二进制文件保存在 build/bin
目录中。
1 | wails build |
接下来看看事件是如何交互的。
有中文文档,却完全没有一个示例……
https://learnku.com/go/t/36913
以上。
]]>使用Homebrew
安装OpenJDK
,方便管理(查看、更新、卸载)
在终端中执行安装脚本:
1 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" |
1 | brew install openjdk@20 |
根据输出的安装信息的提示,为了让 Java wrappers
找到 JDK
,需要手动建立链接:
1 | # For the system Java wrappers to find this JDK, symlink it with |
配置 $JAVA_HOME
环境变量,找到 .bash_profile/.bashrc/.zshrc
等配置文件中的任意一个,添加下面代码:
1 | echo 'export PATH="/opt/homebrew/opt/openjdk/bin:$PATH"' >> ~/.zshrc |
然后退出终端重新打开,或者重新加载配置文件:
1 | # 以 .zshrc 为例 |
动态查找 Java Home
,使用 /usr/libexec/java_home
命令行工具(支持动态查找 Java Home
,默认为最新版本 JDK
的 Java Home
):
1 | > /usr/libexec/java_home -V |
Mac OS X 10.5 以上才支持 /usr/libexec/java_home 命令行工具
配置$JAVA_HOME
环境变量:
1 | echo 'export JAVA_HOME=$(/usr/libexec/java_home)' >> ~/.zshrc |
然后退出终端重新打开,或者重新加载配置文件:
1 | # 以 .zshrc 为例 |
在终端中使用 java
命令行工具,来检查配置是否生效,输出版本信息,表示配置成功:
1 | > java -version |
然后新建一个test.java
文件,打开编辑test.java
。
1 | class test { |
在终端中输入javac test.java
编译test.java
文件。然后输入java test
运行编译文件,就会输出hello world
。
1 | > javac test.java |
到此为止,适用于M1
的java
环境安装成功。
以上。
]]>如果业务代码没执行完,锁却过期了,这时候其他线程又能抢锁了,线程就不安全啦。所以 redisson
内部有个 watchdog
(看门狗)的自动延期的机制,意思是定时监测业务是否执行结束,没结束的话你这个锁是不是快到期了(超过锁的三分之一时间,比如设置的 30s
过期,现在还剩 10s
到期),那就自动续期直到任务结束释放锁,大概是这么个意思。这样做可以防止如果业务代码没执行完,锁却过期了所带来的线程不安全问题。
实际开发中:
1 | // 具有 Watch Dog 自动延期机制 默认续 30s 每隔 30/3=10 秒续到 30s |
10s
,超过就释放。watchdog
(看门狗)自动延期这个策略。但是这里是不是就矛盾了,如果业务上有 bug
,进到死循环或宕机了,那锁还在不断自动延期,锁不释放那不就造成死锁了?所以,所以业务上最好是用 tryLock
而不是 lock
。
用到 redisson
最多的场景一定是分布式锁,一个基础的分布式锁具有三个特性:
互斥
:在分布式高并发的条件下,需要保证,同一时刻只能有一个线程获得锁,这是最最基本的一点。防止死锁
:在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。可重入
(非必要、但重要):我们知道ReentrantLock
是可重入锁,那它的特点就是同一个线程可以重复拿到同一个资源的锁。实现的方案有很多,这里,就以我们平时在网上常看到的 redis
分布式锁方案为例,来对比看看 redisson
提供的分布式锁有什么高级的地方。
我们在网上看到的 redis
分布式锁的工具方法,大都满足互斥、防止死锁的特性,有些工具方法会满足可重入特性。
如果只满足上述 3
种特性会有哪些隐患呢? redis
分布式锁无法自动续期,比如,一个锁设置了1
分钟超时释放,如果拿到这个锁的线程在一分钟内没有执行完毕,那么这个锁就会被其他线程拿到,可能会导致严重的线上问题,我已经在秒杀系统故障排查文章中,看到好多因为这个缺陷导致的超卖了。
redisson
锁的加锁机制如上图所示,线程去获取锁,获取成功则执行 lua
脚本,保存数据到 redis
数据库。
如果获取失败: 一直通过 while
循环尝试获取锁(可自定义等待时间,超时后返回失败),获取成功后,执行 lua
脚本,保存数据到 redis
数据库。
redisson
提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么 redisson
会自动给 redis
中的目标 key
延长超时时间,这在 redisson
中称之为 watchdog
机制。
同时 redisson
还有公平锁、读写锁的实现。
如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。这样也存在一个问题,加入一个线程拿到了锁设置了 30s
超时,在 30s
后这个线程还没有执行完毕,锁超时释放了,就会导致问题, redisson
给出了自己的答案,就是 watchdog
自动延期机制。
redisson
提供了一个监控锁的看门狗,它的作用是在 redisson
实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。
默认情况下,看门狗的续期时间是 30s
,也可以通过修改 Config.lockWatchdogTimeout
来另行指定。
另外 redisson
还提供了可以指定 leaseTime
参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。
上述源码读过来我们可以记住几个关键情报:
watchdog
在当前节点存活时每 10s
给分布式锁的key续期 30s
;watchdog
机制启动,且代码中没有释放锁操作时, watchdog
会不断的给锁续期;2
得出,如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {}
中;如果释放锁操作本身异常了, watchdog
还会不停的续期吗?不会,因为无论释放锁操作是否成功, EXPIRATION_RENEWAL_MAP
中的目标 ExpirationEntry
对象已经被移除了, watchdog
通过判断后就不会继续给锁续期了。
以上。
]]>1 | git clone https://github.com/temporalio/docker-compose.git |
创建项目目录:
1 | mkdir hello-temporal |
初始化项目:
1 | go mod init hello-temporal |
下载最新版本的 Go SDK
(或者在最后执行go mod tidy
):
1 | go get -u go.temporal.io/sdk@latest |
目录结构:
1 | . |
activity/say_hello.go:
1 | package activity |
workflow/handle_name.go:
1 | package workflow |
worker/main.go:
1 | package main |
main.go:
1 | package main |
启动 Worker
:
1 | go run worker/main.go |
在另外一个窗口,启动 Workflow
:
1 | go run. main.go |
以上。
]]>