diff --git a/bigcache.go b/bigcache.go index 5620c0ef..ecfe81e2 100644 --- a/bigcache.go +++ b/bigcache.go @@ -137,6 +137,59 @@ func (c *BigCache) Get(key string) ([]byte, error) { return shard.get(key, hashedKey) } +// Used to store information about keys in GetMulti function +// order is the index in the slice the data should go +// hashedKey is the Sum64 hash of the key +// key is the original key input +type keyInfo struct { + order int + hashedKey uint64 + key string +} + +// GetMulti reads entry for each of the keys. +// returns entries in the same order as the provided keys. +// if entry is not found for a given key, the index will contain nil +func (c *BigCache) GetMulti(keys []string) [][]byte { + shards := make(map[uint64][]keyInfo, len(c.shards)) + entries := make([][]byte, len(keys)) + + for i, key := range keys { + hashedKey := c.hash.Sum64(key) + shardIndex := hashedKey & c.shardMask + shards[shardIndex] = append(shards[shardIndex], keyInfo{order: i, hashedKey: hashedKey, key: key}) + } + + for shardKey, keyInfos := range shards { + hits := make([]uint64, 0, len(keyInfos)) + shard := c.shards[shardKey] + shard.lock.RLock() + for i := range keyInfos { + entry, _ := shard.getWithoutLock(keyInfos[i].key, keyInfos[i].hashedKey) + + if entry != nil { + hits = append(hits, keyInfos[i].hashedKey) + } + + entries[keyInfos[i].order] = entry + } + shard.lock.RUnlock() + + if shard.statsEnabled{ + shard.lock.Lock() + } + + for i := range hits { + shard.hitWithoutLock(hits[i]) + } + + if shard.statsEnabled{ + shard.lock.Unlock() + } + } + return entries +} + // GetWithInfo reads entry for the key with Response info. // It returns an ErrEntryNotFound when // no entry exists for the given key. diff --git a/bigcache_bench_test.go b/bigcache_bench_test.go index b6d044e5..a4be6477 100644 --- a/bigcache_bench_test.go +++ b/bigcache_bench_test.go @@ -69,6 +69,88 @@ func BenchmarkReadFromCache(b *testing.B) { } } +func BenchmarkReadFromCacheManySingle(b *testing.B) { + for _, shards := range []int{1, 512, 1024, 8192} { + b.Run(fmt.Sprintf("%d-shards", shards), func(b *testing.B) { + cache, _ := New(context.Background(), Config{ + Shards: shards, + LifeWindow: 1000 * time.Second, + MaxEntriesInWindow: max(b.N, 100), + MaxEntrySize: 500, + }) + + keys := make([]string, b.N) + for i := 0; i < b.N; i++ { + keys[i] = fmt.Sprintf("key-%d", i) + cache.Set(keys[i], message) + } + + b.ReportAllocs() + b.ResetTimer() + for _, key := range keys { + cache.Get(key) + } + + }) + } +} + +func BenchmarkReadFromCacheManyMulti(b *testing.B) { + for _, shards := range []int{1, 512, 1024, 8192} { + b.Run(fmt.Sprintf("%d-shards", shards), func(b *testing.B) { + cache, _ := New(context.Background(), Config{ + Shards: shards, + LifeWindow: 1000 * time.Second, + MaxEntriesInWindow: max(b.N, 100), + MaxEntrySize: 500, + }) + keys := make([]string, b.N) + for i := 0; i < b.N; i++ { + keys[i] = fmt.Sprintf("key-%d", i) + cache.Set(keys[i], message) + } + + b.ReportAllocs() + b.ResetTimer() + cache.GetMulti(keys) + }) + } +} + +func BenchmarkReadFromCacheManyMultiBatches(b *testing.B) { + for _, shards := range []int{1, 512, 1024, 8192} { + for _, batchSize := range []int{1, 5, 10, 100} { + b.Run(fmt.Sprintf("%d-shards %d-batchSize", shards, batchSize), func(b *testing.B) { + cache, _ := New(context.Background(), Config{ + Shards: shards, + LifeWindow: 1000 * time.Second, + MaxEntriesInWindow: max(b.N, 100), + MaxEntrySize: 500, + }) + keys := make([]string, b.N) + for i := 0; i < b.N; i++ { + keys[i] = fmt.Sprintf("key-%d", i) + cache.Set(keys[i], message) + } + + batches := make([][]string, 0, (len(keys)+batchSize-1)/batchSize) + + for batchSize < len(keys) { + keys, batches = keys[batchSize:], append(batches, keys[0:batchSize:batchSize]) + } + batches = append(batches, keys) + + b.ReportAllocs() + b.ResetTimer() + for _, b := range batches { + cache.GetMulti(b) + + } + }) + } + } +} + func BenchmarkReadFromCacheWithInfo(b *testing.B) { for _, shards := range []int{1, 512, 1024, 8192} { b.Run(fmt.Sprintf("%d-shards", shards), func(b *testing.B) { diff --git a/bigcache_test.go b/bigcache_test.go index 43ec3f57..4abfd7f8 100644 --- a/bigcache_test.go +++ b/bigcache_test.go @@ -29,6 +29,53 @@ func TestWriteAndGetOnCache(t *testing.T) { assertEqual(t, value, cachedValue) } +func TestWriteAndGetOnCacheMulti(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + keys []string + data [][]byte + want string + }{ + { + keys: []string{"k1", "k2", "k3", "k4", "k5"}, + data: [][]byte{ + blob('a', 10), + blob('b', 10), + blob('c', 10), + blob('d', 10), + blob('e', 10), + }, + want: "Get all values ordered", + }, + { + keys: []string{"k1", "k2", "k3", "k4", "k5"}, + data: [][]byte{ + blob('a', 10), + blob('b', 10), + nil, + blob('d', 10), + blob('e', 10), + }, + want: "Get all values ordered with nil", + }, + } { + t.Run(tc.want, func(t *testing.T) { + cache, _ := New(context.Background(), DefaultConfig(5*time.Second)) + + for i := range tc.keys { + if tc.data[i] != nil { + cache.Set(tc.keys[i], tc.data[i]) + } + } + + cachedValues := cache.GetMulti(tc.keys) + + assertEqual(t, tc.data, cachedValues) + }) + + } +} + func TestAppendAndGetOnCache(t *testing.T) { t.Parallel() @@ -836,6 +883,46 @@ func TestWriteAndReadParallelSameKeyWithStats(t *testing.T) { assertEqual(t, ntest*n, int(cache.KeyMetadata(key).RequestCount)) } +func TestWriteAndReadManyParallelSameKeyWithStats(t *testing.T) { + t.Parallel() + + c := DefaultConfig(0) + c.StatsEnabled = true + + cache, _ := New(context.Background(), c) + var wg sync.WaitGroup + ntest := 100 + n := 10 + wg.Add(n) + + keys := []string{"key1", "key2", "key3"} + values := [][]byte{blob('a', 64), blob('b', 64), blob('c', 64)} + + for i := 0; i < ntest; i++ { + for j := range keys { + assertEqual(t, nil, cache.Set(keys[j], values[j])) + } + } + + for j := 0; j < n; j++ { + go func() { + for i := 0; i < ntest; i++ { + v := cache.GetMulti(keys) + assertEqual(t, values, v) + } + wg.Done() + }() + } + + wg.Wait() + + assertEqual(t, Stats{Hits: int64(n * ntest * len(keys))}, cache.Stats()) + + for i := range keys { + assertEqual(t, ntest*n, int(cache.KeyMetadata(keys[i]).RequestCount)) + } +} + func TestCacheReset(t *testing.T) { t.Parallel() diff --git a/shard.go b/shard.go index 4f03b53e..4cc3adb3 100644 --- a/shard.go +++ b/shard.go @@ -61,22 +61,32 @@ func (s *cacheShard) getWithInfo(key string, hashedKey uint64) (entry []byte, re func (s *cacheShard) get(key string, hashedKey uint64) ([]byte, error) { s.lock.RLock() + entry, err := s.getWithoutLock(key, hashedKey) + s.lock.RUnlock() + + if err != nil { + return nil, err + } + + s.hit(hashedKey) + return entry, nil +} + +func (s *cacheShard) getWithoutLock(key string, hashedKey uint64) ([]byte, error) { wrappedEntry, err := s.getWrappedEntry(hashedKey) if err != nil { - s.lock.RUnlock() return nil, err } + if entryKey := readKeyFromEntry(wrappedEntry); key != entryKey { - s.lock.RUnlock() s.collision() if s.isVerbose { s.logger.Printf("Collision detected. Both %q and %q have the same hash %x", key, entryKey, hashedKey) } return nil, ErrEntryNotFound } + entry := readEntry(wrappedEntry) - s.lock.RUnlock() - s.hit(hashedKey) return entry, nil }