【go语言开发】本地缓存的使用,从简单到复杂写一个本地缓存,并对比常用的开源库

这篇具有很好参考价值的文章主要介绍了【go语言开发】本地缓存的使用,从简单到复杂写一个本地缓存,并对比常用的开源库。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本文主要介绍go语言中本地缓存的使用,首先由简单到复杂手写3个本地缓存示例,使用内置的sync,map等数据结构封装cache,然后介绍常见的一些开源库,以及对比常用的开源库

前言

本地缓存是指将一部分数据存储在应用程序本地内存中,以提高数据访问速度和应用程序性能的技术。
使用本地缓存的优势:

  • 提高应用程序性能
  • 减少网络延迟
  • 改善用户体验
  • 降低外部存储系统的负荷

下面我们从简单到复杂写本地缓存

手写本地缓存

CacheNormal

在 Go 中,你可以使用内置的 sync 包和 map 数据结构来实现本地缓存。

我们首先定义了一个名为 Cache 的结构体,其中包含一个 data 字段,它是一个 map[string]interface{} 类型的数据结构,用于存储键值对。我们使用 sync.RWMutex 来保证并发安全性。

然后,我们定义了 Set 方法和 Get 方法,用于设置和获取缓存值。在 Set 方法中,我们使用互斥锁 mu 来保证并发安全。在 Get 方法中,我们使用读写锁 mu 的读锁来实现并发读取。

package cache

import (
	"sync"
)

type CacheNormal struct {
	data map[string]interface{}
	mu   sync.RWMutex
}

func NewCache() *CacheNormal {
	return &CacheNormal{
		data: make(map[string]interface{}),
	}
}

func (c *CacheNormal) Set(key string, value interface{}) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.data[key] = value
}

func (c *CacheNormal) Get(key string) (interface{}, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	value, ok := c.data[key]
	return value, ok
}

代码测试:

package cache

import (
	"fmt"
	"testing"
	"time"
)

func TestCacheNorm(t *testing.T) {
	cache := NewCache()

	// 设置缓存值
	cache.Set("key1", "value1")
	cache.Set("key2", "value2")

	// 读取缓存值
	value1, ok1 := cache.Get("key1")
	fmt.Println("Key1:", value1, ok1)

	value2, ok2 := cache.Get("key2")
	fmt.Println("Key2:", value2, ok2)

	// 等待一段时间
	time.Sleep(5 * time.Second)

	// 再次读取缓存值
	value1, ok1 = cache.Get("key1")
	fmt.Println("Key1:", value1, ok1)

	value2, ok2 = cache.Get("key2")
	fmt.Println("Key2:", value2, ok2)
}

结果展示:
go 使用本地缓存,go,golang,缓存,开源
下面我们实现一个带有过期时间的本地缓存。

CacheEx

要实现带有过期时间的本地缓存,可以使用 Go 的 sync 包和 map 数据结构结合定时器(time.Timer)来实现。

我们定义了一个名为 CacheEx 的结构体,其中包含了一个用于存储缓存项的 data 字段,并且还有一个用于接收过期键的通道 expireCh。

通过调用 NewCacheEx 函数创建一个新的缓存对象,该函数会启动一个协程 startCleanup 来定期清理过期的缓存项。

使用 Set 方法来设置缓存值,并指定缓存项的过期时间。在这个方法中,我们使用互斥锁来保证并发安全性,并将缓存项的过期时间和值存储在 data 中。同时,我们还使用 scheduleExpiration 方法来安排过期时的清理操作。

使用 Get 方法来获取缓存值。在这个方法中,我们使用读锁来进行并发读取,并检查缓存项是否过期。如果缓存项存在且未过期,则返回对应的值;否则返回空值。

package cache

import (
	"sync"
	"time"
)

type CacheEx struct {
	data     map[string]cacheItem
	mu       sync.RWMutex
	expireCh chan string
}

type cacheItem struct {
	value      interface{}
	expiration time.Time
}

func NewCacheEx() *CacheEx {
	c := &CacheEx{
		data:     make(map[string]cacheItem),
		expireCh: make(chan string),
	}
	go c.startCleanup()
	return c
}

func (c *CacheEx) Set(key string, value interface{}, expiration time.Duration) {
	c.mu.Lock()
	defer c.mu.Unlock()

	expireTime := time.Now().Add(expiration)
	c.data[key] = cacheItem{
		value:      value,
		expiration: expireTime,
	}
	go c.scheduleExpiration(key, expireTime)
}

func (c *CacheEx) Get(key string) (interface{}, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()

	item, ok := c.data[key]
	if ok && item.expiration.After(time.Now()) {
		return item.value, true
	}
	return nil, false
}

func (c *CacheEx) Delete(key string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.data, key)
}

func (c *CacheEx) startCleanup() {
	for {
		key := <-c.expireCh
		c.Delete(key)
	}
}

func (c *CacheEx) scheduleExpiration(key string, expireTime time.Time) {
	duration := time.Until(expireTime)
	timer := time.NewTimer(duration)
	<-timer.C
	c.expireCh <- key
}

代码测试:

func TestCacheExpireTime(t *testing.T) {
	cache := NewCacheEx()

	// 设置缓存值,带有过期时间
	cache.Set("key1", "value1", 2*time.Second)
	cache.Set("key2", "value2", 5*time.Second)

	// 读取缓存值
	value1, ok1 := cache.Get("key1")
	fmt.Println("Key1:", value1, ok1)

	value2, ok2 := cache.Get("key2")
	fmt.Println("Key2:", value2, ok2)

	// 等待一段时间
	time.Sleep(3 * time.Second)

	// 再次读取缓存值
	value1, ok1 = cache.Get("key1")
	fmt.Println("Key1:", value1, ok1)

	value2, ok2 = cache.Get("key2")
	fmt.Println("Key2:", value2, ok2)
}

结果展示:
go 使用本地缓存,go,golang,缓存,开源

CacheV3

package cache

import (
	"sync"
	"time"
)

type item struct {
	value      interface{}
	expiration int64
}

type CacheV3 struct {
	items       sync.Map
	lock        sync.RWMutex
	defaultTTL  time.Duration
	maxCapacity int
	evictList   []interface{}
}

func NewCacheV3(defaultTTL time.Duration, maxCapacity int) *CacheV3 {
	return &CacheV3{
		defaultTTL:  defaultTTL,
		maxCapacity: maxCapacity,
		evictList:   make([]interface{}, 0, maxCapacity),
	}
}

func (c *CacheV3) Set(key string, value interface{}, ttl time.Duration) {
	c.lock.Lock()
	defer c.lock.Unlock()

	if c.cacheSize() >= c.maxCapacity {
		c.evict(1)
	}

	if ttl == 0 {
		ttl = c.defaultTTL
	}
	expiration := time.Now().Add(ttl).UnixNano()
	c.items.Store(key, &item{value, expiration})

	time.AfterFunc(ttl, func() {
		c.lock.Lock()
		defer c.lock.Unlock()

		if _, found := c.items.Load(key); found {
			c.items.Delete(key)
			c.evictList = append(c.evictList, key)
		}
	})
}

func (c *CacheV3) Get(key string) (interface{}, bool) {
	c.lock.RLock()
	defer c.lock.RUnlock()

	if val, found := c.items.Load(key); found {
		item := val.(*item)
		if item.expiration > 0 && time.Now().UnixNano() > item.expiration {
			c.items.Delete(key)
			return nil, false
		}
		return item.value, true
	}

	return nil, false
}

func (c *CacheV3) evict(count int) {
	for i := 0; i < count; i++ {
		key := c.evictList[0]
		c.evictList = c.evictList[1:]
		c.items.Delete(key)
	}
}

func (c *CacheV3) cacheSize() int {
	size := 0
	c.items.Range(func(_, _ interface{}) bool {
		size++
		return true
	})
	return size
}

代码测试:

func TestCacheV3(t *testing.T) {
	c := NewCacheV3(time.Minute, 100)

	c.Set("key1", "value1", time.Second*30)
	c.Set("key2", "value2", time.Minute)

	val, found := c.Get("key1")
	if found {
		fmt.Println(val)
	}

	time.Sleep(time.Second * 45)

	val, found = c.Get("key1")
	if found {
		fmt.Println(val)
	}

	time.Sleep(time.Second * 30)

	val, found = c.Get("key1")
	if found {
		fmt.Println(val)
	} else {
		fmt.Println("key1 expired")
	}
}

结果展示:
go 使用本地缓存,go,golang,缓存,开源

开源库

cache2go

最新代码请参考:https://github.com/muesli/cache2go
以下代码仅供参考

type Item struct {
	//read write lock
	sync.RWMutex
	key  interface{}
	data interface{}
	// cache duration.
	duration time.Duration
	// create time
	createTime time.Time
	//last access time
	accessTime time.Time
	//visit times
	count int64
	// callback after deleting
	deleteCallback func(key interface{})
}

//create item.
func NewItem(key interface{}, duration time.Duration, data interface{}) *Item {
	t := time.Now()
	return &Item{
		key:            key,
		duration:       duration,
		createTime:     t,
		accessTime:     t,
		count:          0,
		deleteCallback: nil,
		data:           data,
	}
}

//keep alive
func (item *Item) KeepAlive() {
	item.Lock()
	defer item.Unlock()
	item.accessTime = time.Now()
	item.count++
}

func (item *Item) Duration() time.Duration {
	return item.duration
}

func (item *Item) AccessTime() time.Time {
	item.RLock()
	defer item.RUnlock()
	return item.accessTime
}

func (item *Item) CreateTime() time.Time {
	return item.createTime
}

func (item *Item) Count() int64 {
	item.RLock()
	defer item.RUnlock()
	return item.count
}

func (item *Item) Key() interface{} {
	return item.key
}

func (item *Item) Data() interface{} {
	return item.data
}

func (item *Item) SetDeleteCallback(f func(interface{})) {
	item.Lock()
	defer item.Unlock()
	item.deleteCallback = f
}

// table for managing cache items
type Table struct {
	sync.RWMutex

	//all cache items
	items map[interface{}]*Item
	// trigger cleanup
	cleanupTimer *time.Timer
	// cleanup interval
	cleanupInterval time.Duration
	loadData        func(key interface{}, args ...interface{}) *Item
	// callback after adding.
	addedCallback func(item *Item)
	// callback after deleting
	deleteCallback func(item *Item)
}

func (table *Table) Count() int {
	table.RLock()
	defer table.RUnlock()
	return len(table.items)
}

func (table *Table) Foreach(trans func(key interface{}, item *Item)) {
	table.RLock()
	defer table.RUnlock()

	for k, v := range table.items {
		trans(k, v)
	}
}

func (table *Table) SetDataLoader(f func(interface{}, ...interface{}) *Item) {
	table.Lock()
	defer table.Unlock()
	table.loadData = f
}

func (table *Table) SetAddedCallback(f func(*Item)) {
	table.Lock()
	defer table.Unlock()
	table.addedCallback = f
}

func (table *Table) SetDeleteCallback(f func(*Item)) {
	table.Lock()
	defer table.Unlock()
	table.deleteCallback = f
}

func (table *Table) RunWithRecovery(f func()) {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("occur error %v \r\n", err)
		}
	}()

	f()
}

func (table *Table) checkExpire() {
	table.Lock()
	if table.cleanupTimer != nil {
		table.cleanupTimer.Stop()
	}
	if table.cleanupInterval > 0 {
		table.log("Expiration check triggered after %v for table", table.cleanupInterval)
	} else {
		table.log("Expiration check installed for table")
	}

	// in order to not take the lock. use temp items.
	items := table.items
	table.Unlock()

	//in order to make timer more precise, update now every loop.
	now := time.Now()
	smallestDuration := 0 * time.Second
	for key, item := range items {
		//take out our things, in order not to take the lock.
		item.RLock()
		duration := item.duration
		accessTime := item.accessTime
		item.RUnlock()

		// 0 means valid.
		if duration == 0 {
			continue
		}
		if now.Sub(accessTime) >= duration {
			//cache item expired.
			_, e := table.Delete(key)
			if e != nil {
				table.log("occur error while deleting %v", e.Error())
			}
		} else {
			//find the most possible expire item.
			if smallestDuration == 0 || duration-now.Sub(accessTime) < smallestDuration {
				smallestDuration = duration - now.Sub(accessTime)
			}
		}
	}

	//trigger next clean
	table.Lock()
	table.cleanupInterval = smallestDuration
	if smallestDuration > 0 {
		table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
			go table.RunWithRecovery(table.checkExpire)
		})
	}
	table.Unlock()
}

// add item
func (table *Table) Add(key interface{}, duration time.Duration, data interface{}) *Item {
	item := NewItem(key, duration, data)

	table.Lock()
	table.log("Adding item with key %v and lifespan of %d to table", key, duration)
	table.items[key] = item

	expDur := table.cleanupInterval
	addedItem := table.addedCallback
	table.Unlock()

	if addedItem != nil {
		addedItem(item)
	}

	//find the most possible expire item.
	if duration > 0 && (expDur == 0 || duration < expDur) {
		table.checkExpire()
	}

	return item
}

func (table *Table) Delete(key interface{}) (*Item, error) {
	table.RLock()
	r, ok := table.items[key]
	if !ok {
		table.RUnlock()
		return nil, errors.New(fmt.Sprintf("no item with key %s", key))
	}

	deleteCallback := table.deleteCallback
	table.RUnlock()

	if deleteCallback != nil {
		deleteCallback(r)
	}

	r.RLock()
	defer r.RUnlock()
	if r.deleteCallback != nil {
		r.deleteCallback(key)
	}

	table.Lock()
	defer table.Unlock()
	table.log("Deleting item with key %v created on %s and hit %d times from table", key, r.createTime, r.count)
	delete(table.items, key)

	return r, nil
}

//check exist.
func (table *Table) Exists(key interface{}) bool {
	table.RLock()
	defer table.RUnlock()
	_, ok := table.items[key]

	return ok
}

//if exist, return false. if not exist add a key and return true.
func (table *Table) NotFoundAdd(key interface{}, lifeSpan time.Duration, data interface{}) bool {
	table.Lock()

	if _, ok := table.items[key]; ok {
		table.Unlock()
		return false
	}

	item := NewItem(key, lifeSpan, data)
	table.log("Adding item with key %v and lifespan of %d to table", key, lifeSpan)
	table.items[key] = item

	expDur := table.cleanupInterval
	addedItem := table.addedCallback
	table.Unlock()

	if addedItem != nil {
		addedItem(item)
	}

	if lifeSpan > 0 && (expDur == 0 || lifeSpan < expDur) {
		table.checkExpire()
	}
	return true
}

func (table *Table) Value(key interface{}, args ...interface{}) (*Item, error) {
	table.RLock()
	r, ok := table.items[key]
	loadData := table.loadData
	table.RUnlock()

	if ok {
		//update visit count and visit time.
		r.KeepAlive()
		return r, nil
	}

	if loadData != nil {
		item := loadData(key, args...)
		if item != nil {
			table.Add(key, item.duration, item.data)
			return item, nil
		}

		return nil, errors.New("cannot load item")
	}

	return nil, nil
}

// truncate a table.
func (table *Table) Truncate() {
	table.Lock()
	defer table.Unlock()

	table.log("Truncate table")

	table.items = make(map[interface{}]*Item)
	table.cleanupInterval = 0
	if table.cleanupTimer != nil {
		table.cleanupTimer.Stop()
	}
}

//support table sort
type ItemPair struct {
	Key         interface{}
	AccessCount int64
}

type ItemPairList []ItemPair

func (p ItemPairList) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
func (p ItemPairList) Len() int           { return len(p) }
func (p ItemPairList) Less(i, j int) bool { return p[i].AccessCount > p[j].AccessCount }

//return most visited.
func (table *Table) MostAccessed(count int64) []*Item {
	table.RLock()
	defer table.RUnlock()

	p := make(ItemPairList, len(table.items))
	i := 0
	for k, v := range table.items {
		p[i] = ItemPair{k, v.count}
		i++
	}
	sort.Sort(p)

	var r []*Item
	c := int64(0)
	for _, v := range p {
		if c >= count {
			break
		}

		item, ok := table.items[v.Key]
		if ok {
			r = append(r, item)
		}
		c++
	}

	return r
}

// print log.
func (table *Table) log(format string, v ...interface{}) {
	//fmt.Printf(format+"\r\n", v)
}

func NewTable() *Table {
	return &Table{
		items: make(map[interface{}]*Item),
	}
}

go-cache

https://github.com/patrickmn/go-cache

  • 优点:

    • 简单易用,适合快速集成到现有项目中。
    • 支持过期时间,可以自动淘汰过期的缓存项。
    • 支持多种数据类型的缓存。
  • 缺点:

    • 性能略低于其他库,不适合高并发读写的场景。
    • 不支持分布式缓存。

bigcache

https://github.com/allegro/bigcache

  • 优点:

    • 高性能,适用于需要快速读写大量数据的场景。
    • 使用murmurhash算法来计算哈希值,减少了哈希冲突。
    • 使用多个shard来减少锁竞争。
  • 缺点:

    • 不支持过期时间,只能手动清除过期的缓存项。
    • 内存使用较高,不适合存储大量数据。

groupcache

https://github.com/golang/groupcache

  • 优点:

    • 支持分布式缓存,可以在多台机器上共享缓存。
    • 采用LRU算法来淘汰缓存项,具备一定的缓存性能。
    • 提供一致性哈希算法,可以解决节点扩容等问题。
  • 缺点:

    • 比较复杂,使用起来较为繁琐。
    • 只支持字符串类型的键值对。

本地缓存对比

参考文档:

  • https://zhuanlan.zhihu.com/p/487455942

  • https://www.jianshu.com/p/0ff2e8c61c9c?tdsourcetag=s_pctim_aiomsg

go 使用本地缓存,go,golang,缓存,开源
下面对每个库的详细介绍:文章来源地址https://www.toymoban.com/news/detail-764250.html

  1. go-cache:
  • 描述:go-cache是一款简单而有效的内存缓存库,支持设置过期时间和GC机制。
  • 并发安全:是,使用Go的sync.Map实现数据的并发安全存储和访问。
  • 存储限制:无,可以存储任意类型的数据。
  • 淘汰策略:默认为LRU(最近最少使用)算法,也支持手动删除过期的缓存项。
  • 分布式支持:不支持。
  1. freecache:
  • 描述:freecache是一款高性能的内存缓存库,使用LRU算法进行缓存项的淘汰。
  • 并发安全:是,使用读写锁实现并发安全访问。
  • 存储限制:固定大小,需要在初始化时指定总共可以缓存的字节数。
  • 淘汰策略:默认为LRU(最近最少使用)算法,不支持自定义。
  • 分布式支持:不支持。
  1. bigcache:
  • 描述:bigcache是一款高性能的内存缓存库,使用murmurhash哈希算法快速查找。
  • 并发安全:是,使用多个读写锁来实现高并发的访问控制。
  • 存储限制:固定大小,需要在初始化时指定最多可以缓存的条目数。
  • 淘汰策略:默认为LRU(最近最少使用)算法,不支持自定义。
  • 分布式支持:不支持。
  1. groupcache:
  • 描述:groupcache是一款支持分布式缓存的库,提供一致性哈希和HTTP请求缓存功能。
  • 并发安全:是,使用读写锁实现并发安全访问。
  • 存储限制:无,可以存储任意类型的数据。
  • 淘汰策略:支持自定义淘汰策略,例如手动删除过期的缓存项。
  • 分布式支持:是,支持分布式缓存,将数据分片存储在多个节点上,通过查询一致性哈希环来确定数据所在的节点。
  1. gocache:
  • 描述:gocache是一款快速、强大的内存缓存库,支持过期时间、并发安全和自定义淘汰策略。
  • 并发安全:是,使用读写锁实现并发安全访问。
  • 存储限制:无,可以存储任意类型的数据。
  • 淘汰策略:默认为LRU(最近最少使用)算法,也支持自定义淘汰策略。
  • 分布式支持:不支持。

到了这里,关于【go语言开发】本地缓存的使用,从简单到复杂写一个本地缓存,并对比常用的开源库的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 使用go-cqhttp搭建本地qq机器人 并实现发送信息案例(使用python)语言实现

    目录 1.go-cqhttp简介 2.案例介绍 3.下载go-cqhttp 4.配置安装 ①:下载之后我们可以将执行程序放在一个新建的文件夹中,然后双击执行该程序 直接点击确定执行  ②:然后双击执行该程序,会看到让我们选择一种通信方式,我们直接输入0 选择HTTP通信 ,然后回车  ③:然后他会

    2024年02月14日
    浏览(62)
  • 使用Go语言进行安卓开发

    本文将介绍如何使用Go语言进行安卓开发。我们将探讨使用Go语言进行安卓开发的优点、准备工作、基本概念和示例代码。通过本文的学习,你将了解如何使用Go语言构建高效的安卓应用程序。 随着移动互联网的快速发展,安卓应用程序的需求越来越旺盛。使用传统的Java和K

    2024年02月06日
    浏览(46)
  • 【go语言开发】redis简单使用

    本文主要介绍redis安装和使用。首先安装redis依赖库,这里是v8版本;然后连接redis,完成基本配置;最后测试封装的工具类 欢迎大家访问个人博客网址:https://www.maogeshuo.com,博主努力更新中… 参考文件: Yaml文件配置,Config使用 Log日志封装 常用工具类封装 命令行安装redis

    2024年03月12日
    浏览(59)
  • 使用VSCODE配置GO语言开发环境

    1. 安装GO SDK 官方下载地址是:golan.google.cn/dl 2. 安装完毕后,会自动在配置文件中加入一些内容,其中比较重要的三个是: GOROOT(具体GO语言在硬盘上安装的位置,比如D:/GO) GOPATH(未来使用go install安装第三方工具包时,都会安装在GOPATH指定文件夹下的src或bin目录下,比如

    2024年02月06日
    浏览(46)
  • Go语言开发者的Apache Arrow使用指南:内存管理

    如果你看了上一篇《Go语言开发者的Apache Arrow使用指南:数据类型》 [1] 中的诸多Go操作arrow的代码示例,你很可能会被代码中大量使用的Retain和Release方法搞晕。不光大家有这样的感觉,我也有同样的feeling:**Go是GC语言 [2] ,为什么还要借助另外一套Retain和Release来进行内存管理

    2024年02月11日
    浏览(54)
  • 在使用go语言开发的时候,程序启动后如何获取程序pid

    在Go语言中,标准库并没有直接提供获取进程ID(PID)的函数。通常,你可以使用os包和syscall包来调用底层的操作系统函数来获取PID。 以下是一个获取程序PID的示例代码: 在这个示例中,os.Getpid() 返回当前进程的PID。另外,syscall.Getpid() 也提供了相同的功能。 请注意,这种方

    2024年01月20日
    浏览(48)
  • 初识Go语言25-数据结构与算法【堆、Trie树、用go中的list与map实现LRU算法、用go语言中的map和堆实现超时缓存】

      堆是一棵二叉树。大根堆即任意节点的值都大于等于其子节点。反之为小根堆。   用数组来表示堆,下标为 i 的结点的父结点下标为(i-1)/2,其左右子结点分别为 (2i + 1)、(2i + 2)。 构建堆   每当有元素调整下来时,要对以它为父节点的三角形区域进行调整。 插入元素

    2024年02月12日
    浏览(59)
  • flutter开发实战-显示本地图片网络图片及缓存目录图片

    flutter开发实战-显示本地图片网络图片及缓存目录图片 在最近开发中碰到了需要显示缓存目录图片,这里顺便整理一下,显示本地图片、网络图片、缓存目录图片的方法。 1 在项目根目录下创建名为 images文件夹,也可以将images放在asserts文件夹下 2.在pubspec.yaml中配置images相关

    2024年02月14日
    浏览(39)
  • uniapp 图片本地缓存,本地路径离线使用

    功能介绍:uniapp 多张图片本地存储,下载进度条。 目前还差一个删除功能,有机会再加上

    2024年02月16日
    浏览(34)
  • springboot:缓存不止redis,学会使用本地缓存ehcache

    随着redis的普及,更多的同学对redis分布式缓存更加熟悉,但在一些实际场景中,其实并不需要用到redis,使用更加简单的本地缓存即可实现我们的缓存需求。 今天,我们一起来看看本地缓存组件ehcache ehcache是基于java开发的本地缓存组件,无需单独安装部署,只要引入jar包就

    2024年02月01日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包