cyningsun / blog-sidecar

blog sidecar
0 stars 0 forks source link

sync.singleflight 到底怎么用才对? #111

Open cyningsun opened 3 years ago

cyningsun commented 3 years ago

https://www.cyningsun.com/01-11-2021/golang-concurrency-singleflight.html

背景缓存 在各种场景中被大量使用,在 Cache Miss(缓存未命中)的情况下,就会出现下图的情况:所有的请求被同时打到下游存储上,将会影响下游存储的服务质量,因此需要严格限制访问下游存储的并发量。使用 Golang 编程的人,倾向于不假思索的使用 singleflight 应对 Cache Miss(缓存未命中),即:在绝大多数场景下,singlefli

sttming commented 2 years ago

关于更好的处理方案 “缓存” 存储准实时的数据 + “异步更新” 数据到缓存 有一点疑问: 异步增量更新到redis等缓存,redis发生主从切换是可能丢掉数据更新的,应该如何保证数据的最终一致性呢?

cyningsun commented 2 years ago

关于更好的处理方案 “缓存” 存储准实时的数据 + “异步更新” 数据到缓存 有一点疑问: 异步增量更新到redis等缓存,redis发生主从切换是可能丢掉数据更新的,应该如何保证数据的最终一致性呢?

@sttming 数据存储在 Mysql,然后异步更新到 Redis,此时缓存中存储的是准实时的数据。最终一致依赖的是 Mysql 中的数据,因此主从切换不会有影响

sttming commented 2 years ago

@cyningsun 抱歉,没有描述清楚。按照我的理解,<“缓存” 存储准实时的数据>指的是redis中会缓存全量mysql的数据,依靠<“异步更新” 数据到缓存>对redis中的数据进行更新。我的疑问是异步更新数据到redis时,如果redis发生主从切换,会有概率导致redis丢失这次更新的数据,该如何应对这种情况并保证redis和mysql的数据最终一致呢?

cyningsun commented 2 years ago

@cyningsun 抱歉,没有描述清楚。按照我的理解,<“缓存” 存储准实时的数据>指的是redis中会缓存全量mysql的数据,依靠<“异步更新” 数据到缓存>对redis中的数据进行更新。我的疑问是异步更新数据到redis时,如果redis发生主从切换,会有概率导致redis丢失这次更新的数据,该如何应对这种情况并保证redis和mysql的数据最终一致呢?

文中更多谈低并发更新,高并发读取。异步更新数据到缓存,是指数据直接更新到 Mysql,然后从 Mysql 异步更新到缓存。 高并发更新的最终一致性,要先把并发降低下来,然后依靠一个可靠存储来保证最终一致性(可靠存储的性能一般不高

indexzhuo commented 1 year ago

单并发问题,通过key := key + randN(100)来实现也可以吧,可以减少额外的goroutine。 阻塞读的问题不是很理解,我们对下游rpc请求都会设定一个超时值,例如500ms。当没有使用singleflight的时候,所有请求都会最多等待500ms。如果使用DoChan,并额外设置一个超时时间(例如5ms),那么除了第一个请求,其他请求的超时时间相当于被修改成5ms了,这样是不符合预期的。

cyningsun commented 1 year ago

单并发问题,通过key := key + randN(100)来实现也可以吧,可以减少额外的goroutine。 阻塞读的问题不是很理解,我们对下游rpc请求都会设定一个超时值,例如500ms。当没有使用singleflight的时候,所有请求都会最多等待500ms。如果使用DoChan,并额外设置一个超时时间(例如5ms),那么除了第一个请求,其他请求的超时时间相当于被修改成5ms了,这样是不符合预期的。

  1. 是的
  2. 如果使用 Context 来控制超时,超时时间是一样的。
wjhtime commented 1 year ago

在函数执行完成后直接进行forget不知是否合适?

ch := g.DoChan(key, func() (interface{}, error) {
    ret, err := find(context.Background(), key)
    g.Forget(key)
    return ret, err
})
// Create our timeout
timeout := time.After(500 * time.Millisecond)

var ret singleflight.Result
select {
case <-timeout: // Timeout elapsed
        fmt.Println("Timeout")
    return
case ret = <-ch: // Received result from channel
    fmt.Printf("index: %d, val: %v, shared: %v\n", j, ret.Val, ret.Shared)
}
cyningsun commented 1 year ago

在函数执行完成后直接进行forget不知是否合适?

ch := g.DoChan(key, func() (interface{}, error) {
    ret, err := find(context.Background(), key)
    g.Forget(key)
    return ret, err
})
// Create our timeout
timeout := time.After(500 * time.Millisecond)

var ret singleflight.Result
select {
case <-timeout: // Timeout elapsed
        fmt.Println("Timeout")
    return
case ret = <-ch: // Received result from channel
    fmt.Printf("index: %d, val: %v, shared: %v\n", j, ret.Val, ret.Shared)
}

Forget 是执行完毕之后才调用的。在执行过程中,可能其他协程已拿到同一个 Channel。该部分协程不会受到 Forget 影响

miniyk2012 commented 7 months ago

shared的解释有误, shared表示返回的result是否被多个请求公用, 而不是说是调用fn返回的.

cyningsun commented 7 months ago

shared的解释有误, shared表示返回的result是否被多个请求公用, 而不是说是调用fn返回的.

没错,你的解释更为准确