衔接上文,继续开发。这一章我们接入Redis缓存功能。
设计Redis缓存方案
我们目前需要缓存Tag和Article,主要是解决读取请求。
Tag缓存方案
查询1,根据TagID判断一个tag是否存在:
- 我们可以用Redis的Set结构存储TagID。新增就添加一个TagID,删除就删除TagID。
- 需要解决的问题就是获取新添加的TagID…这个GORM文档有写到。
查询2,添加Tag的时,判断TagName是否存在:
- 修改Set结构为Hash结构,TagID-TagName做Key-Value,然后HEXISTS判断Key是否存在解决查询1.
- 根据HGETALL获取所有的键值,然后判断值是否有匹配(看起来效率不高)。
查询2拓展:
- 使用Set结构存储TagName,直接判断Name是否在Set结构内。
查询3,获取所有的Tag数量:
- 使用HASH结构,HLEN命令即可实现。
查询4, 获取Tag所有的信息:
- 拓展Hash结构,Key-Value改为TagID-Tag结构。
Tag总结:
- Hash结构,TagID-Tag存储Tag信息(不过期)
- Set结构,存储TagName
Article缓存方案
article字段比较多,且跟Tag相比,占用的内存会稍大(文章内容多)。
所以只缓存被请求过的完整信息,加上过期时间。
查询1,获取一篇文章所有信息:
- 用String存储ID-Article(json化)。
- 需要注意的是,只有从数据库获取了这篇文章,我们才把它加到缓存中去。过期时间1天。
查询2,根据ID判断文章是否存在:
- 这个使用前面的String是否存在判断完全不行,因为了缓存并不会缓存所有的文章信息。
- 可以使用Set结构存储所有文章ID,判断文章是否存在。
查询3,获取文章数量:
- Set结构有SCARD命令可以获取所有的ID数量,它就是所有的文章数量。
Article总结:
- 大量的String,缓存Article所有信息,1天过期。
- Set结构,存储所有文章ID。
分页缓存方案
需要注意的是,我们这里需要分页缓存方案。
1. 最基本的统一缓存方案(适应所有查询条件):
- 访问数据库,根据查询条件、分页条件,得到所有的文章ID。
- 根据文章ID在缓存中查找所有的文章内容。
- 缓存数据不全,则在数据库中查找对应ID文章内容。
- 更新缓存。
2. 没有其他任何查询条件,进行单纯的分页查询:
- 在List缓存中计算分页得到ID。
- 缓存数据不全,查找数据库补齐。
- 根据ID在缓存中获取内容。
目前我实现了方案2,就是一种没有其他查询条件的分页缓存方案。
分页缓存逻辑
1. 如果没有其他查询条件,采用方案2
2. 如果有查询条件,先访问数据库根据查询条件获取ID,然后根据ID从缓存中获取数据。
3. 还可以将查询条件结果存入缓存,相同的查询条件直接从缓存获取数据。
List缓存设计方案
这里其实就是保存了所有有效的ID,用来计算分页。
- 初始化时,将当前SQL有效数据ID加入到缓存List。
- 创建时,RPUSH进List
- 修改时,不需要修改List
- 删除时,用LREM删除指定ID即可。
接入Redis
新增Redis配置
修改 conf/setting.ini 文件,新增redis相关配置
1 2 3 4 5 6 7 |
... [redis] Host = 192.168.1.101:6379 Password = MaxIdle = 30 MaxActive = 30 IdleTimeout = 200 |
修改pkg/setting/setting.go文件,新增Redis相关配置读取:
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 |
... //RedisSection 配置文件 type RedisSection struct { Host string `ini:"Host"` Password string `ini:"Password"` MaxIdle int `ini:"MaxIdle"` MaxActive int `ini:"MaxActive"` IdleTimeout int `ini:"IdleTimeout"` } ... //定义导出变量,用于访问数据 var ( App = &AppSection{} Log = &LogSection{} Server = &ServerSection{} MySQL = &MySQLSection{} Redis = &RedisSection{} ) ... func init() { ... err = cfg.Section("redis").MapTo(Redis) if err != nil { log.Fatal("cfg.MapTo RedisSection err: ", err) } Redis.IdleTimeout = Redis.IdleTimeout * time.Second } |
这里只给出Redis相关修改。
Redis工具包
提供功能如下:
- 连接Redis数据库,操作Redis。
- 提供操作相关接口
在pkg目录下新建gredis文件夹,文件夹内新建redis.go文件,写入内容如下:
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
package gredis import ( "context" "encoding/json" "fmt" "ginBlog/pkg/setting" "net" "time" redis "github.com/go-redis/redis/v8" ) //RedisClient 连接客户端 var RedisClient *redis.Client func init() { RedisClient = redis.NewClient(&redis.Options{ //连接信息 Network: "tcp", //网络类型, tcp 或者 unix, 默认tcp Addr: setting.Redis.Host, //ip:port Username: setting.Redis.Username, //用户名, 使用指定用户名验证当前连接 Password: setting.Redis.Password, //密码, DB: 0, //连接后选中的redis数据库index //命令执行失败时的重试策略 MaxRetries: 3, //命令执行失败时最大重试次数,默认3次重试。 MinRetryBackoff: 8 * time.Millisecond, //每次重试最小间隔时间,默认8ms,-1表示取消间隔 MaxRetryBackoff: 512 * time.Millisecond, //每次重试最大时间间隔,默认512ms,-1表示取消间隔 //超时 DialTimeout: 5 * time.Second, //连接建立超时时间,默认5秒 ReadTimeout: 3 * time.Second, //读超时,默认3秒,-1表示取消读超时 WriteTimeout: 3 * time.Second, //写超时,默认与读超时相等 //连接池容量、闲置连接数量、闲置连接检查 PoolSize: 16, //连接池最大Socket连接数,默认为10倍CPU数量,10 * runtime.NumCPU() MinIdleConns: 8, //启动阶段创建指定数量的Idle连接,并长期维持Idle状态的连接数不少于指定数量。 MaxConnAge: 0 * time.Second, //连接存活时长,超过指定时长则关闭连接。默认为0,不关闭旧连接。 PoolTimeout: 4 * time.Second, //当所有连接都处于繁忙状态时,客户端等待可用连接的最大等待时长。默认为读超时+1秒 IdleTimeout: setting.Redis.IdleTimeout, //关闭闲置连接时间,默认5分钟,-1表示取消闲置超时检查 IdleCheckFrequency: 1 * time.Minute, //闲置连接检查周期,默认为1分钟;-1表示不做检查,只在客户端获取连接时对闲置连接进行处理。 //自定义连接函数 Dialer: func(ctx context.Context, network string, addr string, ) (net.Conn, error) { netDialer := net.Dialer{ Timeout: 5 * time.Second, KeepAlive: 5 * time.Minute, } return netDialer.Dial(network, addr) }, //钩子函数,建立新连接时调用 OnConnect: func(ctx context.Context, cn *redis.Conn, ) error { fmt.Println("Redis Conn =", cn) return nil }, }) ctx := context.Background() pong, err := RedisClient.Ping(ctx).Result() fmt.Println("Redis Ping =", pong, err) } //Exist 判断key是否在Redis中 func Exist( ctx context.Context, key string, ) bool { value, err := RedisClient.Exists(ctx, key).Result() if err != nil { return false } return (value > 0) } //Delete 从Redis中删除键值对 func Delete( ctx context.Context, key string, ) (bool, error) { value, err := RedisClient.Del(ctx, key).Result() return (value > 0), err } //String 相关操作 //Set 操作String类型, 将数据写入Redis // t 过期时间, 单位是秒, 0表示没有过期时间 func Set( ctx context.Context, key string, data interface{}, t time.Duration, ) error { value, err := json.Marshal(data) if err != nil { return err } t = t * time.Second return RedisClient.Set(ctx, key, value, t).Err() } //Get 操作String类型, 从Redis读取数据 func Get( ctx context.Context, key string, ) (string, error) { return RedisClient.Get(ctx, key).Result() } //Hash 相关操作 //HSet 操作Hash类型, 将数据写入Hash结构中 // key 是Redis查找Hash结构的key // id-data 就是Hash结构内k-v func HSet( ctx context.Context, key string, id string, data interface{}, ) error { value, err := json.Marshal(data) if err != nil { return err } return RedisClient.HSet(ctx, key, id, value).Err() } //HGet 操作Hash类型, 从Hash结构中读取数据 // key 是Redis查找Hash结构的key // k 是Hash结构内k-v中的k func HGet( ctx context.Context, key string, k string, ) (string, error) { return RedisClient.HGet(ctx, key, k).Result() } //HMGet 操作Hash类型, 从Hash结构中批量读取数据 // key 是Redis查找Hash结构的key // k...不定参数 类型string func HMGet( ctx context.Context, key string, k ...string, ) ([]interface{}, error) { return RedisClient.HMGet(ctx, key, k...).Result() } //HExist 操作Hash类型, 判断key存在于Hash结构中 func HExist( ctx context.Context, key string, k string, ) bool { exist, err := RedisClient.HExists(ctx, key, k).Result() if err != nil { return false } return exist } //HDelete 操作Hash类型, 从Hash结构中删除数据 // key 是Redis查找Hash结构的key // k 是Hash结构内k-v中的k func HDelete( ctx context.Context, key string, k string, ) (bool, error) { value, err := RedisClient.HDel(ctx, key, k).Result() return (value > 0), err } //List 相关操作 //LPush 操作List类型, 将新元素从表头推入List func LPush( ctx context.Context, key string, value interface{}, ) error { return RedisClient.LPush(ctx, key, value).Err() } //RPush 操作List类型, 将新元素从表尾推入List func RPush( ctx context.Context, key string, value interface{}, ) error { return RedisClient.RPush(ctx, key, value).Err() } //LLen 操作List类型, 返回List内元素个数 func LLen( ctx context.Context, key string, ) (int64, error) { return RedisClient.LLen(ctx, key).Result() } //LRange 操作List类型, 返回指定范围内元素 func LRange( ctx context.Context, key string, start int64, stop int64, ) ([]string, error) { return RedisClient.LRange(ctx, key, start, stop).Result() } //LRemove 操作List类型, 从表尾向表头搜索移除1个Value func LRemove( ctx context.Context, key string, value interface{}, ) error { return RedisClient.LRem(ctx, key, -1, value).Err() } //Set 相关操作 //SAdd 操作Set类型, 将数据member写入Set结构中 func SAdd( ctx context.Context, key string, member string, ) error { return RedisClient.SAdd(ctx, key, member).Err() } //SRemove 操作Set类型, 将数据member从Set结构中移除 func SRemove( ctx context.Context, key string, member string, ) error { return RedisClient.SRem(ctx, key, member).Err() } //SIsMember 操作Set类型, 判断member在Set结构中 func SIsMember( ctx context.Context, key string, member string, ) bool { isMember, err := RedisClient.SIsMember(ctx, key, member).Result() if err != nil { return false } return isMember } //ZSet 相关操作 //ZAdd 操作ZSet类型, 将Member写入ZSet结构中 func ZAdd( ctx context.Context, key string, member interface{}, score float64, ) error { z := redis.Z{ Score: score, Member: member, } return RedisClient.ZAdd(ctx, key, &z).Err() } //ZCount 操作ZSet类型, 统计分值在给定范围内节点数量 func ZCount( ctx context.Context, key string, min string, max string, ) (int64, error) { return RedisClient.ZCount(ctx, key, min, max).Result() } //ZRange 操作ZSet类型, 返回给定范围内所有元素 func ZRange( ctx context.Context, key string, start int64, stop int64, ) ([]string, error) { return RedisClient.ZRange(ctx, key, start, stop).Result() } //ZRemove 操作ZSet类型, 删除包含给定成员的所有节点和分值节点 func ZRemove( ctx context.Context, key string, member interface{}, ) error { return RedisClient.ZRem(ctx, key, member).Err() } |
主要是封装了以下方法:
- 公共方法: Exist、Delete
- String相关方法Get、Set
- Hash相关方法HSet、HGet、HMGet、HExist、HDelete
- List相关方法LPush、RPush、LLen、LRange、LRemove
- Set相关方法SAdd、SRemove、SIsMember
- ZSet相关方法ZAdd、ZCount、ZRange、ZRemove
这一步完成之后,我们就可以接入Redis缓存逻辑了。
提供Article和Tag的缓存Key
1. 缓存Prefix
打开 pkg/e 目录,新建 cache.go 文件,写入内容:
1 2 3 4 5 6 7 |
package e //缓存的Prefix const ( CacheArticle = "ARTICLE" CacheTag = "TAG" ) |
2. 缓存Key
项目根目录下新建service目录,然后新建cacheservice目录。
2.1 tag
Tag的缓存有两个结构,一个Hash结构,一个Set结构。
在cacheservice目录下新建tag.go文件,写入内容如下:
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 50 51 |
package cacheservice import ( "ginBlog/pkg/e" "strings" ) //Tag 获取Key的结构 type Tag struct { ID int Name string State int PageNum int PageSize int } //Tag缓存结构设计: // 1. Hash结构, 存储 <ID - models.Tag> // 2. Set结构, 存储 <Name> // 4. List结构, 存储 <ID>, 无查询条件分页 //GetHashIDTagKey 缓存ID-Tag信息的Hash结构Key func (t *Tag) GetHashIDTagKey() string { keys := []string{ e.CacheTag, "Hash", "ID", "Tag", } return strings.Join(keys, "_") } //GetSetNameKey 获取缓存TagName的Set结构Key func (t *Tag) GetSetNameKey() string { keys := []string{ e.CacheTag, "Set", "Name", } return strings.Join(keys, "_") } //GetListIDKey 获取缓存TadID的List结构Key func (t *Tag) GetListIDKey() string { keys := []string{ e.CacheTag, "List", "ID", } return strings.Join(keys, "_") } |
2.2 article
Article有两个结构,大量的String用来缓存文章信息,一个Set结构缓存所有文章ID。
在cacheservice目录下新建article.go文件,写入内容如下:
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 |
package cacheservice import ( "ginBlog/pkg/e" "strconv" "strings" ) //Article 获取Key的结构 type Article struct { ID int TagID int State int PageNum int PageSize int } //Article 缓存结构设计: // 1. 大量String结构, models.Article, 有过期时间 // 2. Set结构, 存储<ID> //GetDataKeyByID 缓存Article数据的String结构Key func (a *Article) GetDataKeyByID() string { keys := []string{ e.CacheArticle, "Data", } if a.ID > 0 { keys = append(keys, "ID="+strconv.Itoa(a.ID)) return strings.Join(keys, "_") } return "" } //GetSetIDKey 缓存ID信息的Set结构Key func (a *Article) GetSetIDKey() string { keys := []string{ e.CacheArticle, "Hash", "ID", "Article", } return strings.Join(keys, "_") } |
在Service层接入Redis缓存逻辑
1. Tag的缓存逻辑
1. 对 models/tag.go 有一些修改:
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
package models import ( "gorm.io/gorm" ) //Tag 标签 type Tag struct { Model Name string `json:"name"` CreatedBy string `json:"created_by"` ModifiedBy string `json:"modified_by"` State int `json:"state"` } //GetTagByID 获取标签 // id tag.ID int 标签ID func GetTagByID(id int) (*Tag, error) { var tag Tag err := db.Where("id = ? AND deleted_on = ?", id, 0).First(&tag).Error return &tag, err } //GetTagByIDs 依据Id批量获取标签 // id tag.ID int 标签ID func GetTagByIDs(id []int) ([]*Tag, error) { var tags []*Tag err := db.Where("id IN ?", id).Find(&tags).Error return tags, err } //IsExistTagByID 判断标签是否存在 // id tag.ID int 标签ID // ret: bool 如果标签存在返回true,否则返回false func IsExistTagByID(id int) (bool, error) { tag, err := GetTagByID(id) if err != nil { if err == gorm.ErrRecordNotFound { return false, nil } return false, err } return tag.ID > 0, nil } //GetTagByName 根据标签名获取标签 // name tag.Name string 标签名称 func GetTagByName(name string) (*Tag, error) { var tag Tag err := db.Where("name = ? AND deleted_on = ?", name, 0).First(&tag).Error return &tag, err } //IsExistTagByName 判断标签是否存在 // name tag.Name string 标签名称 // ret: bool 如果标签存在返回true, 否则返回false func IsExistTagByName(name string) (bool, error) { tag, err := GetTagByName(name) if err != nil { if err == gorm.ErrRecordNotFound { return false, nil } return false, err } return tag.ID > 0, nil } //GetTags 获取多个标签 func GetTags( pageNum int, pageSize int, maps interface{}, ) (tags []*Tag, err error) { err = db.Where(maps).Where("deleted_on = ?", 0).Offset(pageNum).Limit(pageSize).Find(&tags).Error return } //GetTagTotal 获取标签总数 func GetTagTotal( maps interface{}, ) (count int64, err error) { err = db.Model(&Tag{}).Where(maps).Where("deleted_on = ?", 0).Count(&count).Error return } //AddTag 新增标签 func AddTag( data map[string]interface{}, ) (int, error) { tag := &Tag{ Name: data["name"].(string), State: data["state"].(int), CreatedBy: data["created_by"].(string), } err := db.Create(tag).Error return int(tag.ID), err } //EditTag 修改标签 // 不做deleted_on判断,可以通过接口将deleted_on设置为0(恢复数据) func EditTag( data map[string]interface{}, ) error { return db.Model(&Tag{}).Where("id = ?", data["id"]).Updates(data).Error } //DeleteTag 删除标签 // 实现软删除 func DeleteTag(id int) error { return db.Where("id = ?", id).Delete(&Tag{}).Error } //CleanAllTag 清理标签 // 函数会清理所有已经被软删除的标签 func CleanAllTag() error { return db.Unscoped().Where("deleted_on != ?", 0).Delete(&Tag{}).Error } |
2. 修改 service/tagservice/tag.go 文件:
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
package tagservice import ( "context" "encoding/json" "ginBlog/models" "ginBlog/pkg/gredis" "ginBlog/pkg/logging" "ginBlog/service/cacheservice" "strconv" ) //Tag 缓存数据结构体 // 最重要的是支持和models交互 type Tag struct { ID int Name string CreatedBy string ModifiedBy string State int PageNum int PageSize int } //ExistByName 根据名称判断标签是否存在 func (t *Tag) ExistByName() (bool, error) { //先查找缓存, 缓存没有找数据库 cacheSrvTag := cacheservice.Tag{Name: t.Name} setKey := cacheSrvTag.GetSetNameKey() ctx := context.Background() exist := gredis.SIsMember(ctx, setKey, t.Name) if exist { return exist, nil } return models.IsExistTagByName(t.Name) } //ExistByID 根据ID判断标签是否存在 func (t *Tag) ExistByID() (bool, error) { //先查找缓存, 缓存没有找数据库 cacheSrvTag := cacheservice.Tag{ID: t.ID} hashKey := cacheSrvTag.GetHashIDTagKey() tagID := strconv.Itoa(t.ID) ctx := context.Background() exist := gredis.HExist(ctx, hashKey, tagID) if exist { return exist, nil } //更新缓存 tag, err := t.refreshCacheByID() if err != nil { return models.IsExistTagByID(t.ID) } return tag.ID > 0, nil } //Add 新增标签 func (t *Tag) Add() error { id, err := models.AddTag(map[string]interface{}{ "name": t.Name, "created_by": t.CreatedBy, "state": t.State, }) if err != nil { return err } //添加到缓存 t.ID = id t.addCacheByID() return nil } //Edit 修改标签 func (t *Tag) Edit() error { //操作数据库, 修改Tag信息 err := models.EditTag(map[string]interface{}{ "id": t.ID, "name": t.Name, "modified_by": t.ModifiedBy, "state": t.State, }) if err != nil { return err } //更新缓存 t.refreshCacheByID() return nil } //Delete 删除标签 func (t *Tag) Delete() error { //操作数据库 err := models.DeleteTag(t.ID) if err != nil { return err } //删除缓存 t.deleteCacheByID() return nil } //Get 根据ID获取标签 func (t *Tag) Get() (*models.Tag, error) { //查找Redis缓存 cacheSrvTag := cacheservice.Tag{ID: t.ID} hashKey := cacheSrvTag.GetHashIDTagKey() tagID := strconv.Itoa(t.ID) ctx := context.Background() if gredis.HExist(ctx, hashKey, tagID) { data, err := gredis.HGet(ctx, hashKey, tagID) if err != nil { logging.Info(err) } else { var cacheTag *models.Tag json.Unmarshal([]byte(data), &cacheTag) return cacheTag, nil } } //刷新缓存,同时获取数据 tag, err := t.refreshCacheByID() if err != nil { return nil, err } return tag, nil } //GetAll 根据筛选条件获取Tag func (t *Tag) GetAll() ([]*models.Tag, error) { //没有额外查询条件, 查缓存 if len(t.getMaps()) == 1 { tags, err := t.GetAllByRedis() if err != nil { return tags, err } } //有额外条件, 读数据库 return t.GetAllBySQL() } //GetAllByRedis 从缓存根据筛选条件获取Tag, 没有的数据从SQL读取 func (t *Tag) GetAllByRedis() ([]*models.Tag, error) { //从缓存中获取分页ID ctx := context.Background() cacheSrvTag := cacheservice.Tag{} hashKey := cacheSrvTag.GetHashIDTagKey() setKey := cacheSrvTag.GetSetNameKey() listKey := cacheSrvTag.GetListIDKey() start := int64(t.PageNum) stop := int64(t.PageNum + t.PageSize - 1) vTagIDs, err := gredis.LRange(ctx, listKey, start, stop) if err != nil { return nil, err } //判断数据是否在缓存中 var vNoCache []int for _, tagID := range vTagIDs { exist := gredis.HExist(ctx, hashKey, tagID) if !exist { id, _ := strconv.Atoi(tagID) vNoCache = append(vNoCache, id) } } //缓存中没有的数据, 一次性回表查询, 然后放入缓存 if len(vNoCache) > 0 { sqlTags, err := models.GetTagByIDs(vNoCache) if err == nil { for _, cacheTag := range sqlTags { //更新缓存 tagID := strconv.Itoa(int(cacheTag.ID)) //将TagName写入Set缓存 gredis.SAdd(ctx, setKey, cacheTag.Name) //将Tag写入Hash缓存 gredis.HSet(ctx, hashKey, tagID, cacheTag) } } } //最后从缓存读取数据 vCacheTags, err := gredis.HMGet(ctx, hashKey, vTagIDs...) if err != nil { return nil, err } //构造返回数据 var tags []*models.Tag for _, cacheTag := range vCacheTags { if cacheTag != nil { var tagData models.Tag json.Unmarshal([]byte(cacheTag.(string)), &tagData) tags = append(tags, &tagData) } } return tags, nil } //GetAllBySQL 纯SQL根据筛选条件获取Tag func (t *Tag) GetAllBySQL() ([]*models.Tag, error) { tags, err := models.GetTags(t.PageNum, t.PageSize, t.getMaps()) return tags, err } //Count 返回符合条件的Tag数量 func (t *Tag) Count() (int64, error) { //没有额外查询条件, 直接读取缓存 if len(t.getMaps()) == 1 { ctx := context.Background() cacheSrvTag := cacheservice.Tag{} listKey := cacheSrvTag.GetListIDKey() return gredis.LLen(ctx, listKey) } //否则查询数据库 return models.GetTagTotal(t.getMaps()) } func (t *Tag) getMaps() map[string]interface{} { maps := make(map[string]interface{}) maps["deleted_on"] = 0 if t.Name != "" { maps["name"] = t.Name } if t.State != -1 { maps["state"] = t.State } return maps } func (t *Tag) addCacheByID() error { //根据ID查找数据库获取Tag tag, err := models.GetTagByID(t.ID) if err != nil { return err } //更新缓存 cacheSrvTag := cacheservice.Tag{ID: t.ID} hashKey := cacheSrvTag.GetHashIDTagKey() setKey := cacheSrvTag.GetSetNameKey() listKey := cacheSrvTag.GetListIDKey() tagID := strconv.Itoa(t.ID) ctx := context.Background() //将TagName写入Set缓存 gredis.SAdd(ctx, setKey, tag.Name) //将TagId写入List缓存 gredis.RPush(ctx, listKey, tagID) //将Tag写入Hash缓存 gredis.HSet(ctx, hashKey, tagID, tag) return nil } func (t *Tag) refreshCacheByID() (*models.Tag, error) { //根据ID查找数据库获取Tag tag, err := models.GetTagByID(t.ID) if err != nil { return nil, err } cacheSrvTag := cacheservice.Tag{ID: t.ID} hashKey := cacheSrvTag.GetHashIDTagKey() setKey := cacheSrvTag.GetSetNameKey() tagID := strconv.Itoa(t.ID) ctx := context.Background() //先删除Set中旧Name缓存 exist := gredis.HExist(ctx, hashKey, tagID) if exist { //从Hash中获取TagName, 从Set中删除TagName data, err := gredis.HGet(ctx, hashKey, tagID) if err != nil { logging.Info(err) } else { var cacheTag models.Tag json.Unmarshal([]byte(data), &cacheTag) gredis.SRemove(ctx, setKey, cacheTag.Name) } } //写入新缓存 //将TagName写入Set缓存 gredis.SAdd(ctx, setKey, tag.Name) //将Tag写入Hash缓存 gredis.HSet(ctx, hashKey, tagID, tag) return tag, nil } func (t *Tag) deleteCacheByID() { cacheSrvTag := cacheservice.Tag{} hashKey := cacheSrvTag.GetHashIDTagKey() setKey := cacheSrvTag.GetSetNameKey() listKey := cacheSrvTag.GetListIDKey() tagID := strconv.Itoa(t.ID) ctx := context.Background() exist := gredis.HExist(ctx, hashKey, tagID) if exist { //从Hash中获取TagName, 从Set中删除TagName, data, err := gredis.HGet(ctx, hashKey, tagID) if err != nil { logging.Info(err) } else { var cacheTag models.Tag json.Unmarshal([]byte(data), &cacheTag) err := gredis.SRemove(ctx, setKey, cacheTag.Name) if err != nil { logging.Info(err) } } //从Hash中删除Tag _, err = gredis.HDelete(ctx, hashKey, tagID) if err != nil { logging.Info(err) } } //从List中清理 TagID err := gredis.LRemove(ctx, listKey, tagID) if err != nil { logging.Info(err) } } |
Tag的缓存没有过期时间。
2. Article的缓存逻辑
1. models/article.go代码如下:我也忘了改没改。
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
package models import "gorm.io/gorm" //Article 文章 type Article struct { Model TagID int `json:"tag_id" gorm:"index"` Tag Tag `json:"tag"` Title string `json:"title"` Desc string `json:"desc"` Content string `json:"content"` CreatedBy string `json:"created_by"` ModifiedBy string `json:"Modified_by"` State int `json:"state"` } //IsExistArticleByID 判断文章是否存在 // id article.ID int 文章ID // ret: bool 如果文章存在返回true,否则返回false func IsExistArticleByID(id int) (bool, error) { var article Article err := db.Select("id").Where("id = ?", id).Where("deleted_on = ?", 0).First(&article).Error if err != nil { if err == gorm.ErrRecordNotFound { return false, nil } return false, err } return article.ID > 0, err } //GetArticleTotal 获取文章总数 func GetArticleTotal( maps interface{}, ) (count int64, err error) { err = db.Model(&Article{}). Where(maps). Where("deleted_on = ?", 0). Count(&count). Error return } //GetArticels 获取文章 func GetArticels( pageNum int, pageSize int, maps interface{}, ) (articles []*Article, err error) { err = db.Preload("Tag"). Where(maps). Where("deleted_on = ?", 0). Offset(pageNum). Limit(pageSize). Find(&articles). Error return articles, err } //GetAllArticleIDs 获取所有有效Article的ID func GetAllArticleIDs() (articles []*Article, err error) { err = db.Select("id"). Where("deleted_on = ?", 0). Find(&articles). Error return articles, err } //GetArticle 获取文章 func GetArticle( id int, ) (article *Article, err error) { err = db.Preload("Tag"). Where("id = ?", id). Where("deleted_on = ?", 0). First(article). Error return article, err } //AddArticle 新增文章 func AddArticle( data map[string]interface{}, ) (int, error) { article := &Article{ TagID: data["tag_id"].(int), Title: data["title"].(string), Desc: data["desc"].(string), Content: data["content"].(string), CreatedBy: data["created_by"].(string), State: data["state"].(int), } err := db.Create(article).Error return int(article.ID), err } //EditArticle 更新文章 // 更新接口不判断deleted_on字段 func EditArticle( data map[string]interface{}, ) error { return db.Model(&Article{}).Where("id = ?", data["id"]).Updates(data).Error } //DeleteArticle 删除文章 // 实现软删除 func DeleteArticle(id int) error { return db.Where("id = ?", id).Delete(Article{}).Error } //CleanAllArticle 清理文章 func CleanAllArticle() error { return db.Unscoped().Where("deleted_on != ?", 0).Delete(Article{}).Error } |
2. service/articleservice/article.go文件修改如下:
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
package articleservice import ( "context" "encoding/json" "ginBlog/models" "ginBlog/pkg/gredis" "ginBlog/pkg/logging" "ginBlog/service/cacheservice" "strconv" ) //Article 结构体用于数据库交互 type Article struct { ID int TagID int Title string Desc string Content string State int CreatedBy string ModifiedBy string PageNum int PageSize int } //初始化缓存 func init() { //获取缓存中List数量 cacheSrvArticle := cacheservice.Article{} listKey := cacheSrvArticle.GetListIDKey() ctx := context.Background() cacheLen, err := gredis.LLen(ctx, listKey) if err != nil { logging.Info(err) } //获取数据库中Tag数量 maps := make(map[string]interface{}) sqlLen, err := models.GetArticleTotal(maps) if err != nil { logging.Info(err) } //如果数量不一致, 更新List缓存 if cacheLen != sqlLen { logging.Info("[articleservice.init] Refresh Redis Cache Article.") //删除旧缓存 gredis.Delete(ctx, listKey) //从数据库读取所有的ID articles, err := models.GetAllArticleIDs() if err != nil { logging.Info(err) return } //写入到缓存中去 for _, article := range articles { strID := strconv.Itoa(int(article.ID)) gredis.RPush(ctx, listKey, strID) } } } //ExistByID 根据ID判断文章是否存在 func (a *Article) ExistByID() (bool, error) { //先查找Set缓存,如果缓存里面有就不需要访问数据库 cacheSrvArticle := cacheservice.Article{ID: a.ID} setKey := cacheSrvArticle.GetSetIDKey() strID := strconv.Itoa(a.ID) ctx := context.Background() if gredis.SIsMember(ctx, setKey, strID) { return true, nil } //更新缓存 article, err := refreshCacheByID(a.ID) if err != nil { return models.IsExistArticleByID(a.ID) } return article.ID > 0, nil } //Add 新增文章 func (a *Article) Add() error { id, err := models.AddArticle(map[string]interface{}{ "tag_id": a.TagID, "title": a.Title, "desc": a.Desc, "content": a.Content, "created_by": a.CreatedBy, "state": a.State, }) if err != nil { return err } //新增缓存 a.ID = id a.addCacheByID() return nil } //Edit 修改文章 func (a *Article) Edit() error { //修改数据库内容 err := models.EditArticle(map[string]interface{}{ "id": a.ID, "tag_id": a.TagID, "title": a.Title, "desc": a.Desc, "content": a.Content, "state": a.State, "modified_by": a.ModifiedBy, }) if err != nil { return err } //刷新缓存 refreshCacheByID(a.ID) return err } //Delete 删除文章 func (a *Article) Delete() error { //操作数据库 err := models.DeleteArticle(a.ID) if err != nil { return err } //删除缓存 deleteCacheByID(a.ID) return nil } //Get 根据ID获取文章 func (a *Article) Get() (*models.Article, error) { //查找Redis缓存 cache := cacheservice.Article{ID: a.ID} strID := strconv.Itoa(a.ID) dataKey := cache.GetDataKeyPrefix() + strID ctx := context.Background() if gredis.Exist(ctx, dataKey) { data, err := gredis.Get(ctx, dataKey) if err != nil { logging.Info(err) } else { var cacheArticle *models.Article json.Unmarshal([]byte(data), &cacheArticle) return cacheArticle, nil } } //刷新缓存,同时获取数据 article, err := refreshCacheByID(a.ID) if err != nil { return nil, err } return article, nil } //GetAll 根据筛选条件获取文章 func (a *Article) GetAll() ([]*models.Article, error) { //没有额外查询条件, 查缓存 if len(a.getMaps()) == 1 { articles, err := a.GetAllByRedis() if err != nil { return articles, err } } //有额外条件, 读数据库 return a.GetAllBySQL() } //GetAllByRedis 从缓存根据筛选条件获取Article, 没有的数据从SQL读取 func (a *Article) GetAllByRedis() ([]*models.Article, error) { //从缓存中获取分页ID ctx := context.Background() cacheSrvArticle := cacheservice.Article{} listKey := cacheSrvArticle.GetListIDKey() start := int64(a.PageNum) stop := int64(a.PageNum + a.PageSize - 1) vArticleIDs, err := gredis.LRange(ctx, listKey, start, stop) if err != nil { return nil, err } //构造返回数据 var articles []*models.Article dataKeyPrefix := cacheSrvArticle.GetDataKeyPrefix() for _, cacheArticleID := range vArticleIDs { dataKey := dataKeyPrefix + cacheArticleID if gredis.Exist(ctx, dataKey) { //数据在缓存中,获取数据 data, err := gredis.Get(ctx, dataKey) if err != nil { logging.Info(err) return nil, err } var cacheArticle *models.Article json.Unmarshal([]byte(data), &cacheArticle) articles = append(articles, cacheArticle) } else { //缓存中没找到,刷新缓存获取数据 articleID, err := strconv.Atoi(cacheArticleID) cacheArticle, err := refreshCacheByID(articleID) if err != nil { logging.Info(err) return nil, err } articles = append(articles, cacheArticle) } } return articles, nil } //GetAllBySQL 纯SQL根据筛选条件获取Tag func (a *Article) GetAllBySQL() ([]*models.Article, error) { articles, err := models.GetArticels(a.PageNum, a.PageSize, a.getMaps()) return articles, err } func (a *Article) getMaps() map[string]interface{} { maps := make(map[string]interface{}) maps["deleted_on"] = 0 if a.TagID != -1 { maps["tag_id"] = a.TagID } if a.State != -1 { maps["state"] = a.State } return maps } //Count 返回符合条件文章数量 func (a *Article) Count() (int64, error) { //没有额外查询条件, 直接读取缓存 if len(a.getMaps()) == 1 { ctx := context.Background() cacheSrvArticle := cacheservice.Article{} listKey := cacheSrvArticle.GetListIDKey() return gredis.LLen(ctx, listKey) } //否则查找数据库 return models.GetArticleTotal(a.getMaps()) } func (a *Article) addCacheByID() { //根据ID查找数据库获取Article article, err := models.GetArticle(a.ID) if err != nil { return } //更新缓存 cacheSrvArticle := cacheservice.Article{} strID := strconv.Itoa(a.ID) dataKey := cacheSrvArticle.GetDataKeyPrefix() + strID setKey := cacheSrvArticle.GetSetIDKey() listKey := cacheSrvArticle.GetListIDKey() ctx := context.Background() //将id写入Set缓存 gredis.SAdd(ctx, setKey, strID) //将id写入List缓存 gredis.RPush(ctx, listKey, strID) //将Article写一个String缓存, 过期时间24小时 gredis.Set(ctx, dataKey, article, 3600*24) } //refreshCacheByID 根据ID更新缓存 func refreshCacheByID(ID int) (*models.Article, error) { //根据ID查找数据库获取Article article, err := models.GetArticle(ID) if err != nil { return nil, err } //更新缓存 cacheSrvArticle := cacheservice.Article{} strID := strconv.Itoa(ID) dataKey := cacheSrvArticle.GetDataKeyPrefix() + strID //将Article写一个String缓存, 过期时间24小时 ctx := context.Background() gredis.Set(ctx, dataKey, article, 3600*24) return article, nil } //deleteCacheByID 根据ID删除缓存信息 func deleteCacheByID(ID int) { //清理缓存 cacheSrvArticle := cacheservice.Article{} strID := strconv.Itoa(ID) dataKey := cacheSrvArticle.GetDataKeyPrefix() + strID setKey := cacheSrvArticle.GetSetIDKey() listKey := cacheSrvArticle.GetListIDKey() ctx := context.Background() //删除String缓存 gredis.Delete(ctx, dataKey) //删除Set缓存 gredis.SRemove(ctx, setKey, strID) //删除List缓存 gredis.LRemove(ctx, listKey, strID) } |
嗯,以上就是所有代码…下面是测试…还没做。哈哈哈
博主,有些方法没有暴露呦 方便分享下示例项目的github吗?