前言
我们经常使用redis作为1级缓存,它能有效帮助服务层,减少直达数据库的频率,减少查询时间。但是,redis作为缓存仍旧有几个问题:
- 网络开销。服务节点到redis之间,通过tcp通信,这是有开销的。
- 序列化开销。存储结构上,我们往往使用json来将需要缓存的模型序列化到redis上,使用时解析。这一块开销也非常明显。
- 某些配置的值会变动,但是配置的频率非常低,几天几个礼拜才改一下,没有必要浪费1、2步的开销。
至此,我们仍然要为一些场景,配置二级缓存。它服务于:
- 不常变化的键值,配置。
- 实时服务追求极速,redis网络耗时和序列化时想要继续节省。
【二级缓存】具备以下特点:
- 高仿redis,具备可配置的失效期,并且要很短10-30秒。
- 并发安全,具备hash隔离。
- 和业务模型强相关。
- 死亡周期是redis缓存的子集(redis缓存死了,二级缓存必须死。二级缓存死了,redis缓存不一定死)。
- 二级缓存在单点服务里,是缓存一致的
实现
github.com/fwhezfwhez/cmap
关键api
m := cmap.NewMapV2(nil, 16, 10*time.Minute)
m.SetEx()
m.Get()
m.Delete()
个性化定制
userCache := cmap.NewMapV2(nil, 16, 10*time.Minute)
func GetUserInfo(userId string) UserInfo{
u, exist :=userCache.Get(userId)
if exist {
return u.(UserInfo)
}
// user,_ :=GetFromURL()
// user,_ :=GetFromDB()
// user, _ :=GetFromRedis()
// 如果可以的话,在DeleteFromrRedis里,也需要调用 userCache.Delete(userId)
userCache.SetEx(userId, user, 30)
return user
}
利用自动生成来维护【2级缓存】【1级缓存】【数据库】
- 以下代码,利用开源库 github.com/fwhezfwhez/model_convert生成。
生成用例:
func TestTableToStructWithTag(t *testing.T) {
dataSouce := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=%s password=%s", "localhost", "5432", "postgres", "game", "disable", "123")
tableName := "user_transfer_process"
fmt.Println(model_convert.TableToStructWithTag(dataSouce, tableName, map[string]interface{}{
"dialect": "postgres",
"${db_instance}": "db.DB",
"${db_instance_pkg}": "path/to/db",
}))
}
输出结果
import (
"encoding/json"
"fmt"
"github.com/fwhezfwhez/errorx"
"github.com/fwhezfwhez/cmap"
"github.com/garyburd/redigo/redis"
"github.com/jinzhu/gorm"
"path/to/db"
"io/ioutil"
"net/http"
"time"
)
// Auto-Generate Header
/*
Code is auto-generated by github.com/fwhezfwhez/model_convert.Package below might be imported:
- github.com/fwhezfwhez/errorx
- github.com/garyburd/redigo/redis
- github.com/fwhezfwhez/cmap
- github.com/jinzhu/gorm
- shangraomajiang/util/db
You can get them by:
- go get github.com/fwhezfwhez/errorx
- go get github.com/garyburd/redigo/redis
- go get github.com/jinzhu/gorm
To fulfill redis part, don't forget to set TODOs.They are:
- RedisKey() string
- RedisSecondDuration() int
*/
type MatchInfo struct{
Id int `gorm:"column:id;default:" json:"id" form:"id"`
UpdatedAt time.Time `gorm:"column:updated_at;default:" json:"updated_at" form:"updated_at"`
CreatedAt time.Time `gorm:"column:created_at;default:" json:"created_at" form:"created_at"`
ClubId int `gorm:"column:club_id;default:" json:"club_id" form:"club_id"`
Description string `gorm:"column:description;default:" json:"description" form:"description"`
GameId int `gorm:"column:game_id;default:" json:"game_id" form:"game_id"`
GameAreaId int `gorm:"column:game_area_id;default:" json:"game_area_id" form:"game_area_id"`
RangeProps json.RawMessage `gorm:"column:range_props;default:" json:"range_props" form:"range_props"`
StartAt time.Time `gorm:"column:start_at;default:" json:"start_at" form:"start_at"`
EndAt time.Time `gorm:"column:end_at;default:" json:"end_at" form:"end_at"`
}
func (o MatchInfo) TableName() string {
return "match_info"
}
func (o MatchInfo) DB() *gorm.DB {
return db.DB
}
var MatchInfoRedisKeyFormat = ""
func (o MatchInfo) RedisKey() string {
// TODO set its redis key and required args
return fmt.Sprintf(MatchInfoRedisKeyFormat, )
}
var ArrayMatchInfoRedisKeyFormat = ""
func (o MatchInfo) ArrayRedisKey() string {
// TODO set its array key and required args
return fmt.Sprintf(ArrayMatchInfoRedisKeyFormat,)
}
// 2nd-cache switch.More detail refer to '2nd-cache Header'
const (
// Whether use cache
MatchInfoCacheSwitch = false
// Whether use array cache
ArrayMatchInfoCacheSwitch = false
)
func (o MatchInfo) RedisSecondDuration() int {
// TODO set its redis duration, default 1-7 day, return -1 means no time limit
return int(time.Now().Unix() % 7 + 5) * 60
}
// TODO,set using db or not. If set false, o.MustGet() will never get its data from db.
// do not use this
func (o MatchInfo) UseDB() bool {
return false
}
func (o *MatchInfo) GetFromRedis(conn redis.Conn) error {
if o.RedisKey() == "" {
return errorx.NewFromString("object MatchInfo has not set redis key yet")
}
buf,e:= redis.Bytes(conn.Do("GET", o.RedisKey()))
if e==nil && string(buf)=="DISABLE"{
return fmt.Errorf("not found record in db nor redis")
}
if e == redis.ErrNil {
return e
}
if e != nil && e != redis.ErrNil {
return errorx.Wrap(e)
}
e = json.Unmarshal(buf, &o)
if e!=nil {
return errorx.Wrap(e)
}
return nil
}
func (o *MatchInfo) ArrayGetFromRedis(conn redis.Conn) ([]MatchInfo, error) {
if o.ArrayRedisKey() == "" {
return nil, errorx.NewFromString("object MatchInfo has not set redis key yet")
}
var list = make([]MatchInfo, 0, 10)
buf, e := redis.Bytes(conn.Do("GET", o.ArrayRedisKey()))
// avoid passing through and hit database
// When o.ArrayMustGet() not found both in redis and db, will set its key DISABLE
// and return 'fmt.Errorf("not found record in db nor redis")'
if e == nil && string(buf) == "DISABLE" {
return nil, fmt.Errorf("not found record in db nor redis")
}
// Not found in redis
if e == redis.ErrNil {
return nil, e
}
// Server error, should be logged by caller
if e != nil && e != redis.ErrNil {
return nil, errorx.Wrap(e)
}
e = json.Unmarshal(buf, &list)
if e != nil {
return nil, errorx.Wrap(e)
}
return list, nil
}
// engine should prepare its condition.
// if record not found,it will return 'var notFound = fmt.Errorf("not found record in db nor redis")'.
// If you want to ignore not found error, do it like:
// if e:= o.MustGet(conn, engine.Model(Model{}).Where("condition =?", arg)).Error;e!=nil {
// if e.Error() == "not found record in db nor redis"{
// log.Println(e)
// return
// }
// }
func (o *MatchInfo) MustGet(conn redis.Conn, engine *gorm.DB) error {
var shouldSyncToCache bool
if MatchInfoCacheSwitch {
if e := o.getFromCache(); e == nil {
return nil
}
defer func() {
if shouldSyncToCache {
fmt.Println("exec sync to cache")
o.syncToCache()
}
}()
}
e := o.GetFromRedis(conn)
// When redis key stores its value 'DISABLE', will returns notFoundError and no need to query from db any more
if e!=nil && e.Error() == "not found record in db nor redis" {
return e
}
if e == nil {
shouldSyncToCache = true
return nil
}
if e != nil {
var count int
if e2 := engine.Count(&count).Error; e2 != nil {
return errorx.GroupErrors(errorx.Wrap(e), errorx.Wrap(e2))
}
if count == 0 {
var notFound = fmt.Errorf("not found record in db nor redis")
if o.RedisSecondDuration() == -1 {
conn.Do("SET", o.RedisKey(), "DISABLE", "NX")
} else {
conn.Do("SET", o.RedisKey(), "DISABLE", "EX", o.RedisSecondDuration(), "NX")
}
return notFound
}
if e3 := engine.First(&o).Error; e3 != nil {
return errorx.GroupErrors(errorx.Wrap(e), errorx.Wrap(e3))
}
shouldSyncToCache = true
if e == redis.ErrNil {
o.SyncToRedis(conn)
return nil
}
return errorx.Wrap(e)
}
return nil
}
func (o *MatchInfo) ArrayMustGet(conn redis.Conn, engine *gorm.DB) ([]MatchInfo, error) {
var shouldSyncToCache bool
var arr []MatchInfo
if ArrayMatchInfoCacheSwitch {
if arr, e := o.ArrayGetFromCache(); e == nil {
return arr, nil
}
defer func() {
if shouldSyncToCache {
fmt.Println("exec sync to cache")
o.ArraySyncToCache(arr)
}
}()
}
list, e := o.ArrayGetFromRedis(conn)
// When redis key stores its value 'DISABLE', will returns notFoundError and no need to query from db any more
// When call ArrayDeleteFromRedis(), will activate its redis and db query
if e != nil && e.Error() == "not found record in db nor redis" {
return nil, e
}
// get from redis success.
if e == nil {
shouldSyncToCache = true
arr = list
return list, nil
}
// get from redis fail, try db
if e != nil {
var count int
if e2 := engine.Count(&count).Error; e2 != nil {
return nil, errorx.GroupErrors(errorx.Wrap(e), errorx.Wrap(e2))
}
if count == 0 {
var notFound = fmt.Errorf("not found record in db nor redis")
if o.RedisSecondDuration() == -1 {
conn.Do("SET", o.ArrayRedisKey(), "DISABLE", "NX")
} else {
conn.Do("SET", o.ArrayRedisKey(), "DISABLE", "EX", o.RedisSecondDuration(), "NX")
}
return nil, notFound
}
if e3 := engine.Find(&list).Error; e3 != nil {
return nil, errorx.GroupErrors(errorx.Wrap(e), errorx.Wrap(e3))
}
shouldSyncToCache = true
arr = list
// try sync to redis
if e == redis.ErrNil {
o.ArraySyncToRedis(conn, list)
return list, nil
}
return nil, errorx.Wrap(e)
}
return nil, nil
}
func (o MatchInfo) SyncToRedis(conn redis.Conn) error {
if o.RedisKey() == "" {
return errorx.NewFromString("object MatchInfo has not set redis key yet")
}
buf, e := json.Marshal(o)
if e != nil {
return errorx.Wrap(e)
}
if o.RedisSecondDuration() == -1 {
if _, e := conn.Do("SET", o.RedisKey(), buf); e != nil {
return errorx.Wrap(e)
}
} else {
if _, e := conn.Do("SETEX", o.RedisKey(), o.RedisSecondDuration(), buf); e != nil {
return errorx.Wrap(e)
}
}
return nil
}
func (o MatchInfo) ArraySyncToRedis(conn redis.Conn, list []MatchInfo) error {
if o.ArrayRedisKey() == "" {
return errorx.NewFromString("object MatchInfo has not set redis key yet")
}
buf, e := json.Marshal(list)
if e != nil {
return errorx.Wrap(e)
}
if o.RedisSecondDuration() == -1 {
if _, e := conn.Do("SET", o.ArrayRedisKey(), buf); e != nil {
return errorx.Wrap(e)
}
} else {
if _, e := conn.Do("SETEX", o.ArrayRedisKey(), o.RedisSecondDuration(), buf); e != nil {
return errorx.Wrap(e)
}
}
return nil
}
func (o MatchInfo) DeleteFromRedis(conn redis.Conn) error{
if o.RedisKey() != "" {
if _, e := conn.Do("DEL", o.RedisKey()); e != nil {
return errorx.Wrap(e)
}
}
if o.ArrayRedisKey() != "" {
if _, e := conn.Do("DEL", o.ArrayRedisKey()); e != nil {
return errorx.Wrap(e)
}
}
if MatchInfoCacheSwitch {
o.deleteFromCache()
}
if ArrayMatchInfoCacheSwitch {
o.ArraydeleteFromCache()
}
return nil
}
func (o MatchInfo) ArrayDeleteFromRedis(conn redis.Conn) error{
return o.DeleteFromRedis(conn)
}
// Dump data through api GET remote url generated by 'GenerateListApi()' to local database.
// This method should never used in production. It's best to to run it before app is running.
//
// mode=1, each time will delete old local data and dump from api.
// mode=2, each time will update/keep the existed data. Mode=2 is developing.
func (o MatchInfo) DumpToLocal(url string,engine *gorm.DB, mode int) error {
tableName := o.TableName()
tran := engine.Begin()
if e:=tran.Exec(fmt.Sprintf("delete from %s", tableName)).Error; e!=nil{
tran.Rollback()
return errorx.Wrap(e)
}
type Result struct{
Data []MatchInfo `json:"data"`
Count int `json:"count"`
}
var result Result
resp,e:=http.Get(url)
if e!=nil {
tran.Rollback()
return errorx.Wrap(e)
}
if resp ==nil || resp.Body ==nil {
tran.Rollback()
return errorx.NewFromString("resp or body nil")
}
defer resp.Body.Close()
buf,e := ioutil.ReadAll(resp.Body)
if e!=nil {
tran.Rollback()
return errorx.Wrap(e)
}
if resp.StatusCode != 200 {
var body string
if len(buf)<100 {
body = string(buf)
} else{
body = string(buf[:100])
}
return errorx.NewFromStringf("status not 200, got %d,body %s", resp.StatusCode, body)
}
if e:=json.Unmarshal(buf, &result);e!=nil{
tran.Rollback()
return errorx.Wrap(e)
}
for i,_ := range result.Data {
data := result.Data[i]
if e:=tran.Model(&o).Create(&data).Error; e!=nil{
tran.Rollback()
return errorx.Wrap(e)
}
}
tran.Commit()
return nil
}
// 2nd-cache Header
// 2nd-cache share RedisKey() as its key.
// self Header
var (
MatchInfoCache *cmap.MapV2
MatchInfoNotFoundErr = fmt.Errorf("not found in cache")
MatchInfoSwitchOffErr = fmt.Errorf("2nd-cache switch is off")
)
// cache expires in 15s
func (o *MatchInfo) cacheDuration() int {
return 15
}
func (o *MatchInfo) getFromCache() error {
if MatchInfoCacheSwitch == false {
return MatchInfoSwitchOffErr
}
tmp, ok := MatchInfoCache.Get(o.RedisKey())
if !ok {
return MatchInfoNotFoundErr
}
*o = tmp.(MatchInfo)
fmt.Println("get from cache")
return nil
}
func (o *MatchInfo) deleteFromCache() {
if MatchInfoCacheSwitch == false {
return
}
MatchInfoCache.Delete(o.RedisKey())
}
func (o *MatchInfo) syncToCache() {
if MatchInfoCacheSwitch == false {
return
}
MatchInfoCache.SetEx(o.RedisKey(), *o, o.cacheDuration())
}
// self Tail
// array Header
var (
ArrayMatchInfoCache *cmap.MapV2
ArrayMatchInfoNotFoundErr = fmt.Errorf("not found in cache")
ArrayMatchInfoSwitchOffErr = fmt.Errorf("2nd-cache switch is off")
)
func init() {
if MatchInfoCacheSwitch {
MatchInfoCache = cmap.NewMapV2(nil, 8, time.Second*60)
}
if ArrayMatchInfoCacheSwitch {
ArrayMatchInfoCache = cmap.NewMapV2(nil, 8, time.Second*60)
}
}
func (o *MatchInfo) ArrayGetFromCache() ([]MatchInfo, error) {
if ArrayMatchInfoCacheSwitch == false {
return nil, ArrayMatchInfoSwitchOffErr
}
tmp, ok := ArrayMatchInfoCache.Get(o.ArrayRedisKey())
if !ok {
return nil, ArrayMatchInfoNotFoundErr
}
fmt.Println("get from cache")
return tmp.([]MatchInfo), nil
}
func (o *MatchInfo) ArraydeleteFromCache() {
if ArrayMatchInfoCacheSwitch == false {
return
}
ArrayMatchInfoCache.Delete(o.ArrayRedisKey())
}
func (o *MatchInfo) ArraySyncToCache(arr []MatchInfo) {
if ArrayMatchInfoCacheSwitch == false {
return
}
ArrayMatchInfoCache.SetEx(o.ArrayRedisKey(), arr, o.cacheDuration() )
}
// array Tail
// 2nd-cache Tail
// flexible-cache Header
// func (o MatchInfo) ${cache_name}Key() string{
// // TODO-Set cache redis key
// return ""
// }
// func (o MatchInfo) ${cache_name}Duration() int{
// // TODO-Set cache redis key expire duration. Default 1-7 days
// return int(time.Now().Unix() % 7 + 5) * 60
// }
// func (o *MatchInfo) ${cache_name}MustGet(conn redis.Conn, source func(${cache_name} *${cache_type})error) (${cache_type}, error) {
// rs, e:= redis.${Cache_type}(conn.Do("GET", o.${cache_name}Key()))
// if e !=nil {
// if e == redis.ErrNil {
// if e:=source(&rs); e!=nil {
// return rs, errorx.Wrap(e)
// }
// if _, e= conn.Do("SETEX", o.${cache_name}Key(), ${cache_name}Duration(), rs),; e!=nil {
// return rs, errorx.Wrap(e)
// }
// return rs,nil
// }
// return rs, errorx.Wrap(e)
// }
// return rs,nil
// }
// flexible-cache Tail
// no-decode Header
//
// MustGetNoDecode do most similar work as MustGet do, but it will not unmarshal data from redis into 'o', in the meanwhile, will return its raw json stream as return.
// This function aims to save cost of decoding in the only case that you want to return 'o' itself and has nothing changed to inner values.
// 'engine' should prepare its condition.
// if record not found,it will return 'var notFound = fmt.Errorf("not found record in db nor redis")'.
// If you want to ignore not found error, do it like:
// if buf, e:= o.MustGetNoDecode(conn, engine.Model(Model{}).Where("condition =?", arg)).Error;e!=nil {
// if e.Error() == "not found record in db nor redis" || e == redis.ErrNil {
// log.Println(e)
// return
// }
// }
//
func (o *MatchInfo) MustGetNoDecode(conn redis.Conn, engine *gorm.DB) (json.RawMessage, error) {
var shouldSyncToCache bool
if MatchInfoCacheSwitch {
if e := o.getFromCache(); e == nil {
return nil, nil
}
defer func() {
if shouldSyncToCache {
fmt.Println("exec sync to cache")
o.syncToCache()
}
}()
}
arrBuf, e := o.GetFromRedisNoDecode(conn)
// When redis key stores its value 'DISABLE', will returns notFoundError and no need to query from db any more
if e!=nil && e.Error() == "not found record in db nor redis" {
return nil, e
}
if e == nil {
shouldSyncToCache = true
return arrBuf, nil
}
if e != nil {
var count int
if e2 := engine.Count(&count).Error; e2 != nil {
return nil, errorx.GroupErrors(errorx.Wrap(e), errorx.Wrap(e2))
}
if count == 0 {
var notFound = fmt.Errorf("not found record in db nor redis")
if o.RedisSecondDuration() == -1 {
conn.Do("SET", o.RedisKey(), "DISABLE", "NX")
} else {
conn.Do("SET", o.RedisKey(), "DISABLE", "EX", o.RedisSecondDuration(), "NX")
}
return nil, notFound
}
if e3 := engine.First(&o).Error; e3 != nil {
return nil, errorx.GroupErrors(errorx.Wrap(e), errorx.Wrap(e3))
}
shouldSyncToCache = true
if e == redis.ErrNil {
o.SyncToRedis(conn)
return nil,nil
}
return nil, errorx.Wrap(e)
}
return nil,nil
}
// GetFromRedisNoDecode will return its json raw stream and will not decode into 'o'.
// It aims to save cost of decoding if json stream is decoded slowly.
func (o *MatchInfo) GetFromRedisNoDecode(conn redis.Conn) (json.RawMessage, error) {
if o.RedisKey() == "" {
return nil, errorx.NewFromString("object MatchInfo has not set redis key yet")
}
buf,e:= redis.Bytes(conn.Do("GET", o.RedisKey()))
if e==nil && string(buf)=="DISABLE"{
return nil, fmt.Errorf("not found record in db nor redis")
}
if e == redis.ErrNil {
return nil, e
}
if e != nil && e != redis.ErrNil {
return nil, errorx.Wrap(e)
}
return buf, nil
}
// ArrayMustGetNoDecode will not unmarshal json stream to 'arr' and return json.Rawmessage as return value instead if it's found in redis,
// otherwise will return arr from cache or db.
//
// This function aims to save cost of decoding in the read-only case of 'o'. It means you should do nothing changed to its json value.
/*
arr, arrBuf, e:= o.ArrayMustGetNoDecode(conn, engine)
if e!=nil {
// handle error
}
if len(arrBuf) >0 {
c.JSON(200, gin.H{"message":"success", "data": arrBuf})
} else {
c.JSON(200, gin.H{"message":"success", "data": arr})
}
*/
func (o *MatchInfo) ArrayMustGetNoDecode(conn redis.Conn, engine *gorm.DB) ([]MatchInfo,json.RawMessage, error) {
var shouldSyncToCache bool
var arr []MatchInfo
if ArrayMatchInfoCacheSwitch {
if arr, e := o.ArrayGetFromCache(); e == nil {
return arr, nil, nil
}
defer func() {
if shouldSyncToCache {
fmt.Println("exec sync to cache")
o.ArraySyncToCache(arr)
}
}()
}
arrBuf, e := o.ArrayGetFromRedisNoDecode(conn)
// When redis key stores its value 'DISABLE', will returns notFoundError and no need to query from db any more
// When call ArrayDeleteFromRedis(), will activate its redis and db query
if e != nil && e.Error() == "not found record in db nor redis" {
return nil,nil, e
}
// get from redis success.
if e == nil {
// shouldSyncToCache = true
// arr = list
return nil, arrBuf, nil
}
// get from redis fail, try db
if e != nil {
var list = make([]MatchInfo,0, 100)
var count int
if e2 := engine.Count(&count).Error; e2 != nil {
return nil,nil, errorx.GroupErrors(errorx.Wrap(e), errorx.Wrap(e2))
}
if count == 0 {
var notFound = fmt.Errorf("not found record in db nor redis")
if o.RedisSecondDuration() == -1 {
conn.Do("SET", o.ArrayRedisKey(), "DISABLE", "NX")
} else {
conn.Do("SET", o.ArrayRedisKey(), "DISABLE", "EX", o.RedisSecondDuration(), "NX")
}
return nil, nil, notFound
}
if e3 := engine.Find(&list).Error; e3 != nil {
return nil, nil, errorx.GroupErrors(errorx.Wrap(e), errorx.Wrap(e3))
}
shouldSyncToCache = true
arr = list
// try sync to redis
if e == redis.ErrNil {
o.ArraySyncToRedis(conn, list)
return list, nil, nil
}
return nil, nil, errorx.Wrap(e)
}
return nil, nil, nil
}
func (o *MatchInfo) ArrayGetFromRedisNoDecode(conn redis.Conn) (json.RawMessage, error) {
if o.ArrayRedisKey() == "" {
return nil, errorx.NewFromString("object MatchInfo has not set redis key yet")
}
buf, e := redis.Bytes(conn.Do("GET", o.ArrayRedisKey()))
// avoid passing through and hit database
// When o.ArrayMustGet() not found both in redis and db, will set its key DISABLE
// and return 'fmt.Errorf("not found record in db nor redis")'
if e == nil && string(buf) == "DISABLE" {
return nil, fmt.Errorf("not found record in db nor redis")
}
// Not found in redis
if e == redis.ErrNil {
return nil, e
}
// Server error, should be logged by caller
if e != nil && e != redis.ErrNil {
return nil, errorx.Wrap(e)
}
return buf, nil
}
// no-decode Tail
// Auto-Generate Tail
标记缓存
var MatchInfoRedisKeyFormat = "xyx:match_info:%s:%d"
func (o MatchInfo) RedisKey() string {
// TODO set its redis key and required args
return fmt.Sprintf(MatchInfoRedisKeyFormat, config.Mode, o.ClubId)
}
func (o MatchInfo) RedisSecondDuration() int {
// TODO set its redis duration, default 1-7 day, return -1 means no time limit
return int(time.Now().Unix()%7+1) * 24 * 60 * 60
}
标记是否使用二级缓存
// 2nd-cache switch.More detail refer to '2nd-cache Header'
const (
// Whether use cache
MatchInfoCacheSwitch = false
// Whether use array cache
ArrayMatchInfoCacheSwitch = false
)
查询时:
func GetMatchInfo(clubId int, conn redis.Conn) (matchModel.MatchInfo, error) {
var m = matchModel.MatchInfo{
ClubId: clubId,
}
if conn == nil {
conn = redistool.RedisPool.Get()
defer conn.Close()
}
engine := m.DB().Model(m).Where("club_id=?", clubId)
if e := m.MustGet(conn, engine); e != nil {
if e!=redis.ErrNil && e.Error() != "not found record in db nor redis" {
return m, errorx.Wrap(e)
}
// not found this record, then do init or return errr
// - m.Init()
// - return m, errorx.Wrap(e)
return m, errorx.Wrap(e)
}
return m, nil
}
修改时
m.DB().Model(m).Where("id=?", m.Id).Updates(m)
m.DeleteFromRedis(conn)
结语
回复一些大家在使用【二级缓存】时遇到的问题。
- 二级缓存和redis缓存以及数据库之间的关系,在维护上会不会很困难?
如果每次都基于某一种场景,去临时添加缓存,二级缓存。那么在开发上,为了维护一致性便容易出错。出错的原因体现在,手敲代码容易敲错逻辑,缓存之间的销毁周期,销毁顺序,在考量时带来的心智负担,会让人很容易出错。新人扛不起这一个工作,老人也会有可见的出错概率。
所以,在使用缓存,不论是1级还是2级,要形成良好的1,2级缓存使用习惯。这里,我是推荐【代码生成】,将1,2级缓存的编写自动化,这样就能写一次正确,以后都不会出错了。
- 二级缓存注意事项有哪些?
- 要对1,2级缓存,形成可以复制的【代码生成】方案,消除维护心智。
- 要具备较短的生命周期,10-30秒最好。因为二级缓存是单点的,要和分布式保持最终一致。即仅允许10-30秒内不一致的问题。
- 要和业务模型强绑定。因为不同业务模型,不应该存在race竞争、锁竞争。
- 实现上,最好向Redis靠拢。最少具备【setex】api