0%

问题:如何在不安全的网络上传送秘钥?

假设有两个人 alice 和 bob,在不安全的网络情况下,￿他们俩沟通的每一句话都会被第三方 joe 知道,如何协商出一个只有他们两个人知道的秘钥?
所有的信息都会被第三方知道的情况下,通过两个人独自保密的信息,计算得到一个相同的值.需要的计算量必须非常大,使第三方即使知道沟通的内容,也很难计算得出结果.

Diffie-Hellman 密钥交换协议的有效性依赖计算离散对数的难度.

y=g^x mod p,在已知g,x,p的情况下,计算 y 是非常快的,但是已知 y,g,p 的情况下计算 x 的值是非常困难的.p 取很大的素数.模运算中为何要用素数作为模
秘钥交换过程:

  • alice 和 bob 先协商 g,p 的值
  • alice 私密的值 a , A=g^a mod p ,将 A 发送给 bob
  • bob 私密的值 b, B=g^b mod p,将 B 发送给 alice
  • alice 计算 B^a mod p = (g^b)^a mod p = g^ab mod p = S
  • bob 计算 A^b mod p = (g^a)^b mod p = g^ab mof p = S
  • 现在 alice 和 bob 有了共同的秘钥 S,由于计算量很大,即使 joe 知道 g,p,A,B,也很难计算出 ab 的值

原文 The Go Memory Model
Go的内存模型描述的是”在一个 groutine中 对变量进行读操作能够侦测到在其他goroutine中对该变量的写操作”的条件。

Happens Before

对变量 v 的读 read r 要观测到 write w 对变量 v 的写,需要同时满足:

  • r 不发生在 w 之前
  • 没有其他的 w’ 对变量 v 的写发生在 w 之后, r 之前.

为了保证 read 观察到唯一一个 write w.必须同时满足:

  • w 发生在 r之前
  • 其他 write 发生在 w 之前或者 r 之后
    这对条件比前一对更严格,要求没有其他的 write 没有与 w 或者 r 并发执行.

在单个 goroutine 中没有并发,是满足这两个条件的,保证 r 能看到 w 对 v 的写入.在多个 goroutine 共享变量 v 时,必须使用同步事件
满足 happens-before 条件确保 read 能读取到期望的 writes.

同步

goroutine 创建

The go statement that starts a new goroutine happens before the goroutine’s execution begins

1
2
3
4
5
6
7
8
9
10
11
12
package main
var a string

func f(){
print(a)
}

func hello(){
a = "hello world"
go f()
}

调用 hello 将会打印 “hello world”,可能发生在 hello return 之前或者之后.

channel 通信

A send on a channel happens before the corresponding receive from that channel completes. 在一个channel中,发送 happens-before 相应的接收完成之前.
The closing of a channel happens before a receive that returns a zero value because the channel is closed. 关闭一个 channel happens-before 接收返回的 0 值之前.为 0 值是因为 channel 被关闭.

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
var c = make(chan int,10)
var a = ""
func f(){
a = "hello world"
c <- 1 // 先发生,可以替换成 close(c)
}
func main(){
go f()
<- c
print(a)
}

A receive from an unbuffered channel happens before the send on that channel completes. 无缓冲 channel 的接收 happens-before 在发送完成之前.

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
var c = make(chan int,0)
var a = ""
func f(){
a = "hello world"
c <- 1 // 先发生,可以替换成 close(c)
}
func main(){
go f()
<- c
print(a)
}

The kth receive on a channel with capacity C happens before the k+Cth send from that channel completes. 容量为 C 的 channel 第 k 次接收 happens-before k+C 次发送完成之前.无缓存通道就是C 为 0 时的特殊情况.

不正确的同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a string
var done bool

func setup() {
a = "hello, world"
done = true
}

func main() {
go setup()
for !done {
}
print(a)
}

对于主线程来说, setup 里面的执行顺序是不一致的, done = true 可能发生在 a 赋值之前,更糟糕的是, setup goroutine 中对 done 的修改,对
主线程来说可能是不可见的,将导致主线程死循环.

解决同步问题的思路:使用显示的同步机制.

公司有一个有 6000w 数据量的大表,在查询插入时慢,希望通过分表解决这个问题.

总共分为如下步骤:

  • 分表数规划
  • 分表主键选择
  • 同步脚本编写
  • 增量数据处理
  • 上线迁移

分表数量规划

现有 6000w 数据是 3-4 年的积累, 规划为 单表上线为 500w,现在业务高速发展的时期,希望能支撑未来 2-3 年的使用,
2^5=32 张表,可容纳的总数据量为 32*500w= 1.6亿,满足业务需求.

主键选择

使用 openid(28 位字符串) 作为分表主键,使用 hashcode函数,对 32 取余,分到各个表中.
ps: 用户的数据不多的话,有的用户数据多,有的用户数据少. 不一定能保证数据的均匀性.

同步脚本的编写

开始编写脚本时,发现了两个问题

    1. 在取旧数据时使用的 offsetlimit 的方式,由于 offset 越到后面需要计算的数据量越大,同步特别的慢,
    1. 使用单个线程跑同步脚本,并且还是单条循环插入

第一次改善如下:

    1. 使用主键id区间的模式获取数据,提升的查询的速度
      1
      SELECT * from tablename where id > prevId and id <= nextId
    1. 开启了 32goroutine 同时插入

第一次改善提升了查询的速度,但是插入的速度还是很慢.

第二次改善如下:

  • 将单条插入改为拼接 sql 语句插入,大大提高了插入速度
    但是这个引起了另外一个问题:原来的数据里面有特殊的字符,如:',\等,在插入时会引起 sql语句语法错误,所以需要将他们进行转义.
    最开始将'转义成'',没什么问题,但是 gorm 在执行时会将 \ 转义掉,所以需要将 \ 转义成 \\\\.
    最开始没有发现转义的问题,测试同步完成检验数据的时候发现数据量对不上,然后在同步时将打印错误 log 时才发现.
    脚本如下 ./sync.sh 2>> logs.

增量数据处理

因为业务一直在跑,不能中断,增量数据处理同步写入旧表和新表,待业务低峰期(😂😂半夜) 切换数据表.

上线迁移

上线迁移时,发现磁盘容量不足,需要先扩容磁盘,防止磁盘写满.

在日常编码中,有很多不确定结果,比如网络请求,不确定什么时候能够成功,这就需要我们有一种重试机制,保证服务的高可用性.

重试分为以下几种情况:

  • 重试固定次数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 重试固定次数
    func RetryCount(count int, fn func() error) bool {
    for i := 0; i < count; i++ {
    err := fn()
    if err == nil {
    return true
    }
    }
    return false
    }

  • 在特定时间内重试不定次数,失败后休眠一段时间后继续重试
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
// 在 duration 内,一直调用 fn,直到成功,失败后等待 sleep
func RetryToTimeOut(duration time.Duration, sleep time.Duration, fn func() error) bool {
ch := make(chan struct{})
isEnd := false
go func() {
for {
if isEnd {
break
}
err := fn()
if err == nil {
break
}
time.Sleep(sleep)
}
ch <- struct{}{}
}()
select {
case <-ch:
return true
case <-time.After(duration):
isEnd = true
return false
}
}

如果自定义的函数不是返回error,需要使用闭包再包一下.