旅游网站改版方案,wordpress 获取文章id,专门做湘菜的网站,wordpress 背景透明架构#xff1f;何谓架构#xff1f;好像并没有一个准确的概念。以前我觉得架构就是搭出一套完美的框架#xff0c;可以让其他开发人员减少不必要的代码开发量#xff1b;可以完美地实现高内聚低耦合的准则;可以尽可能地实现用最少的硬件资源#xff0c;实现最高的程序效率…架构何谓架构好像并没有一个准确的概念。以前我觉得架构就是搭出一套完美的框架可以让其他开发人员减少不必要的代码开发量可以完美地实现高内聚低耦合的准则;可以尽可能地实现用最少的硬件资源实现最高的程序效率......事实上架构也并非只是追求这些。因为程序是人写出来的所以似乎架构更多的需要考虑人这个因素。
我们发现即便我们在程序设计之初定了诸多规范到了实际开发过程中由于种种原因规范并没有按照我们预想的情况落实。这个时候我的心里突然有一个声音约束大于规范冒了出来。但是约束同样会带来一些问题比如牺牲了一些性能比如带了一定的学习成本。但是似乎一旦约束形成会在后续业务不断发展中带来便利。
架构师似乎总是在不断地做抉择。我想架构师心里一定有一个声音世间安得两全法不负如来不负卿。
Cache接口设计的想法
基于约束大于规范的想法我们有了如下一些约束
第一、把业务中常用到的缓存的方法集合通过接口的方式进行约束。
第二、基于缓存采用cache aside模式。 读数据时先读缓存如果有就返回。没有再读数据源将数据放到缓存 写数据时先写数据源然后让缓存失效
我们把这个规范进行封装以达到约束的目的。
基于上述的约束我们进行了如下的封装
package cacheimport (contexttime
)type Cache interface {// 删除缓存// 先删除数据库数据再删除缓存数据DelCtx(ctx context.Context, query func() error, keys ...string) error// 根据key获取缓存如果缓存不存在// 通过query方法从数据库获取缓存并设置缓存使用默认的失效时间TakeCtx(ctx context.Context, key string, query func() (interface{}, error)) ([]byte, error)// 根据key获取缓存如果缓存不存在// 通过query方法从数据库获取缓存并设置缓存TakeWithExpireCtx(ctx context.Context, key string, expire time.Duration, query func() (interface{}, error)) ([]byte, error)
}细心的朋友可能已经发现这个接口中的方法集合中都包含了一个函数传参。为什么要有这样一个传参呢首先在go中函数是一等公民其地位和其他数据类型一样都可以做为函数的参数。这个特点使我们的封装更方便。因为我需要把数据库的操作封装到我的方法中以达到约束的目的。关于函数式编程我在另一篇文章中《golang函数式编程》有写过不过我尚有部分原理还没有搞清楚还需要找时间继续探究。
函数一等公民这个特点似乎很好理解但是进一步思考我们可能会想到数据库操作入参不是固定的啊这个要怎么处理呢很好的问题。事实上我们可以利用闭包的特点把这些不是固定的入参传到函数内部。
基于redis实现缓存的想法
主要就是考虑缓存雪崩缓存穿透等问题其中缓存雪崩和缓存穿透的设计参考了go-zero项目中的设计我在go-zero设计思想的基础上进行了封装。
package cacheimport (contextencoding/jsonerrorsfmttimegithub.com/redis/go-redis/v9github.com/zeromicro/go-zero/core/mathxgithub.com/zeromicro/go-zero/core/syncxgorm.io/gorm/logger
)const (notFoundPlaceholder * //数据库没有查询到记录时缓存值设置为*避免缓存穿透// make the expiry unstable to avoid lots of cached items expire at the same time// make the unstable expiry to be [0.95, 1.05] * secondsexpiryDeviation 0.05
)// indicates there is no such value associate with the key
var errPlaceholder errors.New(placeholder)
var ErrNotFound errors.New(not found)// ErrRecordNotFound record not found error
var ErrRecordNotFound errors.New(record not found) //数据库没有查询到记录时返回该错误type RedisCache struct {rds *redis.Clientexpiry time.Duration //缓存失效时间notFoundExpiry time.Duration //数据库没有查询到记录时缓存失效时间logger logger.Interfacebarrier syncx.SingleFlight //允许具有相同键的并发调用共享调用结果unstableExpiry mathx.Unstable //避免缓存雪崩失效时间随机值
}func NewRedisCache(rds *redis.Client, log logger.Interface, barrier syncx.SingleFlight, opts ...Option) *RedisCache {if log nil {log logger.Default.LogMode(logger.Info)}o : newOptions(opts...)return RedisCache{rds: rds,expiry: o.Expiry,notFoundExpiry: o.NotFoundExpiry,logger: log,barrier: barrier,unstableExpiry: mathx.NewUnstable(expiryDeviation),}
}func (r *RedisCache) DelCtx(ctx context.Context, query func() error, keys ...string) error {if err : query(); err ! nil {r.logger.Error(ctx, fmt.Sprintf(Failed to query: %v, err))return err}for _, key : range keys {if err : r.rds.Del(ctx, key).Err(); err ! nil {r.logger.Error(ctx, fmt.Sprintf(Failed to delete key %s: %v, key, err))//TODO 起个定时任务异步重试}}return nil
}func (r *RedisCache) TakeCtx(ctx context.Context, key string, query func() (interface{}, error)) ([]byte, error) {return r.TakeWithExpireCtx(ctx, key, r.expiry, query)
}func (r *RedisCache) TakeWithExpireCtx(ctx context.Context, key string, expire time.Duration, query func() (interface{}, error)) ([]byte, error) {// 在过期时间的基础上增加一个随机值避免缓存雪崩expire r.aroundDuration(expire)// 并发控制同一个key的请求只有一个请求执行其他请求等待共享结果res, err : r.barrier.Do(key, func() (interface{}, error) {cacheVal, err : r.doGetCache(ctx, key)if err ! nil {// 如果缓存中查到的是notfound的占位符直接返回if errors.Is(err, errPlaceholder) {return nil, ErrNotFound} else if !errors.Is(err, ErrNotFound) {return nil, err}}// 缓存中存在值直接返回if len(cacheVal) 0 {return cacheVal, nil}data, err : query()if errors.Is(err, ErrRecordNotFound) {//数据库中不存在该值则将占位符缓存到redisif err : r.setCacheWithNotFound(ctx, key); err ! nil {r.logger.Error(ctx, fmt.Sprintf(Failed to set not found key %s: %v, key, err))}return nil, ErrNotFound} else if err ! nil {return nil, err}cacheVal, err json.Marshal(data)if err ! nil {return nil, err}if err : r.rds.Set(ctx, key, cacheVal, expire).Err(); err ! nil {r.logger.Error(ctx, fmt.Sprintf(Failed to set key %s: %v, key, err))return nil, err}return cacheVal, nil})if err ! nil {return []byte{}, err}//断言为[]byteval, ok : res.([]byte)if !ok {return []byte{}, fmt.Errorf(failed to convert value to bytes)}return val, nil
}func (r *RedisCache) aroundDuration(duration time.Duration) time.Duration {return r.unstableExpiry.AroundDuration(duration)
}// 获取缓存
func (r *RedisCache) doGetCache(ctx context.Context, key string) ([]byte, error) {val, err : r.rds.Get(ctx, key).Bytes()if err ! nil {if err redis.Nil {return nil, ErrNotFound}return nil, err}if len(val) 0 {return nil, ErrNotFound}// 如果缓存的值为notfound的占位符则表示数据库中不存在该值避免再次查询数据库避免缓存穿透if string(val) notFoundPlaceholder {return nil, errPlaceholder}return val, nil
}// 数据库没有查询到值则设置占位符避免缓存穿透
func (r *RedisCache) setCacheWithNotFound(ctx context.Context, key string) error {notFoundExpiry : r.aroundDuration(r.notFoundExpiry)if err : r.rds.Set(ctx, key, notFoundPlaceholder, notFoundExpiry).Err(); err ! nil {r.logger.Error(ctx, fmt.Sprintf(Failed to set not found key %s: %v, key, err))return err}return nil
}package cacheimport timeconst (defaultExpiry time.Hour * 24 * 7defaultNotFoundExpiry time.Minute
)type (// Options is used to store the cache options.Options struct {Expiry time.DurationNotFoundExpiry time.Duration}// Option defines the method to customize an Options.Option func(o *Options)
)func newOptions(opts ...Option) Options {var o Optionsfor _, opt : range opts {opt(o)}if o.Expiry 0 {o.Expiry defaultExpiry}if o.NotFoundExpiry 0 {o.NotFoundExpiry defaultNotFoundExpiry}return o
}// WithExpiry returns a func to customize an Options with given expiry.
func WithExpiry(expiry time.Duration) Option {return func(o *Options) {o.Expiry expiry}
}// WithNotFoundExpiry returns a func to customize an Options with given not found expiry.
func WithNotFoundExpiry(expiry time.Duration) Option {return func(o *Options) {o.NotFoundExpiry expiry}
}最后附上部分测试用例数据库操作的逻辑我没有写通过模拟的方式实现。
package cacheimport (contexttestinggithub.com/redis/go-redis/v9github.com/zeromicro/go-zero/core/syncxgorm.io/gorm/logger
)func TestRedisCache(t *testing.T) {rdb : redis.NewClient(redis.Options{Addr: , // Redis地址Password: , // 密码无密码则为空DB: 11, // 使用默认DB})ctx : context.Background()rc : NewRedisCache(rdb, logger.Default.LogMode(logger.Info), syncx.NewSingleFlight())// 测试 TakeCtx 方法key : testKeyqueryVal : hello, world// 通过闭包的方式模拟查询数据库的操作query : func() (interface{}, error) {return queryVal, nil}val, err : rc.TakeCtx(ctx, key, query)if err ! nil {t.Fatalf(unexpected error: %v, err)}t.Log(return query func val:, string(val))// 再次调用 TakeCtx 方法应该返回缓存的值queryVal this should not be returnedval, err rc.TakeCtx(ctx, key, query)if err ! nil {t.Fatalf(unexpected error: %v, err)}t.Log(cache val:, string(val))// 测试 DelCtx 方法if err : rc.DelCtx(ctx, func() error {t.Log(mock query before delete)return nil}, key); err ! nil {t.Fatalf(unexpected error: %v, err)}queryVal this should be cached// 验证键是否已被删除val, err rc.TakeCtx(ctx, key, query)if err ! nil {t.Fatalf(unexpected error: %v, err)}if string(val) ! this should be cached {t.Fatalf(unexpected value: %s, string(val))}
} 这篇文章就写到这里结束了。水平有限有写的不对的地方还望广大网友斧正不胜感激。