v2ray

背景

以前一直使用 ssr 搭建,后面被封了几次。就算是在怎么模拟,比如指向自己的博客等等方式来规避,还是不可避免的被彻底封掉了。后面开始计划走 cdn 的方式,这一块 ssr 支持的不好,所以投入了 v2ray 的怀抱,来尝试一下。

步骤

  1. 使用官方的脚本命令安装。
    1
    bash <(curl -L https://raw.githubusercontent.com/v2fly/fhs-install-v2ray/master/install-release.sh)
  2. config.json 示例。主要关注点在于 inbounds 。routing 一般在 v2ray 客户端做了处理,到服务器一般都是要翻墙的。不然何苦还要绕。routing 其实还可以在扩展,比如 “geoip:cn” 等等。但因为我的多台服务器都在一个国家。这个意义就不大了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    {
    "log": {
    "loglevel": "info",
    "access": "/var/log/v2ray/access.log",
    "error": "/var/log/v2ray/error.log"
    },
    "inbounds": [{
    "port": XXXXX,
    "listen": "127.0.0.1",
    "protocol": "vmess",
    "settings": {
    "clients": [
    {
    "id": "XXXXXXXXXX",
    "level": 1,
    "security": "auto",
    "alterId": 10
    }
    ],
    "disableInsecureEncryption": false
    },
    "streamSettings": {
    "network": "ws",
    "wsSettings": {
    "path": "/XXXXXXXX",
    "headers": {
    "Host": "XXXXX"
    }
    }
    }
    }],
    "outbounds": [{
    "protocol": "freedom",
    "settings": {}
    },{
    "protocol": "blackhole",
    "settings": {},
    "tag": "blocked"
    }],
    "routing": {
    "rules": [
    {
    "type": "field",
    "ip": ["geoip:private"],
    "outboundTag": "blocked"
    }
    ]
    }
    }
  3. 启动。systemctl start v2ray
  4. nginx 中转。将你先前配置的路径中转到对应的配置端口。这块其实 v2ray 和 https 没关系,由 nginx 来进行处理。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    location /XXXXXXX {
    proxy_redirect off;
    proxy_pass http://127.0.0.1:XXXX;
    #proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    # Show real IP in v2ray access.log
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

连接

  1. android
    很明显,由很多开源的 apk 可以下载安装。比如 github 上的 v2rayNG 等等。
  2. windows\mac 也类似。有很多开源的可以安装,比如 github 上 windows 有 V2RayW,mac 有 V2rayU。
  3. ios 就不一样了。因为苹果政策的关系,除非你有兴趣自己进行编译,但尤其吐糟的一点就是 network extension 限制还很严格。apple store 上的 Shadowrocket 很好用,价格也不贵,使用很方便。要是不嫌弃的话,其实免费的 Shadowlink 也能用,就是广告有点多,功能也比较少。这些应该都要国外 appid 才能下载到(现在不清楚,以前能连外网注册起来还是比较简单的,就是购买东西现在好像不支持 visa 了,只能弯道通过礼品卡充值购买)。

结论

使用了一段时间,感觉还是比较稳定的。也拯救了我的被封的 ip。可怜我买的 CN2 GIA 服务器,本来 1080p 无压力的,一走 cdn 直接就只能看 144p 的视频,还老断,cloud flare 免费的真是不稳定啊。

背景

最近发现在空指针上有一些问题,在开发过程中 nil 的判断有时候没有起作用。导致了 panic。

研究

go 里面对象从反射看分为 type 和 value 即动态类型和值。
在 go 中 nil 也是有类型的,以下是输出 nil 的 type 和 value

1
<nil> <invalid reflect.Value>

go 在一般定义对象时候会赋予初始值,如果我们没有设置初始化的值。基础类型不说,指针会是 nil,接口也是 nil,struct 就是创建了一个空的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type TestPointer interface {
Print() error
}

type TestPoint struct {
}

func (t *TestPoint) Print() error {
fmt.Println("", reflect.TypeOf(t), reflect.ValueOf(t))
return nil
}

var tPoint *TestPoint
var tInterface TestPointer
var tPointS TestPoint

关键来了,指针和接口的 nil 实际上不是一回事。指针赋予的初始值 nil,type 是指针类型,value 是不存在。接口的初始值就是 nil,type 是 nil,value 是不存在。

1
2
assert.True(t, tPoint == nil)
assert.True(t, tInterface == nil)

这断言运行起来没问题,是不是看上去很正常。

1
2
3
4
5
6
tfunc := func(t TestPointer) bool {
fmt.Println("", reflect.TypeOf(t), reflect.ValueOf(t))
return t == nil
}
assert.False(t, tfunc(tPoint))
assert.True(t, tfunc(tInterface))

这断言 ok,细心看就会发现 tfunc(tPoint) 返回值是 false 了。
这里面有2个问题。

  1. 为什么 tfunc(tPoint) 是 false。
  2. 为什么同样都是 nil,传递到方法里面判断就会产生不一样的后果。
    这是不是意味着 nil 判断其实是不准确的。通过打印 reflect.TypeOf(t), reflect.ValueOf(t) ,这2个值是不一样的(但结果在方法内还是方法外是一样的)。
    1
    2
    *user.TestPoint <nil>
    <nil> <invalid reflect.Value>
    这种情况下不影响对 nil 的判断。但是将 tPoint 作为参数类型是 TestPointer 传入则判断出错,我们用 print() 打印一下。
    1
    2
    0x0
    (0x0,0x0)
    ,在方法内打印结果是
    1
    2
    (0x1225ba0,0x0)
    (0x0,0x0)
    这其实表明了一点是,在 go 中,将具体类型作为接口类型传入时,会产生指针,指针会指向具体的类型。从这里大概解释了问题。

底层

interface 使用 iface 和 eface 数据结构来表示。这个从上面可知,在接口参数 传入 struct 时,是(0x1225ba0,0x0),说明 tab 不是 nil,data 是 nil。而 == nil 判断需要 tab 和 data 都是 nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type iface struct {
tab *itab
data unsafe.Pointer
}

type itab struct {
inter *interfacetype // 8 字节
_type *_type // 8字节
hash uint32 // 4字节,与 _type 里 hash 相同
_ [4]byte // 4字节
fun [1]uintptr // 8字节,函数多了往后偏移,variable sized. fun[0]==0 means _type does not implement inter.
}

type eface struct {
_type *_type
data unsafe.Pointer
}

结论

  1. 如果方法传参是定义好的 interface,那么在方法内部判 nil 的时候需要特别注意,如果外界传入的是实现了 interface 的 struct, 判 nil 会出现问题,可以将其转为特定类型或者用反射来判断。
  2. 如何避免出现这个问题。在别的语言比如 java 等面向对象的,没有这个问题。go 中,因为采用了 struct 自动转 interface ,这导致和 nil 的定义相冲突,产生了此类问题。nil 的定义 go 很清晰 var nil Type nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type。interface 的解析也很清楚。但是2者结合就容易出现理解出问题,想消除 bug 只能不断加深理解,不断挖坑填坑。

引言

activity 生命周期大家都比较清楚,onAttach->onCreate->onStart>onResume->onStop->onDestroy。 但是 activity 的 view 绘制到底发生在什么时候,它的创建、绘制、前台可交互等等的时期。

详解

我们在 activity onCreate 里面会设置 setContentView。实际上是创建了一个 view,它的目标是创建 DecorView ,进行初始化和布局的加载。

1
2
3
4
5
6
mWindow = new PhoneWindow(this, window, activityConfigCallback);

public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

但在源码里面 ActivityThread 里可以看到是在 onResume 之后,把 DecorView 添加到 WindowManager,从这时候开始该 activity 开始绘制到屏幕上。由 WindowManager 移交给 ViewRootImpl 渲染绘制。绘制完成后即开始交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
if (r == null) {
// We didn't actually resume the activity, so skipping any follow-up actions.
return;
}
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}

// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow = true;
}
...
}

总结

现在我们对 activity view 展示、初始化、绘制时机等等有了一个基础的了解。这有什么用呢?
比如如果我们想知道 activity 的绘制完成时间,那么很明显,除了重写 onWindowFocusChanged 方法外,最准确的是 DecorView 的绘制完成,可以监听其 onDraw 完成后的一次 vsync。

引言

探究各个语言发展历史和特性

java

特性

  1. 跨平台
  2. 面向对象,多态、继承、封装
  3. 双亲委托机制
  4. 注解
  5. aop 编程
  6. 自动垃圾回收

go

一种静态强类型、编译型、并发型、并具有垃圾回收功能的编程语言

特性

  1. 协程
  2. 天然的 web server 支持
  3. 速度快。
  4. 简单,不支持多态等等,也不许内部循环 import
  5. 自动垃圾回收

引言

前几天看到一篇文章,讲了一些数据库的注意事项。里面注意到 mysql innodb 为了避免二次查找用了索引覆盖技术。

详解

索引覆盖技术即索引里覆盖了所有的查询结果,可直接返回。
这个问题起因是在 innodb 里索引叶子节点存储的是主键,一般情况下还需要用主键进行二次查找,会大幅度降低效率。
这一点可以用延迟关联查询和一般查询来进行验证。
在其它数据库类似 postgresql 和 sql server 新版本都支持 index include,是为了在索引里就包含了一些需要的数据,以此来使用索引覆盖提高效率。

运用

但在现实使用的时候,很多时候并没有走索引覆盖,可能计划的美美的,但一运行,what??还不如不加索引呢。比如 postgresql 里面索引覆盖和 VACUUM 有关,因为其要查询数据是否更改。所以计划器会权衡来决定是否使用仅索引扫描。

总结

索引覆盖是一种机会性的,这点尤其在复杂查询时候很明显(需要由查询优化器来进行权衡)。我们可以在写 sql 或者创建索引时尽量朝这方面发展,但是它明显是不可靠的,所以最好能先预期到索引对其它方面的影响。

下载新版 redis

redis 地址

1
2
3
4
5
cd /usr/local/redis/
wget https://download.redis.io/releases/redis-6.0.9.tar.gz
tar xzf redis-6.0.9.tar.gz
cd redis-6.0.9
make

创建路径

1
2
3
mkdir /usr/local/redis-cluster
cd redis-cluster/
mkdir -p 7000/data 7001/data 7002/data 7003/data 7004/data 7005/data

复制 redis 脚本

1
2
3
mkdir /usr/local/redis-cluster/bin
cd /usr/local/redis/src
cp mkreleasehdr.sh redis-benchmark redis-check-aof redis-check-dump redis-cli redis-server redis-trib.rb /usr/local/redis-cluster/bin

复制实例并修改配置

1
2
cp /usr/local/redis/redis-6.0.9/* /usr/local/redis-cluster/7000/
vim /usr/local/redis-cluster/7000/redis.conf

注意修改下列配置文件项,其余自行决定修改

1
2
3
4
5
6
7
8
9
port 7000(每个节点的端口号)
daemonize yes
bind 192.168.119.131(绑定当前机器 IP,默认本机 127.0.0.1)
dir /usr/local/redis-cluster/7000/data/(数据文件存放位置)
pidfile /var/run/redis_7000.pid(pid 7000和port要对应)
cluster-enabled yes(启动集群模式)
cluster-config-file nodes7000.conf(7000和port要对应)
cluster-node-timeout 15000
appendonly yes

最重要的 cluster 三个配置项 取消注释
重复另外5个节点。可以将 7000/ 复制到其他的目录。vim 进入后可以全局替换

1
:%s/7000/7001/g

启动集群

1
2
3
4
5
6
/usr/local/redis-cluster/bin/redis-server /usr/local/redis-cluster/7000/redis.conf
/usr/local/redis-cluster/bin/redis-server /usr/local/redis-cluster/7001/redis.conf
/usr/local/redis-cluster/bin/redis-server /usr/local/redis-cluster/7002/redis.conf
/usr/local/redis-cluster/bin/redis-server /usr/local/redis-cluster/7003/redis.conf
/usr/local/redis-cluster/bin/redis-server /usr/local/redis-cluster/7004/redis.conf
/usr/local/redis-cluster/bin/redis-server /usr/local/redis-cluster/7005/redis.conf

创建集群

1
/usr/local/redis-cluster/bin/redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1

期间会询问主从节点,是否同意?一般没特殊的,前三个主节点,后3个从节点。

调试

注意使用 -c 参数来启动集群模式

1
/usr/local/redis-cluster/bin/redis-cli -c -h 127.0.0.1 -p 7000 

动态扩容、增加节点和减少节点,重新分配槽大小等

redis集群有 16384 个slot。
新增节点很方便

1
2
3
4
5
//新增了主节点
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7001

//新增从节点
redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7001 --cluster-slave --cluster-master-id *****

新增后需要重新分配 slot

1
redis-cli --cluster reshard 127.0.0.1:7000 --cluster-from 1c08b1c921ab1e646323474c09113de627491c11 --cluster-to 8537eee4ae61a744feb1c805e5d0df18ffd5b00a --cluster-slots 1024 --cluster-yes --cluster-timeout 50000 --cluster-replace

动态扩容可以写脚本,或者一般云服务提供商都有成熟方案。
期间有几个致命问题

1
2
3
1.整体搬迁延迟,大 Key 的问题。需要设定阈值,超过阈值不搬迁,但这不是很好的方案,而且阈值不好定
2.搬迁过程中,多 key 会存在访问错误。mget/mset 会报错,slave 读取会出现空数据或者 loading 错误
3.扩容速度会占用 cpu。单个 redis qps 上限8万,每秒搬1万个,就至少占用12.5的

json 解析库选择

gson,moshi,kotlinx.serialization。
gson 是 java 官方推荐的反序列化及序列化Json框架,kotlinx.serialization 是 kotlin 官方推荐库
moshi 是 java 和 kotlin 都能使用的库。
gson 在 kotlin 里面默认值设置会有问题,一旦没有空的构造方法,就会出现 null,其次和 kotlin 各种检查也有水土不服,比较它主要针对的 java,kotlin 虽然最终也转 java,但明显不受我们控制。
Moshi 绑定等规则基本和 gson 一致,读写使用了 okio,会有一定提升。对 kotlin 支持友好,如果混编的话还是选择 Moshi 好。

程序流程

出现问题

bug、新需求、新的场景要兼容等等

解决问题

  1. 为什么会出现这种问题?
  2. 问题汇总和细化
  3. 问题归类
  4. 解决问题方案
  5. 该方案会导致其它问题吗?权衡。风险、概率、损失

总结

通常来说解决一类问题,都会导致其它问题的发生,所以需要尽量提前预期到。
好的设计一个意义在于能通过整体规划来把不可控因素控制在一定范围内。

一个程序在早期就能有意识规避 bug ,那远远好于在后期发现 bug,进行处理,越复杂系统越是如此。

设计的意义

设计在于可以为未来扩展做很好的铺垫。在现在快速试错的市场下,敏捷开发和快速迭代是基本要求。期间我们可以注意到在一开始的瑕疵,到中后期想修复需要付出上千倍不止的代价。
所以在开始阶段,我们可以有一个长远的目标,可以不清晰,不明确,主要是看的远。然后尽量朝着目标靠近,这样可以为后面减轻大量的工作。
期间一个重要的指标是区分重要程度,否则这里很容易陷入各种细节,导致工作和收益明显不成正比。之前我在开发中就经常陷入细节中,比如发现代码有几万分之一甚至几十万分之一的概率出错,如果这是随手修改倒还好,但一旦要修改大量代码就有点不值当。会大量提高成本包括复查、测试、风险等。针对这种问题,我们可以把它局限在很小范围内简化处理,如果能稍带上一个日志能检测更好。当然,在要求严格的场景发现这种问题,那要尽快汇报解决。
我们能根据目标和需求把各个细节都局限在各个方块内,做到就算一个被替换或者出错也不会影响其它,就算基本成功了。至于这种设计是否能优化,符合需求,做到更好,就是仁者见仁,智者见智了。有人持有性能第一,那么就会从性能考虑,算法等等放一块统一优化处理,以算法为核心。有人持有架构第一,那么会从扩展性考虑,根据模块未来考虑将其解耦,通过接口隔离等等方式。

比如我在公司做基本库和组件的工作,负责 app 底层的架构和设计。这块就要求能看的远,预期到 app 未来的方向,最可能或者最通用会朝哪个方向发展。从而能做到支持 app 业务快速开发工作。如果让 app 业务开发组用起来还觉得麻烦,而且不能实际解决问题,那么可以想象 app 基本不会使用,宁愿自己开发个简单的,满足自己需求的。
这一点其实之前我比较看好 Picasso 的图片处理库,因为它精巧简洁,能很好满足简单使用和自己自定义的使用。但后来发现 glide 使用的更广泛,因为 glide 加了很多实用功能,比如图片加载和 activty 生命周期绑定,自动三级缓存,按大小缓存(以空间换时间)等等,但它又不想 Fresco 侵入性强,用法复杂等缺点。可以说 glide 提供了一站式服务,而且能满足大部分场景,这一点是其它库无法直接媲美的。对业务开发者来说明显 glide 这种更符合要求。

一直实践以来, redis 的性能很高,且内存占用很小。一直想知道是为什么?这次看 redis 源码,首先就奔着这一点去看了。

redis 底层存储原理

使用了 sds 进行存储,其实他就是 c 的 char 指针,所以说其和 c 是无缝贴合的。
但是它不在由 c 语言进行管理,而是自定义结构体,里面是长度、可用大小,flag 以及数据。

1
2
3
4
5
6
7
8
9
10
typedef char *sds;

struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};

#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))

它的结构很简单,长度,分配的空间,标示和内容。redis 里面涉及到 char 的一般都会用指针找到位置,然后赋值。

实际存储在 db dic 里面的 是 redisObject。里面存储了类型、编码、过期算法、连接数和内容即 sds。

1
2
3
4
5
6
7
8
9
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;

看起来很简单,但它通过一系列措施来确保高效。
比如如果长度小于等于 44,那么就会保存不变区域的 string。具体是因为 1+3+4+8 是 16 个字节,再加上 sds 的最小 3 个字节,还有一个 ‘\0’ 结尾字节,在 64 字节(小字节下避免空间浪费)下得出 44。内部实现会让 redisObject 和 sds 内存一块申请。
append 的时候会检查,如果没有,正常创建。有的话会尝试转为 raw 类型。
总结来看:

  1. 扩容。凭借指针操作和记录的当前 len 和申请 alloc ,可以快速进行判断决定是扩展内存还是直接就申请新的内存。
  2. 内容利用。redis 对小数据做了很多的优化,比如申请一次内存、int 类型直接存储、共享创建等等
  3. 精准内存操作。会尽量到位到具体的内存地址,然后直接操作赋值或者复制等等。

启动流程

官方文档写的挺清晰的,server.c main 内部主要方法是 initServerConfig() -> initServer() -> aeMain(server.el) 。redis 内部真的大量封装了原生的方法,利用宏编译来尽可能地提高在各个平台的性能。比如在 zmalloc 内部进行自己的内存管理,根据各个平台函数提供调用来确保分配的内存和内存大小明确。

注册 file event 回调

initServer -> createSocketAcceptHandler -> aeCreateFileEvent 里面会注册 event handler,比如 acceptTcpHandler 内部会取出数据,然后注册 acceptCommonHandler,conn 状态变为 CONN_STATE_ACCEPTING

1
2
3
4
5
6
7
8
typedef enum {
CONN_STATE_NONE = 0,
CONN_STATE_CONNECTING,
CONN_STATE_ACCEPTING,
CONN_STATE_CONNECTED,
CONN_STATE_CLOSED,
CONN_STATE_ERROR
} ConnectionState;

开始 eventloop

aeMain(server.el) -> aeProcessEvents 内部会 aeApiPoll -> rfileProc 即前面注册的回调调用处理 file event。 以 acceptTcpHandler 为例,先连接 client socket fd,然后进行读取且 conn 状态变为 CONN_STATE_ACCEPTINGacceptTcpHandler 之后调用 acceptCommonHandler 创建 client,将状态变为 CONN_STATE_CONNECTED,执行 clientAcceptHandler 方法,读取成功后调用 moduleFireServerEvent 来进行执行之前注册到 RedisModule_EventListeners 的 callback。
那么它们是怎么和命令处理关联起来的。
acceptCommonHandler 方法创建 client 的时候会创建 createClient,在创建过程中会通过 connSetReadHandler 调用 connSocketSetReadHandler 注册 readQueryFromClient 方法。 而 connSetReadHandler 方法内部是关键,它会readQueryFromClient 方法赋值 conn->read_handler,然后调用 aeCreateFileEvent 创建事件,mark 是 AE_READABLE,回调是 .ae_handler(connSocketEventHandler),其内部会根据连接情况和最后的处理分别调用方法。
InitServerLast -> initThreadedIO -> IOThreadMain -> readQueryFromClient -> processInputBuffer -> processCommandAndResetClient
总结一下,可以分为几个步骤:

  1. socket 监听,调用系统的方法进行监听,然后进行封装打标记,并赋值 eventLoop->fired,返回数量。
  2. eventloop 根据数量遍历,从 fired 拿到相应 fd、mark 信息,然后回调一开始初始化设置的监听 accept aeFileProc,不管哪种如 ip 或者 tls 等,最后都会调用 acceptCommonHandler,该方法作用在上面详细说明了。它最终会产生 readQueryFromClient 事件。
  3. readQueryFromClient 会解析客户端发起的参数,解析,然后进行命令的调用,它会调用一开始 populateCommandTable 方法注册的命令(redisCommand redisCommandTable[] 里读取)。
  4. 以 get 为例,会从 db 的 dic 查询 key 的值(db 默认为 0 ,除非 select 切换)。

redis 内部举例

list 用的 quicklist,本质上就是双向链表,但是它内部还兼容了 ziplist,quicklist 内部的 node 会有 char 数组来存储压缩的内容。

结论

  1. redis 主要是充分利用内存会让缓存达到很高的速度,在辅助一些内存优化即可实现高速缓存。
  2. redis 内部的单线程来确保避免了线程的切换损耗,网络 IO 这块用多线程会有更好的性能。
  3. 启发,其实很多时候程序的瓶颈都在内存、网络、存储等等,cpu 存在不够的情况很低,cpu 不够如果不是因为高计算量,其实优化空间很大。所以在我们开发的时候重点关注那些会产生 IO 的操作。

protobuf 优势

  • 跨平台跨语言
  • 高效

准备

  • 安装 protobuf
1
2
go install google.golang.org/protobuf/cmd/protoc-gen-go
https://github.com/protocolbuffers/protobuf 下载最新release
  • 新建 blockchain.proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
syntax = "proto3";
package tutorial;


import "google/protobuf/timestamp.proto";


option go_package = ".;test";


message Transaction{
string Sender = 1;
string Recipient = 2;
int32 Amount = 3;
}


message Block {
int32 Index = 1;
google.protobuf.Timestamp Timestamp = 2;


repeated Transaction Transactions = 3;


int32 Proof = 4;
string PreviousHash = 5;
}


message BlockchainData{
repeated Block CurrentBlockchain = 1;
repeated Transaction CurrentTransactions = 2;
repeated string Nodes = 3;
}
  • 生成 go 文件,注意目录
1
protoc  -I=/Users/test --go_out=/Users/test /Users/blockchain.proto
  • test,使用 reids 存储
1
2
3
4
5
6
7
8
var jsonTest bool
func GetData(data interface{}) ([]byte, error) {
if jsonTest {
return json.Marshal(data)
} else {
return proto.Marshal(data.(proto.Message))
}
}

现象

  • 10000次,转后存redis。花费7.7s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
redis not open
[StoreAllChain] time cost = 1.275416ms
redis not open
[StoreAllChain] time cost = 1.040458ms
[TestProtoSpeed] time cost = 6.619953912s

Redis open
[StoreAllChain] time cost = 1.471769ms
[StoreAllChain] time cost = 1.158825ms
[StoreAllChain] time cost = 1.081494ms
[TestProtoSpeed] time cost = 7.785562597s

删除其它日志
[TestProtoSpeed] time cost = 7.827823686s
--- PASS: TestProtoSpeed (7.83s)
  • 10000次,json 对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
redis not open
[StoreAllChain] time cost = 2.838655ms
redis not open
[StoreAllChain] time cost = 2.539433ms
redis not open
[StoreAllChain] time cost = 2.775757ms
[TestJsonSpeed] time cost = 12.611491383s

Redis open
[StoreAllChain] time cost = 2.643816ms
[StoreAllChain] time cost = 3.363096ms
[StoreAllChain] time cost = 2.989628ms
[StoreAllChain] time cost = 2.51413ms
[TestJsonSpeed] time cost = 16.264468555s

删除其它日志
[TestJsonSpeed] time cost = 17.868695349s
--- PASS: TestJsonSpeed (17.88s)

测试平均结果

10000 20000
Protobuf 7 24
Json 16 61

结论

protobuf 在 go 上的表现和其原始并没有量级的差距,但还是好过 go 原生 json 解析不少。其跨语言跨平台的特性和高效还是值得一试的。

0%