缓存是高并发服务的基础,毫不夸张的说没有缓存高并发服务就无从谈起。本项目缓存使用Redis,Redis是目前主流的缓存数据库,支持丰富的数据类型,其中集合类型的底层主要依赖:整数数组、双向链表、哈希表、压缩列表和跳表五种数据结构。由于底层依赖的数据结构的高效性以及基于多路复用的高性能I/O模型,所以Redis也提供了非常强悍的性能。下图展示了Redis数据类型对应的底层数据结构。
在go-zero中默认集成了缓存model数据的功能,我们在使用goctl自动生成model代码的时候加上 -c 参数即可生成集成缓存的model代码
goctl model mysql datasource -url="root:123456@tcp(127.0.0.1:3306)/product" -table="*" -dir="./model" -c通过简单的配置我们就可以使用model层的缓存啦,model层缓存默认过期时间为7天,如果没有查到数据会设置一个空缓存,空缓存的过期时间为1分钟,model层cache配置和初始化如下:
CacheRedis: - Host: 127.0.0.1:6379 Type: node CategoryModel: model.NewCategoryModel(conn, c.CacheRedis)这次演示的代码主要会基于product-rpc服务,为了简单我们直接使用grpcurl来进行调试,注意启动的时候主要注册反射服务,通过goctl自动生成的rpc服务在dev或test环境下已经帮我们注册好了,我们需要把我们的mode设置为dev,默认的mode为pro,如下代码所示:
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { product.RegisterProductServer(grpcServer, svr) if c.Mode == service.DevMode || c.Mode == service.TestMode { reflection.Register(grpcServer) } })直接使用go install安装grpcurl工具,so easy !!!妈妈再也不用担心我不会调试gRPC了
go install github.com/fullstorydev/grpcurl/cmd/grpcurl启动服务,通过如下命令查询服务,服务提供的方法,可以看到当前提供了Product获取商品详情接口和Products批量获取商品详情接口
~ grpcurl -plaintext 127.0.0.1:8081 list grpc.health.v1.Health grpc.reflection.v1alpha.ServerReflection product.Product ~ grpcurl -plaintext 127.0.0.1:8081 list product.Product product.Product.Product product.Product.Products我们先往product表里插入一些测试数据,测试数据放在lebron/sql/data.sql文件中,此时我们查看id为1的商品数据,这时候缓存中是没有id为1这条数据的
127.0.0.1:6379> EXISTS cache:product:product:id:1 (integer) 0通过grpcurl工具来调用Product接口查询id为1的商品数据,可以看到已经返回了数据
~ grpcurl -plaintext -d '{"product_id": 1}' 127.0.0.1:8081 product.Product.Product { "productId": "1", "name": "夹克1" }再看redis中已经存在了id为1的这条数据的缓存,这就是框架给我们自动生成的缓存
127.0.0.1:6379> get cache:product:product:id:1 {\"Id\":1,\"Cateid\":2,\"Name\":\"\xe5\xa4\xb9\xe5\x85\x8b1\",\"Subtitle\":\"\xe5\xa4\xb9\xe5\x85\x8b1\",\"Images\":\"1.jpg,2.jpg,3.jpg\",\"Detail\":\"\xe8\xaf\xa6\xe6\x83\x85\",\"Price\":100,\"Stock\":10,\"Status\":1,\"CreateTime\":\"2022-06-17T17:51:23Z\",\"UpdateTime\":\"2022-06-17T17:51:23Z\"}我们再请求id为666的商品,因为我们表里没有id为666的商品,框架会帮我们缓存一个空值,这个空值的过期时间为1分钟
127.0.0.1:6379> get cache:product:product:id:666 "*"当我们删除数据或者更新数据的时候,以id为key的行记录缓存会被删除
缓存索引我们的分类商品列表是需要支持分页的,通过往上滑动可以不断地加载下一页,商品按照创建时间倒序返回列表,使用游标的方式进行分页。
怎么在缓存中存储分类的商品呢?我们使用Sorted Set来存储,member为商品的id,即我们只在Sorted Set中存储缓存索引,查出缓存索引后,因为我们自动生成了以主键id索引为key的缓存,所以查出索引列表后我们再查询行记录缓存即可获取商品的详情,Sorted Set的score为商品的创建时间。
下面我们一起来分析分类商品列表的逻辑该怎么写,首先先从缓存中读取当前页的商品id索引,调用cacheProductList方法,注意,这里调用查询缓存方法忽略了error,为什么要忽略这个error呢,因为我们期望的是尽最大可能的给用户返回数据,也就是redis挂掉了的话那我们就会从数据库查询数据返回给用户,而不会因为redis挂掉而返回错误。
pids, _ := l.cacheProductList(l.ctx, in.CategoryId, in.Cursor, int64(in.Ps))cacheProductList方法实现如下,通过ZrevrangebyscoreWithScoresAndLimitCtx倒序从缓存中读数据,并限制读条数为分页大小
func (l *ProductListLogic) cacheProductList(ctx context.Context, cid int32, cursor, ps int64) ([]int64, error) { pairs, err := l.svcCtx.BizRedis.ZrevrangebyscoreWithScoresAndLimitCtx(ctx, categoryKey(cid), cursor, 0, 0, int(ps)) if err != nil { return nil, err } var ids []int64 for _, pair := range pairs { id, _ := strconv.ParseInt(pair.Key, 10, 64) ids = append(ids, id) } return ids, nil }为了表示列表的结束,我们会在Sorted Set中设置一个结束标志符,该标志符的member为-1,score为0,所以我们在从缓存中查出数据后,需要判断数据的最后一条是否为-1,如果为-1的话说明列表已经加载到最后一页了,用户再滑动屏幕的话前端就不会再继续请求后端的接口了,逻辑如下,从缓存中查出数据后再根据主键id查询商品的详情即可
pids, _ := l.cacheProductList(l.ctx, in.CategoryId, in.Cursor, int64(in.Ps)) if len(pids) == int(in.Ps) { isCache = true if pids[len(pids)-1] == -1 { isEnd = true } }如果从缓存中查出的数据为0条,那么我们就从数据库中查询该分类下的数据,这里要注意从数据库查询数据的时候我们要限制查询的条数,我们默认一次查询300条,因为我们每页大小为10,300条可以让用户下翻30页,大多数情况下用户根本不会翻那么多页,所以我们不会全部加载以降低我们的缓存资源,当用户真的翻页超过30页后,我们再按需加载到缓存中
func (m *defaultProductModel) CategoryProducts(ctx context.Context, cateid, ctime, limit int64) ([]*Product, error) { var products []*Product err := m.QueryRowsNoCacheCtx(ctx, &products, fmt.Sprintf("select %s from %s where cateid=? and status=1 and create_time