高并发代码怎么写?
笔者最近从go zero框架写的代码中获得不小启发。所以写出一个文档用来总结,并且尝试把go zero的这些并发特性,用spring boot实现。
- 没有查询条件到Primary映射的缓存
- 通过查询条件到DB去查询行记录,然后
- 把Primary到行记录的缓存写到redis里
- 把查询条件到Primary的映射保存到redis里,框架的Take方法自动做了
- 可能的过期顺序
- 查询条件到Primary的映射缓存未过期
- Primary到行记录的缓存未过期
- 直接返回缓存行记录
- Primary到行记录的缓存已过期
- 通过Primary到DB获取行记录,并写入缓存
- 此时存在的问题是,查询条件到Primary的缓存可能已经快要过期了,短时间内的查询又会触发一次数据库查询
- 要避免这个问题,可以让上面粗体部分第一个过期时间略长于第二个,比如5秒
- 通过Primary到DB获取行记录,并写入缓存
- Primary到行记录的缓存未过期
- 查询条件到Primary的映射缓存已过期,不管Primary到行记录的缓存是否过期
- 查询条件到Primary的映射会被重新获取,获取过程中会自动写入新的Primary到行记录的缓存,这样两种缓存的过期时间都是刚刚设置
- 查询条件到Primary的映射缓存未过期
- 通过查询条件到DB去查询行记录,然后
- 有查询条件到Primary映射的缓存
- 没有Primary到行记录的缓存
- 通过Primary到DB查询行记录,并写入缓存
- 有Primary到行记录的缓存
- 直接返回缓存结果
- 没有Primary到行记录的缓存
1.第一种情况,基于主键的缓存
- 我们基于key,从缓存里查找数据
- 如果找到了,就返回数据
- 找不到,就去查询数据库查询数据
- 查询到数据库的数据之后,根据 key,查询到的value设置到数据库当中
2.第二种情况,基于唯一索引的缓存逻辑
代码分为两个block,
在block1当中:
找到索引和主键的缓存的情况:
- 从redis当中查找唯一索引index-key的缓存,这个时候找到的话,找到的key。
- 如果查到了key-value的映射,直接返回value
- 如果找不到value,从数据库查询到value
- 在redis中设置key-value的缓存
找不到索引和主键的缓存情况:
- 通过唯一索引从数据库查找结果index-value
- 设置index-key的缓存
- 设置key-value的缓存
总结下db缓存设计:
- 都是先更新db,然后删除缓存
- 缓存只删除不更新
- 行记录始终只存储一份,即主键对应行记录
- 唯一索引仅缓存主键值,不直接缓存行记录(参考mysql索引思想)
- 防缓存穿透设计,默认一分钟
- 不缓存多行记录
3.业务层缓存设计
- 增加内存缓存:通过内存缓存来存储当前可能突发访问量比较大的数据,常用的存储方案采用map数据结构来存储,map数据存储实现比较简单,但缓存过期处理则需要增加定时器来处理,另一种方案是通过go-zero库中的 Cache ,其是专门 用于内存管理.
- 采用biz redis,并设置合理的过期时间
1 | type Content struct { |
4.内存缓存
cache 实现的功能包括
- 缓存自动失效,可以指定过期时间
- 缓存大小限制,可以指定缓存个数
- 缓存增删改
- 缓存命中率统计
- 并发安全
- 缓存击穿
5.缓存击穿
缓存击穿是指访问某个非常热的数据,缓存不存在,导致大量的请求发送到了数据库,这会导致数据库压力陡增,缓存击穿经常发生在热点数据过期失效时,如下图所示:
既然缓存击穿经常发生在热点数据过期失效的时候,那么我们不让缓存失效不就好了,每次查询缓存的时候不要使用Exists来判断key是否存在,而是使用Expire给缓存续期,通过Expire返回结果判断key是否存在,既然是热点数据通过不断地续期也就不会过期了
还有一种简单有效的方法就是通过singleflight来控制,singleflight的原理是当同时有很多请求同时到来时,最终只有一个请求会最终访问到资源,其他请求都会等待结果然后返回。获取商品详情使用singleflight进行保护示例如下:
1 | func (l *ProductLogic) Product(in *product.ProductItemRequest) (*product.ProductItem, error) { |
6.缓存穿透
缓存穿透是指要访问的数据既不在缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。此时也就没办法从数据库中读出数据再写入缓存来服务后续的请求,类似的请求如果多的话就会给缓存和数据库带来巨大的压力。
针对缓存穿透问题,解决办法其实很简单,就是缓存一个空值,避免每次都透传到数据库,缓存的时间可以设置短一点,比如1分钟,其实上文已经有提到了,当我们访问不存在的数据的时候,go-zero框架会帮我们自动加上空缓存,比如我们访问id为999的商品,该商品在数据库中是不存在的。
7.缓存雪崩
缓存雪崩时指大量的的应用请求无法在Redis缓存中进行处理,紧接着应用将大量的请求发送到数据库,导致数据库被打挂,好惨呐!!缓存雪崩一般是由两个原因导致的,应对方案也不太一样。
第一个原因是:缓存中有大量的数据同时过期,导致大量的请求无法得到正常处理。
针对大量数据同时失效带来的缓存雪崩问题,一般的解决方案是要避免大量的数据设置相同的过期时间,如果业务上的确有要求数据要同时失效,那么可以在过期时间上加一个较小的随机数,这样不同的数据过期时间不同,但差别也不大,避免大量数据同时过期,也基本能满足业务的需求。
第二个原因是:Redis出现了宕机,没办法正常响应请求了,这就会导致大量请求直接打到数据库,从而发生雪崩
针对这类原因一般我们需要让我们的数据库支持熔断,让数据库压力比较大的时候就触发熔断,丢弃掉部分请求,当然熔断是对业务有损的。
在go-zero的数据库客户端是支持熔断的,如下在ExecCtx方法中使用熔断进行保护
1 | func (db *commonSqlConn) ExecCtx(ctx context.Context, q string, args ...interface{}) ( |