import (
"fmt"
"github.com/zeromicro/go-zero/core/stores/redis"
)
func test() {
//1.获取redis连接
redisObj := redis.New("addr")
//2.设置数据
key := "abc"
err := redisObj.Set(key, "123erf")
if err != nil {
fmt.Printf("set fail:%v\n", err)
}
//3.获取数据
res, err := redisObj.Get(key)
if err != nil {
fmt.Printf("get fail:%v\n", err)
}
fmt.Printf("result:%s", res)
}
goctl model mysql datasource -url="root:root@tcp(127.0.0.1:3306)/demo" -table="t_user" -dir="./model" -c -style goZero
- 缓存增删改,自动失效,可以指定过期时间
- 缓存大小限制,可以指定缓存个数,有LRU算法
- 缓存命中率统计
- 并发安全,解决缓存击穿问题
import (
"github.com/zeromicro/go-zero/core/collection"
"time"
)
func test2() {
// 初始化 cache,其中 WithLimit 可以指定最大缓存的数量
c, err := collection.NewCache(time.Minute, collection.WithLimit(10000))
if err != nil {
panic(err)
}
// 设置缓存
c.Set("key", "aaaa")
// 获取缓存,ok:是否存在
v, ok := c.Get("key")
// 删除缓存
c.Del("key")
// 获取缓存,如果 key 不存在的,则会调用 func 去生成缓存
v, err := c.Take("key", func() (interface{}, error) {
return "vvvv", nil
})
}
func NewCache(expire time.Duration, opts ...CacheOption) (*Cache, error) {
cache := &Cache{//声明缓存结构,初始化内容
data: make(map[string]interface{}),
expire: expire,
lruCache: emptyLruCache, //默认是一个空的LRU结构,可以通过opts来控制
barrier: syncx.NewSingleFlight(),//解决缓存击穿的核心方法
unstableExpiry: mathx.NewUnstable(expiryDeviation),//框架自己做的一个并发安全的随机数
}
for _, opt := range opts {
opt(cache) //执行预加载函数
}
...
cache.stats = newCacheStat(cache.name, cache.size)//缓存命中统计模块,初始化
timingWheel, err := NewTimingWheel(time.Second, slots, func(k, v interface{}) {
key, ok := k.(string)
if !ok {
return
}
cache.Del(key)
})//定时器模块,初始化
...
return cache, nil
}
- lruCache: Cache有大小限制, 通过LRU淘汰策略管理大小的
- barrier: 用来解决缓存击穿的核心方法
- unstableExpiry: 框架自己做的一个并发安全的随机数
type (
CacheOption func(cache *Cache) //用来操作缓存的函数,用在实例化缓存时
Cache struct {
name string //缓存名称
lock sync.Mutex //并发锁
data map[string]interface{} //缓存内容
expire time.Duration //过期时间
timingWheel *TimingWheel //框架封装的定时器
lruCache lru //LRU组件
barrier syncx.SingleFlight //缓存并发安全组件,可以解决缓存击穿的问题
unstableExpiry mathx.Unstable //生成随机数的插件
stats *cacheStat //统计命中率模块
}
)
这一块代码大同小异,都是加锁,操作,再解锁的方式来进行并发管理的。通过这一块代码的阅读,基本可以明确LRU模块和命中率统计模块是如何工作的
func (c *Cache) Del(key string) {
c.lock.Lock() //上锁
delete(c.data, key) // 删除元素
c.lruCache.remove(key) //移除LRU
c.lock.Unlock() // 解锁
c.timingWheel.RemoveTimer(key) //移除定时器, 注意先解锁,后移除。
}
func (c *Cache) Get(key string) (interface{}, bool) {
...
if ok { //统计命中率
c.stats.IncrementHit()
} else {
c.stats.IncrementMiss()
}
...
}
func (c *Cache) Set(key string, value interface{}) {
c.lock.Lock() //上锁
_, ok := c.data[key] //判断KEY是否存在
c.data[key] = value //赋值
c.lruCache.add(key) //添加到LRU
c.lock.Unlock() //解锁
expiry := c.unstableExpiry.AroundDuration(c.expire) //设置过期值
if ok {
c.timingWheel.MoveTimer(key, expiry)
} else {
c.timingWheel.SetTimer(key, value, expiry)
}
}
func (c *Cache) doGet(key string) (interface{}, bool) {
...
value, ok := c.data[key]
if ok {
c.lruCache.add(key) //添加到LRU中
}
...
}
func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}, error) {
if val, ok := c.doGet(key); ok {//直接获取KEY的值
c.stats.IncrementHit() //记录命中
return val, nil
}
var fresh bool
//核心方法,主要是用了syncx.NewSharedCalls实现的功能
val, err := c.barrier.Do(key, func() (interface{}, error) {
//这里进行了一次dobble check。解决并发时,有些协程可能已经把数据查出来并加载到缓存了。
if val, ok := c.doGet(key); ok {
return val, nil
}
v, e := fetch()//执行方法,获取CACHE。这个方法应该尽量的保证效率
if e != nil {
return nil, e
}
fresh = true
c.Set(key, v) //设置缓存
return v, nil
})
...
if fresh {
//fetch 获取到数据为空,记录miss次数
c.stats.IncrementMiss()
return val, nil
}
// 直接把之前查到的数据返回,并记录命中次数
c.stats.IncrementHit()
return val, nil
}
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 当链表满的时候,将链表尾部的数据丢弃
type (
lru interface { //声明一个接口
add(key string)
remove(key string)
}
emptyLru struct{} //默认的空结构,newCache的时候默认用的就这个
keyLru struct {//带有限制的结构,ewCache的时候通过调用withLimit方法设置
limit int //总长度
evicts *list.List //元素链
//存放的是元素在链表中的地址。利用MAP操作O1的特性,不用遍历链表即可找到需要的元素的地址。
elements map[string]*list.Element
onEvict func(key string) //删除的后置操作
}
)
//使用时调用该方法初始化keyLru
func newKeyLru(limit int, onEvict func(key string)) *keyLru {
...
}
func (klru *keyLru) add(key string) {
if elem, ok := klru.elements[key]; ok {
klru.evicts.MoveToFront(elem)//如果新增元素已存在,就直接移到最前面
return
}
elem := klru.evicts.PushFront(key)//在链表最前面增加一个元素
klru.elements[key] = elem //记录这个元素的地址
if klru.evicts.Len() > klru.limit {
klru.removeOldest()//如果链表的最大长度超过配置,移除最老的元素
}
}
func (klru *keyLru) remove(key string) {
...
}
func (klru *keyLru) removeOldest() {
elem := klru.evicts.Back()//取链表最后一个元素
if elem != nil {
klru.removeElement(elem) //移除元素
}
}
func (klru *keyLru) removeElement(e *list.Element) {
klru.evicts.Remove(e)//移除链表中的元素
key := e.Value.(string)//获取Key
delete(klru.elements, key)//移除MAP中的元素
klru.onEvict(key) //执行删除的后置操作
}
type cacheStat struct {
name string //名称,最后打印日志记录是要用到
hit uint64 //命中缓存次数
miss uint64 //未命中次数
sizeCallback func() int //自定义回调函数,会在打印结果的时候用到
}
func newCacheStat(name string, sizeCallback func() int) *cacheStat {...}
func (cs *cacheStat) IncrementHit() {
atomic.AddUint64(&cs.hit, 1) //记录命中次数
}
func (cs *cacheStat) IncrementMiss() {
atomic.AddUint64(&cs.miss, 1)
}
func (cs *cacheStat) statLoop() {
...
}
func main() {
const round = 5
var wg sync.WaitGroup
barrier := syncx.NewSharedCalls()
wg.Add(round)
for i := 0; i < round; i++ {
// 多个线程同时执行
go func() {
defer wg.Done()
// 可以看到,多个线程在同一个key上去请求资源,获取资源的实际函数只会被调用一次
val, err := barrier.Do("once", func() (interface{}, error) {
// sleep 1秒,为了让多个线程同时取once这个key上的数据
time.Sleep(time.Second)
// 生成了一个随机的id
return stringx.RandId(), nil
})
if err != nil {
fmt.Println(err)
} else {
fmt.Println(val)
}
}()
}
wg.Wait()
}
import (
"fmt"
"github.com/zeromicro/go-zero/core/stringx"
"github.com/zeromicro/go-zero/core/syncx"
"sync"
"time"
)
syncx.NewSingleFlight()
// SharedCalls接口提供了Do和DoEx两种方法
type SharedCalls interface {
Do(key string, fn func() (interface{}, error)) (interface{}, error)
DoEx(key string, fn func() (interface{}, error)) (interface{}, bool, error)
}
// call代表对指定资源的一次请求
type call struct {
wg sync.WaitGroup // 用于协调各个请求goroutine之间的资源共享
val interface{} // 用于保存请求的返回值
err error // 用于保存请求过程中发生的错误
}
type sharedGroup struct {
calls map[string]*call
lock sync.Mutex
}
key参数:可以理解为资源的唯一标识。
fn参数:真正获取资源的方法
// 当多个请求同时使用Do方法请求资源时
func (g *sharedGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
// 先申请加锁
g.lock.Lock()
// 根据key,获取对应的call结果,并用变量c保存
if c, ok := g.calls[key]; ok {
// 拿到call以后,释放锁,此处call可能还没有实际数据,只是一个空的内存占位
g.lock.Unlock()
// 调用wg.Wait,判断是否有其他goroutine正在申请资源,如果阻塞,说明有其他goroutine正在获取资源
c.wg.Wait()
// 当wg.Wait不再阻塞,表示资源获取已经结束,可以直接返回结果
return c.val, c.err
}
// 没有拿到结果,则调用makeCall方法去获取资源,注意此处仍然是锁住的,可以保证只有一个goroutine可以调用makecall
c := g.makeCall(key, fn)
// 返回调用结果
return c.val, c.err
}
func (g *sharedGroup) DoEx(key string, fn func() (interface{}, error)) (val interface{}, fresh bool, err error) {
g.lock.Lock()
if c, ok := g.calls[key]; ok {
g.lock.Unlock()
c.wg.Wait()
return c.val, false, c.err
}
c := g.makeCall(key, fn)
return c.val, true, c.err
}
// 进入makeCall的一定只有一个goroutine,因为要拿锁锁住的
func (g *sharedGroup) makeCall(key string, fn func() (interface{}, error)) *call {
// 创建call结构,用于保存本次请求的结果
c := new(call)
// wg加1,用于通知其他请求资源的goroutine等待本次资源获取的结束
c.wg.Add(1)
// 将用于保存结果的call放入map中,以供其他goroutine获取
g.calls[key] = c
// 释放锁,这样其他请求的goroutine才能获取call的内存占位
g.lock.Unlock()
defer func() {
// delete key first, done later. can't reverse the order, because if reverse,
// another Do call might wg.Wait() without get notified with wg.Done()
g.lock.Lock()
delete(g.calls, key)
g.lock.Unlock()
// 调用wg.Done,通知其他goroutine可以返回结果,这样本批次所有请求完成结果的共享
c.wg.Done()
}()
// 调用fn方法,将结果填入变量c中
c.val, c.err = fn()
return c
}
import (
"fmt"
"github.com/zeromicro/go-zero/core/stores/redis"
)
func test3() {
//1.获取redis连接
store := redis.New("")
//2.拼接一个key
redisLockKey := fmt.Sprintf("%v%v", "key", "")
//3.初始化redislock
redisLock := redis.NewRedisLock(store, redisLockKey)
//4.可选操作,设置 redislock 过期时间
redisLock.SetExpire(10)
if ok, err := redisLock.Acquire(); !ok || err != nil {
fmt.Println("为获取到锁")
return
}
defer func() {
recover()
// 3. 释放锁
redisLock.Release()
}()
}
- ex seconds :设置key过期时间,单位s
- px milliseconds :设置key过期时间,单位毫秒
- nx:key不存在时,设置key的值
- xx:key存在时,才会去设置key的值
func NewRedisLock(store *Redis, key string) *RedisLock {
return &RedisLock{
store: store,
key: key,
id: stringx.Randn(randomLen),
}
}
type RedisLock struct {
store *Redis
seconds uint32
key string
id string
}
- Lua 脚本保证原子性「当然,把多个操作在 Redis 中实现成一个操作,也就是单命令操作」
- 使用了 set key value px milliseconds nx
- value 具有唯一性
- 加锁时首先判断 key 的 value 是否和之前设置的一致,一致则修改过期时间
const lockCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
return "OK"
else
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end`
func (rl *RedisLock) Acquire() (bool, error) {
return rl.AcquireCtx(context.Background())
}
func (rl *RedisLock) AcquireCtx(ctx context.Context) (bool, error) {
seconds := atomic.LoadUint32(&rl.seconds)
resp, err := rl.store.EvalCtx(ctx, lockCommand, []string{rl.key}, []string{
rl.id, strconv.Itoa(int(seconds)*millisPerSecond + tolerance),
})
if err == red.Nil {
return false, nil
} else if err != nil {
logx.Errorf("Error on acquiring lock for %s, %s", rl.key, err.Error())
return false, err
} else if resp == nil {
return false, nil
}
reply, ok := resp.(string)
if ok && reply == "OK" {
return true, nil
}
logx.Errorf("Unknown reply when acquiring lock for %s: %v", rl.key, resp)
return false, nil
}
const delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end`
func (rl *RedisLock) Release() (bool, error) {
return rl.ReleaseCtx(context.Background())
}
func (rl *RedisLock) ReleaseCtx(ctx context.Context) (bool, error) {
resp, err := rl.store.EvalCtx(ctx, delCommand, []string{rl.key}, []string{rl.id})
if err != nil {
return false, err
}
reply, ok := resp.(int64)
if !ok {
return false, nil
}
return reply == 1, nil
}
- 布隆过滤器可以看成一个很长的初始值都为0的,二进制数组+一系列随机hash算法映射函,类似于set的数据结构,统计结果不太准确(在统计存在时)
- 在添加key时: 使用多个hash函数,对key进行hash运算得到一个整数索引,然后与位数组长度进行取模运算拿到位置,每个hash函数都会得到不同的位置,将这几个位置设置为1,完成了一次add操作
- 查询key时,只要通过key进行hash运算拿到整数索引后与位数组长度进行取模运算,拿到位数组的位置,只要一次位数组中指定的位置值为0,就说明key不存在
//表示经过多少散列函数计算
//固定14次
maps = 14
type (
// 定义布隆过滤器结构体
Filter struct {
bits uint
bitSet bitSetProvider
}
//位数组操作接口定义
bitSetProvider interface {
check([]uint) (bool, error)
set([]uint) error
}
)
func New(store *redis.Redis, key string, bits uint) *Filter {
return &Filter{
bits: bits,
bitSet: newRedisBitSet(store, key, bits),
}
}
func (f *Filter) Add(data []byte) error {
// 获取数据多次hash后的各key
locations := f.getLocations(data)
// 插入数据
return f.bitSet.set(locations)
}
func (f *Filter) Exists(data []byte) (bool, error) {
// 同数据set一致,获取数据多次hash后,偏移量切片
locations := f.getLocations(data)
// 调用check方法进行检测
isSet, err := f.bitSet.check(locations)
if err != nil {
return false, err
}
return isSet, nil
}
func newRedisBitSet(store *redis.Redis, key string, bits uint) *redisBitSet {
return &redisBitSet{
store: store,
key: key,
bits: bits,
}
}
//redis位数组
type redisBitSet struct {
store *redis.Client
key string
bits uint
}
//k次散列计算出k个offset
func (f *Filter) getLocations(data []byte) []uint {
//创建指定容量的切片
locations := make([]uint, maps)
//maps表示k值,作者定义为了常量:14
for i := uint(0); i < maps; i++ {
//哈希计算,使用的是"MurmurHash3"算法,并每次追加一个固定的i字节进行计算
hashValue := hash.Hash(append(data, byte(i)))
//取下标offset
locations[i] = uint(hashValue % uint64(f.bits))
}
return locations
}
//检查偏移量offset数组是否全部为1
//是:元素可能存在
//否:元素一定不存在
func (r *redisBitSet) check(offsets []uint) (bool, error) {
args, err := r.buildOffsetArgs(offsets)
if err != nil {
return false, err
}
//执行脚本
resp, err := r.store.Eval(testScript, []string{r.key}, args)
//这里需要注意一下,底层使用的go-redis
//redis.Nil表示key不存在的情况需特殊判断
if err == redis.Nil {
return false, nil
} else if err != nil {
return false, err
}
exists, ok := resp.(int64)
if !ok {
return false, nil
}
return exists == 1, nil
}
//将k位点全部设置为1
func (r *redisBitSet) set(offsets []uint) error {
args, err := r.buildOffsetArgs(offsets)
if err != nil {
return err
}
_, err = r.store.Eval(setScript, []string{r.key}, args)
//底层使用的是go-redis,redis.Nil表示操作的key不存在
//需要针对key不存在的情况特殊判断
if err == redis.Nil {
return nil
} else if err != nil {
return err
}
return nil
}
//构建偏移量offset字符串数组,因为go-redis执行lua脚本时参数定义为[]stringy
//因此需要转换一下
func (r *redisBitSet) buildOffsetArgs(offsets []uint) ([]string, error) {
var args []string
for _, offset := range offsets {
if offset >= r.bits {
return nil, ErrTooLargeOffset
}
args = append(args, strconv.FormatUint(uint64(offset), 10))
}
return args, nil
}
//删除
func (r *redisBitSet) del() error {
_, err := r.store.Del(r.key)
return err
}
//自动过期
func (r *redisBitSet) expire(seconds int) error {
return r.store.Expire(r.key, seconds)
}