Compare commits
No commits in common. "main" and "v5.0.0" have entirely different histories.
235
README.md
235
README.md
|
@ -1,235 +0,0 @@
|
||||||
# Candy Cache
|
|
||||||
|
|
||||||
**CandyCache** — это простой и эффективный кэш на языке Go, который позволяет хранить данные с ограниченным временем жизни (**TTL**).
|
|
||||||
|
|
||||||
# Установка
|
|
||||||
|
|
||||||
Для использования CandyCache в вашем проекте, установите его, используя ```go get git.hikan.ru/serr/candycache```, далее просто добавьте ```git.hikan.ru/serr/candycache``` в блок импорта.
|
|
||||||
|
|
||||||
# Основные возможности
|
|
||||||
|
|
||||||
- Автоматическая очистка устаревших элементов и возможность ее отключения.
|
|
||||||
- Кэшем можно управлять вручную.
|
|
||||||
- Конкурентный доступ к данным возможен благодаря мьютексам.
|
|
||||||
- Кэш может хранить данные любых типов.
|
|
||||||
- Можно создавать и загружать дампы.
|
|
||||||
|
|
||||||
# Использование
|
|
||||||
|
|
||||||
## Создание кэша
|
|
||||||
|
|
||||||
### С автоматической очисткой
|
|
||||||
|
|
||||||
Для создания нового экземпляра кэша используйте функцию **Cacher**, передавая интервал очистки в наносекундах.
|
|
||||||
|
|
||||||
Если требуется указать интервал в секундах/минутах/часах и т.д. - используйте множители из пакета **time**:
|
|
||||||
```go
|
|
||||||
cache := candycache.Cacher(10 * time.Minute) // Очистка каждые 10 минут
|
|
||||||
```
|
|
||||||
|
|
||||||
### Без автоматической очистки
|
|
||||||
|
|
||||||
Если автоматичская очистка не нужна - просто передайте параметром любое отрицательное число:
|
|
||||||
|
|
||||||
```go
|
|
||||||
cache := candycache.Cacher(-1) // Кэш не будет очищаться автоматически
|
|
||||||
```
|
|
||||||
|
|
||||||
## Добавление элемента
|
|
||||||
|
|
||||||
Для добавления элемента в кэш используйте метод **Set**:
|
|
||||||
```go
|
|
||||||
cache.Set("key", "value", 5 * time.Minute) // Элемент будет считаться устаревшим через 5 минут
|
|
||||||
```
|
|
||||||
В случае, если по указанном ключу уже что-то хранится, оно будет заменено на новый элемент.
|
|
||||||
|
|
||||||
## Получение элемента
|
|
||||||
|
|
||||||
Для получения элемента из кэша используйте метод **Get**:
|
|
||||||
|
|
||||||
```go
|
|
||||||
value, err := cache.Get("key") // Получение значения по ключу "key"
|
|
||||||
```
|
|
||||||
Если элемент найден, то в переменную **value** будет записано значение, а в **err** - **nil**. Если элемент не найден, то в **err** будет записано **key not found**, а значением вернется **nil**.
|
|
||||||
|
|
||||||
## Удаление элемента
|
|
||||||
|
|
||||||
Для удаления элемента по ключу используйте метод **Delete**:
|
|
||||||
|
|
||||||
```go
|
|
||||||
err := cache.Delete("key1")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Ошибка:", err) // Не найдено записи по ключу
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Элемент будет удален, не смотря на то, устаревший он или нет.
|
|
||||||
|
|
||||||
## Массовое удаление элементов
|
|
||||||
|
|
||||||
### Удаление устаревших элементов
|
|
||||||
|
|
||||||
Для удаления устаревших элементов используйте метод **Cleanup**:
|
|
||||||
|
|
||||||
```go
|
|
||||||
cache.Cleanup() // Перебирает все элементы кэша, удаляет устаревшие
|
|
||||||
```
|
|
||||||
|
|
||||||
### Удаление всех элементов кэша
|
|
||||||
|
|
||||||
Для полной очистки кэша используйте метод **Flush**:
|
|
||||||
|
|
||||||
```go
|
|
||||||
cache.Flush() // Удаляет все элементы кэша, не смотря на то, устаревшие они или нет
|
|
||||||
```
|
|
||||||
|
|
||||||
## Получение информации о кэше
|
|
||||||
|
|
||||||
### Получение списка элементов
|
|
||||||
|
|
||||||
Для получения списка всех элементов в кэше используйте метод **List**:
|
|
||||||
|
|
||||||
```go
|
|
||||||
items := cache.List() // Список всех элементов кэша
|
|
||||||
for _, item := range items {
|
|
||||||
fmt.Printf("Ключ: %s, Значение: %v, Момент устаревания: %d\n", item.Key, item.Item.Data(), item.Item.DestroyTimestamp())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Получить список устаревших элементов можно так
|
|
||||||
```go
|
|
||||||
items := ExpiredList()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Получение количества элементов
|
|
||||||
|
|
||||||
Для получения количества элементов в кэше используйте метод **Count**:
|
|
||||||
|
|
||||||
```go
|
|
||||||
count := cache.Count() // Количество элементов в кэше
|
|
||||||
```
|
|
||||||
|
|
||||||
### Получение размера кэша
|
|
||||||
|
|
||||||
Для получения размера всего кэша в байтах используйте метод **Size**:
|
|
||||||
|
|
||||||
```go
|
|
||||||
size := cache.Size() // Размер кэша в байтах
|
|
||||||
```
|
|
||||||
|
|
||||||
Данный метод возвращает корректное значение, если в кэше элементы представлены этими типами данных:
|
|
||||||
|
|
||||||
```go
|
|
||||||
bool
|
|
||||||
int, int8, int16, int32, int64
|
|
||||||
uint, uint8, uint16, uint32, uint64, uintptr
|
|
||||||
float32, float64
|
|
||||||
complex64, complex128
|
|
||||||
array, slice, string
|
|
||||||
map, struct, func
|
|
||||||
```
|
|
||||||
|
|
||||||
**И композициями этих типов**.
|
|
||||||
|
|
||||||
В противном случае значение может быть не точным.
|
|
||||||
|
|
||||||
## Работа с дампами
|
|
||||||
|
|
||||||
В модуле присутствуют методы **Save** и **Load**, позволяющие создавать и загружать дампы. Эти методы удовлетворяют интерфейсам **io.Writer** и **io.Reader** соответственно, т.е. их можно использовать и для работы с файлами, и для работы с буфферами.
|
|
||||||
|
|
||||||
### Сценарий 1
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Person struct {
|
|
||||||
Name string
|
|
||||||
Age int
|
|
||||||
Hobbies []string
|
|
||||||
}
|
|
||||||
|
|
||||||
cache := candycache.Cacher(10 * time.Minute) // Создаем кэш с интервалом очистки 10 минут
|
|
||||||
|
|
||||||
cache.Set("key1", "string", 5*time.Minute)
|
|
||||||
cache.Set("key2", 2, 10*time.Minute)
|
|
||||||
cache.Set("key7", -2.1231, 10*time.Minute)
|
|
||||||
cache.Set("key3", []string{"string1", "string2"}, 10*time.Minute)
|
|
||||||
cache.Set("key4", map[string]int{"a": 1, "b": 2}, 10*time.Minute)
|
|
||||||
cache.Set("key5", Person{Name: "Alice", Age: 30, Hobbies: []string{"reading", "swimming"}}, 10*time.Minute)
|
|
||||||
cache.Set("key6", []Person{
|
|
||||||
{Name: "Bob", Age: 25, Hobbies: []string{"coding", "gaming"}},
|
|
||||||
{Name: "Charlie", Age: 35, Hobbies: []string{"hiking", "photography"}},
|
|
||||||
}, 10*time.Minute)
|
|
||||||
|
|
||||||
file, err := os.Create("cache_dump.json")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("error creating file: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cache.Save(file); err != nil { // Сохранение кэша в файл
|
|
||||||
log.Fatal("error saving cache: ", err)
|
|
||||||
}
|
|
||||||
file.Close()
|
|
||||||
|
|
||||||
cache.Flush() // Удаление всех элементов из кэша
|
|
||||||
|
|
||||||
file, err = os.Open("cache_dump.json")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("error opening file: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cache.Load(file); err != nil { // Загрузка кэша из файла
|
|
||||||
fmt.Println("error load cache:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
list := cache.List() // Получаю список элементов кэша
|
|
||||||
|
|
||||||
for _, i := range list {
|
|
||||||
fmt.Println(i.Key, i.Item.Data(), i.Item.DestroyTimestamp())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Сценарий 2
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Person struct {
|
|
||||||
Name string
|
|
||||||
Age int
|
|
||||||
Hobbies []string
|
|
||||||
}
|
|
||||||
|
|
||||||
cache := candycache.Cacher(10 * time.Minute) // Создаем кэш с интервалом очистки 10 минут
|
|
||||||
|
|
||||||
cache.Set("key1", "string", 5*time.Minute)
|
|
||||||
cache.Set("key2", 2, 10*time.Minute)
|
|
||||||
cache.Set("key7", -2.1231, 10*time.Minute)
|
|
||||||
cache.Set("key3", []string{"string1", "string2"}, 10*time.Minute)
|
|
||||||
cache.Set("key4", map[string]int{"a": 1, "b": 2}, 10*time.Minute)
|
|
||||||
cache.Set("key5", Person{Name: "Alice", Age: 30, Hobbies: []string{"reading", "swimming"}}, 10*time.Minute)
|
|
||||||
cache.Set("key6", []Person{
|
|
||||||
{Name: "Bob", Age: 25, Hobbies: []string{"coding", "gaming"}},
|
|
||||||
{Name: "Charlie", Age: 35, Hobbies: []string{"hiking", "photography"}},
|
|
||||||
}, 10*time.Minute)
|
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
|
|
||||||
if err := cache.Save(&buffer); err != nil { // Сохранение бэкапа
|
|
||||||
log.Fatal("error saving cache: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.Set("key1", "lost", -1)
|
|
||||||
cache.Set("key2", "lost", -1)
|
|
||||||
cache.Set("key3", "lost", -1)
|
|
||||||
cache.Set("key4", "lost", -1)
|
|
||||||
cache.Set("key5", "lost", -1)
|
|
||||||
cache.Set("key6", "lost", -1)
|
|
||||||
cache.Set("key7", "lost", -1)
|
|
||||||
|
|
||||||
if err := cache.Load(&buffer); err != nil { // Восстановление бэкапа
|
|
||||||
log.Fatal("error loading cache: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
list := cache.List() // Получаю список элементов кэша
|
|
||||||
|
|
||||||
for _, i := range list {
|
|
||||||
fmt.Println(i.Key, i.Item.Data(), i.Item.DestroyTimestamp())
|
|
||||||
}
|
|
||||||
```
|
|
166
candycache.go
166
candycache.go
|
@ -1,21 +1,12 @@
|
||||||
package candycache
|
package candycache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JSON структура для создания/загрузки дампов
|
|
||||||
type Dump struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
DestroyTimestamp int64 `json:"destroyTimestamp"`
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Структура виде ключ-значение для возвращения списка элементов кэша с их ключами.
|
// Структура виде ключ-значение для возвращения списка элементов кэша с их ключами.
|
||||||
type KeyItemPair struct {
|
type KeyItemPair struct {
|
||||||
Key string
|
Key string
|
||||||
|
@ -44,6 +35,8 @@ func Cacher(cleanupInterval time.Duration) *Cache {
|
||||||
cleanupInterval: cleanupInterval,
|
cleanupInterval: cleanupInterval,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Запускаем Garbage Collector если интервал очистки больше 0
|
||||||
|
// Иначе (если он отрицательный) кэш будет жить до ручного вызова Cleanup
|
||||||
if cleanupInterval > 0 {
|
if cleanupInterval > 0 {
|
||||||
go cache.gc(cleanupInterval)
|
go cache.gc(cleanupInterval)
|
||||||
}
|
}
|
||||||
|
@ -67,7 +60,7 @@ func (c *Cache) Cleanup() {
|
||||||
defer c.Unlock()
|
defer c.Unlock()
|
||||||
|
|
||||||
for key, item := range c.storage {
|
for key, item := range c.storage {
|
||||||
if item.destroyTimestamp <= time.Now().UnixNano() {
|
if item.destroyTimestamp <= time.Now().Unix() {
|
||||||
delete(c.storage, key)
|
delete(c.storage, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,37 +77,18 @@ func (c *Cache) Flush() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получение элемента из кэша по ключу.
|
// Получение элемента из кэша по ключу.
|
||||||
func (c *Cache) Get(key string) (interface{}, error) {
|
func (c *Cache) Get(key string) (interface{}, bool) {
|
||||||
c.RLock()
|
c.RLock()
|
||||||
defer c.RUnlock()
|
defer c.RUnlock()
|
||||||
|
|
||||||
item, found := c.storage[key]
|
item, found := c.storage[key]
|
||||||
|
|
||||||
|
// Элемент не найден в кэше
|
||||||
if !found {
|
if !found {
|
||||||
return nil, errors.New("key not found")
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return item.data, nil
|
return item.data, true
|
||||||
}
|
|
||||||
|
|
||||||
// Определяет является ли элемент устаревшим.
|
|
||||||
// Вторым аргументов возвращается есть элемент в кэше или нет.
|
|
||||||
// Первым - устаревший элемент или нет.
|
|
||||||
func (c *Cache) IsExpired(key string) (bool, error) {
|
|
||||||
c.RLock()
|
|
||||||
defer c.RUnlock()
|
|
||||||
|
|
||||||
item, found := c.storage[key]
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return false, errors.New("key not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.destroyTimestamp <= time.Now().UnixNano() {
|
|
||||||
return true, nil
|
|
||||||
} else {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаление элемента по ключу.
|
// Удаление элемента по ключу.
|
||||||
|
@ -135,12 +109,12 @@ func (c *Cache) Delete(key string) error {
|
||||||
// key - ключ.
|
// key - ключ.
|
||||||
// data - данные.
|
// data - данные.
|
||||||
// ttl - время жизни элемента (time to life) в наносекундах.
|
// ttl - время жизни элемента (time to life) в наносекундах.
|
||||||
func (c *Cache) Set(key string, data interface{}, ttl time.Duration) {
|
func (c *Cache) Add(key string, data interface{}, ttl time.Duration) {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
defer c.Unlock()
|
defer c.Unlock()
|
||||||
|
|
||||||
c.storage[key] = Item{
|
c.storage[key] = Item{
|
||||||
destroyTimestamp: time.Now().UnixNano() + int64(ttl),
|
destroyTimestamp: time.Now().Unix() + int64(ttl.Seconds()),
|
||||||
data: data,
|
data: data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,13 +127,15 @@ func (c *Cache) Count() int {
|
||||||
return len(c.storage)
|
return len(c.storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Возвращает список всех элементов кэша.
|
// Печать всех элементов кэша (ключ и время уничтожения).
|
||||||
func (c *Cache) List() []KeyItemPair {
|
func (c *Cache) List() []KeyItemPair {
|
||||||
c.RLock()
|
c.RLock()
|
||||||
defer c.RUnlock()
|
defer c.RUnlock()
|
||||||
|
|
||||||
items := []KeyItemPair{}
|
// Создаем срез для хранения пар ключ-значение
|
||||||
|
items := make([]KeyItemPair, 0, len(c.storage))
|
||||||
|
|
||||||
|
// Заполняем срез парами ключ-значение
|
||||||
for key, item := range c.storage {
|
for key, item := range c.storage {
|
||||||
items = append(items, KeyItemPair{Key: key, Item: item})
|
items = append(items, KeyItemPair{Key: key, Item: item})
|
||||||
}
|
}
|
||||||
|
@ -167,22 +143,6 @@ func (c *Cache) List() []KeyItemPair {
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
// Возвращает список всех устаревших элементов кэша.
|
|
||||||
func (c *Cache) ExpiredList() []KeyItemPair {
|
|
||||||
c.RLock()
|
|
||||||
defer c.RUnlock()
|
|
||||||
|
|
||||||
items := []KeyItemPair{}
|
|
||||||
|
|
||||||
for key, item := range c.storage {
|
|
||||||
if item.destroyTimestamp <= time.Now().UnixNano() {
|
|
||||||
items = append(items, KeyItemPair{Key: key, Item: item})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
// Вернет размер всего кэша в байтах.
|
// Вернет размер всего кэша в байтах.
|
||||||
func (c *Cache) Size() int {
|
func (c *Cache) Size() int {
|
||||||
c.RLock()
|
c.RLock()
|
||||||
|
@ -196,74 +156,29 @@ func (c *Cache) Size() int {
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save сохраняет кэш в io.Writer в формате JSON, записывая каждый элемент по отдельности.
|
// ПОДДЕРЖИВАЕМЫЕ ТИПЫ:
|
||||||
func (c *Cache) Save(w io.Writer) error {
|
// Bool +
|
||||||
c.RLock()
|
// Int +
|
||||||
defer c.RUnlock()
|
// Int8 +
|
||||||
|
// Int16 +
|
||||||
if _, err := w.Write([]byte("[\n")); err != nil {
|
// Int32 +
|
||||||
return err
|
// Int64 +
|
||||||
}
|
// Uint +
|
||||||
|
// Uint8 +
|
||||||
encoder := json.NewEncoder(w)
|
// Uint16 +
|
||||||
first := true
|
// Uint32 +
|
||||||
for key, item := range c.storage {
|
// Uint64 +
|
||||||
entry := Dump{
|
// Uintptr +
|
||||||
Key: key,
|
// Float32 +
|
||||||
DestroyTimestamp: item.destroyTimestamp,
|
// Float64 +
|
||||||
Data: item.data,
|
// Complex64 +
|
||||||
}
|
// Complex128 +
|
||||||
|
// Array +
|
||||||
if !first {
|
// Func +
|
||||||
if _, err := w.Write([]byte(",\n")); err != nil {
|
// Map +
|
||||||
return err
|
// Slice +
|
||||||
}
|
// String +
|
||||||
}
|
// Struct
|
||||||
first = false
|
|
||||||
|
|
||||||
if err := encoder.Encode(entry); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := w.Write([]byte("]")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load загружает кэш из io.Reader в формате JSON.
|
|
||||||
func (c *Cache) Load(r io.Reader) error {
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(r)
|
|
||||||
|
|
||||||
if _, err := decoder.Token(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for decoder.More() {
|
|
||||||
entry := Dump{}
|
|
||||||
|
|
||||||
if err := decoder.Decode(&entry); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.storage[entry.Key] = Item{
|
|
||||||
destroyTimestamp: entry.DestroyTimestamp,
|
|
||||||
data: entry.Data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := decoder.Token(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isize(i interface{}) int {
|
func isize(i interface{}) int {
|
||||||
if i == nil {
|
if i == nil {
|
||||||
return 0
|
return 0
|
||||||
|
@ -302,12 +217,3 @@ func (i *Item) Data() interface{} {
|
||||||
func (i *Item) DestroyTimestamp() int64 {
|
func (i *Item) DestroyTimestamp() int64 {
|
||||||
return i.destroyTimestamp
|
return i.destroyTimestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Определяет является ли элемент устаревшим.
|
|
||||||
func (i *Item) IsExpired() bool {
|
|
||||||
if i.destroyTimestamp <= time.Now().UnixNano() {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue