LRU缓存算法
双向链表+哈希表(非线程安全)
https://leetcode.cn/problems/lru-cache/solutions/259678/lruhuan-cun-ji-zhi-by-leetcode-solution/
/*** LRU算法: 哈希表+双向链表实现* 1. 双向链表按照被使用的顺序来存储, 靠近头部的节点是最近使用的, 靠近尾部的节点是最久未使用的* 2. 哈希表存储key和node映射关系, 通过key能快速定位到链表中的节点* @author zhangjunfeng* @date 2023/2/2 16:15*/
public class LRUCache {class DLinkedNode {int key;int value;DLinkedNode prev;DLinkedNode next;public DLinkedNode() {}public DLinkedNode(int _key, int _value) {key = _key;value = _value;}}private Map<Integer, DLinkedNode> cache = new HashMap<>();private int size;private int capacity;private DLinkedNode head, tail;public LRUCache(int _capacity) {this.size = 0;this.capacity = _capacity;head = new DLinkedNode();tail = new DLinkedNode();head.next = tail;tail.prev = head;}/*** 1. 先判断key是否存在, 不存在返回-1* 2. 若key存在, 则key对应的节点就是最近访问节点, 通过哈希表映射到在双向链表中的位置, 然后将节点移动到链表头部* @param key* @return*/public int get(int key) {DLinkedNode node = cache.get(key);if (node == null) {return -1;}// key存在则移动到链表头部, 表示最近访问moveToHead(node);return node.value;}/*** 1. 如果key不存在, 创建一个新节点并在链表头部添加该节点, 判断链表长度是否超出容量限制, 若超出容量, 则删除链表尾部结点* 2. 如果key存在, 覆盖旧值, 将节点移动到头部* @param key* @param value*/public void put(int key, int value) {DLinkedNode node = cache.get(key);if (node == null) {// node不存在, 则创建一个新节点DLinkedNode newNode = new DLinkedNode(key, value);// 添加进哈希表cache.put(key, newNode);// 添加到链表头部, 表示最近访问addToHead(newNode);// 链表长度加1++size;// 如果超出缓存容量if (size > capacity) {// 删除链表最后一个结点, 去掉最长时间未访问的DLinkedNode tail = removeTail();// 去掉哈希表中对应节点cache.remove(tail.key);// 减小链表长度--size;}} else {// 如果缓存中有// 先覆盖旧值node.value = value;// 再将节点移到链表头部, 表示最近访问moveToHead(node);}}/*** 添加一个结点需要修改四条链* @param node*/private void addToHead(DLinkedNode node) {node.prev = head;node.next = head.next;head.next.prev = node;head.next = node;}/*** 删除一个结点需要修改两条链* @param node*/private void removeNode(DLinkedNode node) {node.prev.next = node.next;node.next.prev = node.prev;}/*** 把结点移到头部*/private void moveToHead(DLinkedNode node) {// 先删除节点removeNode(node);// 再将该节点移到头部addToHead(node);}/*** 删除尾结点并返回*/private DLinkedNode removeTail() {DLinkedNode last = tail.prev;removeNode(last);return last;}public void print() {DLinkedNode cur = head.next;while (cur != null && cur.next != null) {System.out.println("key: " + cur.key + "; value: " + cur.value);cur = cur.next;}System.out.println("-----------------");}
}
线程安全版本
实现方法
ConcurrentHashMap
+ ConcurrentLinkedQueue
+ReadWriteLock
ConcurrentLinkedQueue简单介绍
ConcurrentLinkedQueue是一个基于单向链表的无界无锁线程安全的队列,适合在高并发环境下使用,效率比较高。 我们在使用的时候,可以就把它理解为我们经常接触的数据结构——队列,不过是增加了多线程下的安全性保证罢了。和普通队列一样,它也是按照先进先出(FIFO)的规则对接点进行排序。 另外,队列元素中不可以放置null元素。
ConcurrentLinkedQueue中
最主要的两个方法是:offer(value)
和poll()
,分别实现队列的两个重要的操作:入队和出队(offer(value)
等价于 add(value)
)。
我们添加一个元素到队列的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。
利用ConcurrentLinkedQueue
队列先进先出的特性,每当我们 put
/get
(缓存被使用)元素的时候,我们就将这个元素存放在队列尾部,这样就能保证队列头部的元素是最近最少使用的。
ReadWriteLock简单介绍
ReadWriteLock
是一个接口,位于java.util.concurrent.locks
包下,里面只有两个方法分别返回读锁和写锁:
public interface ReadWriteLock {/*** 返回读锁*/Lock readLock();/*** 返回写锁*/Lock writeLock();
}
ReentrantReadWriteLock
是ReadWriteLock
接口的具体实现类。
读写锁还是比较适合缓存这种读多写少的场景。读写锁可以保证多个线程和同时读取,但是只有一个线程可以写入。但是,有一个问题是当读锁被线程持有的时候,读锁是无法被其它线程申请的,会处于阻塞状态,直至读锁被释放。
另外,同一个线程持有写锁时是可以申请读锁,但是持有读锁的情况下不可以申请写锁。
ScheduledExecutorService 简单介绍
ScheduledExecutorService
是一个接口,ScheduledThreadPoolExecutor
是其主要实现类。
ScheduledThreadPoolExecutor
主要用来在给定的延迟后运行任务,或者定期执行任务。 这个在实际项目用到的比较少,因为有其他方案选择比如quartz
。但是,在一些需求比较简单的场景下还是非常有用的!
ScheduledThreadPoolExecutor
使用的任务队列 DelayQueue
封装了一个 PriorityQueue
,PriorityQueue
会对队列中的任务进行排序,执行所需时间短的放在前面先被执行,如果执行所需时间相同则先提交的任务将被先执行。
原理
LRU缓存指的是当缓存大小已达到最大分配容量的时候,如果再要去缓存新的对象数据的话,就需要将缓存中最近访问最少的对象删除掉以便给新来的数据腾出空间。
ConcurrentHashMap
是线程安全的Map,我们可以利用它缓存 key,value形式的数。ConcurrentLinkedQueue
是一个线程安全的基于链表的队列(先进先出),我们可以用它来维护 key 。每当我们put/get(缓存被使用)元素的时候,我们就将这个元素对应的 key 存放在队列尾部,这样就能保证队列头部的元素是最近最少使用的。当我们的缓存容量不够的时候,我们直接移除队列头部对应的key以及这个key对应的缓存即可!
另外,我们用到了ReadWriteLock
(读写锁)来保证线程安全。
代码实现
/*** @author shuang.kou* <p>* 使用 ConcurrentHashMap+ConcurrentLinkedQueue+ReadWriteLock实现线程安全的 LRU 缓存* 这里只是为了学习使用,本地缓存推荐使用 Guava 自带的。*/
public class MyLruCache<K, V> {/*** 缓存的最大容量*/private final int maxCapacity;private ConcurrentHashMap<K, V> cacheMap;private ConcurrentLinkedQueue<K> keys;/*** 读写锁*/private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();private Lock writeLock = readWriteLock.writeLock();private Lock readLock = readWriteLock.readLock();public MyLruCache(int maxCapacity) {if (maxCapacity < 0) {throw new IllegalArgumentException("Illegal max capacity: " + maxCapacity);}this.maxCapacity = maxCapacity;cacheMap = new ConcurrentHashMap<>(maxCapacity);keys = new ConcurrentLinkedQueue<>();}public V put(K key, V value) {// 加写锁writeLock.lock();try {//1.key是否存在于当前缓存if (cacheMap.containsKey(key)) {moveToTailOfQueue(key);cacheMap.put(key, value);return value;}//2.是否超出缓存容量,超出的话就移除队列头部的元素以及其对应的缓存if (cacheMap.size() == maxCapacity) {System.out.println("maxCapacity of cache reached");removeOldestKey();}//3.key不存在于当前缓存。将key添加到队列的尾部并且缓存key及其对应的元素keys.add(key);cacheMap.put(key, value);return value;} finally {writeLock.unlock();}}public V get(K key) {//加读锁readLock.lock();try {//key是否存在于当前缓存if (cacheMap.containsKey(key)) {// 存在的话就将key移动到队列的尾部moveToTailOfQueue(key);return cacheMap.get(key);}//不存在于当前缓存中就返回Nullreturn null;} finally {readLock.unlock();}}public V remove(K key) {writeLock.lock();try {//key是否存在于当前缓存if (cacheMap.containsKey(key)) {// 存在移除队列和Map中对应的Keykeys.remove(key);return cacheMap.remove(key);}//不存在于当前缓存中就返回Nullreturn null;} finally {writeLock.unlock();}}/*** 将元素添加到队列的尾部(put/get的时候执行)*/private void moveToTailOfQueue(K key) {keys.remove(key);keys.add(key);}/*** 移除队列头部的元素以及其对应的缓存 (缓存容量已满的时候执行)*/private void removeOldestKey() {K oldestKey = keys.poll();if (oldestKey != null) {cacheMap.remove(oldestKey);}}public int size() {return cacheMap.size();}}
并发测试
int threadNum = 10;
int batchSize = 10;
//init cache
MyLruCache<String, Integer> myLruCache = new MyLruCache<>(batchSize * 10);
//init thread pool with 10 threads
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadNum);
//init CountDownLatch with 10 count
CountDownLatch latch = new CountDownLatch(threadNum);
AtomicInteger atomicInteger = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
for (int t = 0; t < threadNum; t++) {fixedThreadPool.submit(() -> {for (int i = 0; i < batchSize; i++) {int value = atomicInteger.incrementAndGet();myLruCache.put("id" + value, value);}latch.countDown();});
}
//wait for 10 threads to complete the task
latch.await();
fixedThreadPool.shutdown();
System.out.println("Cache size:" + myLruCache.size());//Cache size:100
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
System.out.println(String.format("Time cost:%dms", duration));//Time cost:511ms
线程安全并且带有过期时间
实际上就是在我们上面时间的LRU缓存的基础上加上一个定时任务去删除缓存,单纯利用 JDK 提供的类,我们实现定时任务的方式有很多种:
Timer
:不被推荐,多线程会存在问题。ScheduledExecutorService
:定时器线程池,可以用来替代Timer
DelayQueue
:延时队列quartz
:一个很火的开源任务调度框架,很多其他框架都是基于quartz
开发的,比如当当网的elastic-job
就是基于quartz
二次开发之后的分布式调度解决方案- …
最终我们选择了 ScheduledExecutorService
,主要原因是它易用(基于DelayQueue
做了很多封装)并且基本能满足我们的大部分需求。
我们在我们上面实现的线程安全的 LRU 缓存基础上,简单稍作修改即可!我们增加了一个方法:
private void removeAfterExpireTime(K key, long expireTime) {scheduledExecutorService.schedule(() -> {//过期后清除该键值对cacheMap.remove(key);keys.remove(key);}, expireTime, TimeUnit.MILLISECONDS);
}
我们put元素的时候,如果通过这个方法就能直接设置过期时间。
完整源码
/*** @author shuang.kou* <p>* 使用 ConcurrentHashMap+ConcurrentLinkedQueue+ReadWriteLock+ScheduledExecutorService实现线程安全的 LRU 缓存* 这里只是为了学习使用,本地缓存推荐使用 Guava 自带的,使用 Spring 的话,推荐使用Spring Cache*/
public class MyLruCacheWithExpireTime<K, V> {/*** 缓存的最大容量*/private final int maxCapacity;private ConcurrentHashMap<K, V> cacheMap;private ConcurrentLinkedQueue<K> keys;/*** 读写锁*/private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();private Lock writeLock = readWriteLock.writeLock();private Lock readLock = readWriteLock.readLock();private ScheduledExecutorService scheduledExecutorService;public MyLruCacheWithExpireTime(int maxCapacity) {if (maxCapacity < 0) {throw new IllegalArgumentException("Illegal max capacity: " + maxCapacity);}this.maxCapacity = maxCapacity;cacheMap = new ConcurrentHashMap<>(maxCapacity);keys = new ConcurrentLinkedQueue<>();scheduledExecutorService = Executors.newScheduledThreadPool(3);}public V put(K key, V value, long expireTime) {// 加写锁writeLock.lock();try {//1.key是否存在于当前缓存if (cacheMap.containsKey(key)) {moveToTailOfQueue(key);cacheMap.put(key, value);return value;}//2.是否超出缓存容量,超出的话就移除队列头部的元素以及其对应的缓存if (cacheMap.size() == maxCapacity) {System.out.println("maxCapacity of cache reached");removeOldestKey();}//3.key不存在于当前缓存。将key添加到队列的尾部并且缓存key及其对应的元素keys.add(key);cacheMap.put(key, value);if (expireTime > 0) {removeAfterExpireTime(key, expireTime);}return value;} finally {writeLock.unlock();}}public V get(K key) {//加读锁readLock.lock();try {//key是否存在于当前缓存if (cacheMap.containsKey(key)) {// 存在的话就将key移动到队列的尾部moveToTailOfQueue(key);return cacheMap.get(key);}//不存在于当前缓存中就返回Nullreturn null;} finally {readLock.unlock();}}public V remove(K key) {writeLock.lock();try {//key是否存在于当前缓存if (cacheMap.containsKey(key)) {// 存在移除队列和Map中对应的Keykeys.remove(key);return cacheMap.remove(key);}//不存在于当前缓存中就返回Nullreturn null;} finally {writeLock.unlock();}}/*** 将元素添加到队列的尾部(put/get的时候执行)*/private void moveToTailOfQueue(K key) {keys.remove(key);keys.add(key);}/*** 移除队列头部的元素以及其对应的缓存 (缓存容量已满的时候执行)*/private void removeOldestKey() {K oldestKey = keys.poll();if (oldestKey != null) {cacheMap.remove(oldestKey);}}private void removeAfterExpireTime(K key, long expireTime) {scheduledExecutorService.schedule(() -> {//过期后清除该键值对cacheMap.remove(key);keys.remove(key);}, expireTime, TimeUnit.MILLISECONDS);}public int size() {return cacheMap.size();}}
测试效果:
MyLruCacheWithExpireTime<Integer,String> myLruCache = new MyLruCacheWithExpireTime<>(3);
myLruCache.put(1,"Java",3;
myLruCache.put(2,"C++",3;
myLruCache.put(3,"Python",1500);
System.out.println(myLruCache.size());//3
Thread.sleep(2;
System.out.println(myLruCache.size());//2
资料来自
- https://zhuanlan.zhihu.com/p/135936339
相关文章:
LRU缓存算法
双向链表哈希表(非线程安全) https://leetcode.cn/problems/lru-cache/solutions/259678/lruhuan-cun-ji-zhi-by-leetcode-solution/ /*** LRU算法: 哈希表双向链表实现* 1. 双向链表按照被使用的顺序来存储, 靠近头部的节点是最近使用的, 靠近尾部的节…...
@Configuration注解
Configuration注解介绍 Configuration注解,用于标注一个类是一个spring的配置类(同时,也是一个bean),配置类中可以使用ComponentScan、Import、ImportResource 和 Bean等注解的方式定义beanDefinition。 Target(Elem…...

基于springboot+vue的食疗系统
基于springbootvue的食疗系统 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取项目下载方式🍅 一、项目背景介绍&…...

sklearn学习-朴素贝叶斯
文章目录一、概述1、真正的概率分类器2、sklearn中的朴素贝叶斯二、不同分布下的贝叶斯1、高斯朴素贝叶斯GaussianNB2、探索贝叶斯:高斯朴素贝叶斯擅长的数据集3、探索贝叶斯:高斯朴素贝叶斯的拟合效果与运算速度总结一、概述 1、真正的概率分类器 算法…...

分享112个HTML艺术时尚模板,总有一款适合您
分享112个HTML艺术时尚模板,总有一款适合您 112个HTML艺术时尚模板下载链接:https://pan.baidu.com/s/1D3-mfPOud-f3vy9yLl-bmw?pwdfph2 提取码:fph2 Python采集代码下载链接:采集代码.zip - 蓝奏云 时尚平面模特网站模板 潮…...
用GDB远程调试运行于QEMU的程序
1. 前言 限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。 2. 测试环境 本文使用 Ubuntu 16.04.4 LTS QEMU 环境进行调试。 3. 用 GDB 调试 QEMU 内程序 3.1 编写用来调试的程序 我们用 ARM32 来进行调试…...

20 堆排序
文章目录1 堆排序的概念2 堆排序基本思想3 堆排序步骤图解说明4 堆排序的代码实现1 堆排序的概念 1) 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O(nlogn)…...

2023最新文件快递柜系统网站源码 | 匿名口令分享 | 临时文件分享
内容目录一、详细介绍二、效果展示1.部分代码2.效果图展示三、学习资料下载一、详细介绍 2023最新文件快递柜系统网站源码 | 匿名口令分享 | 临时文件分享 很多时候,我们都想将一些文件或文本传送给别人,或者跨端传递一些信息,但是我们又不…...
分片策略(二)
分片策略 基本概念 分片键 用于分片的字段,是将数据库或表拆分的字段,比如,我可以使用user_id作为分片键将用户数据分到不同的表中,这里的user_id就是分片键,除了这种单字段分片,ShardingSphere还支持多…...

Qt之调色板类QPalette的使用
文章目录QPalette调色板类前言代码知识点讲解QPalette调色板类 前言 Qt提供的调色板类QPalette专门用于管理部件的外观显示,相当于部件或对话框的调色板,管理他们所有的颜色信息。每个部件都包含一个QPalette对象,在显示时,按照…...

Kotlin 32. Kotlin 多语言支持
Kotlin 多语言支持 对于 Kotlin 来说,当我们新建一个项目时,会默认在 values/ 文件夹下,生成一个 strings.xml 文件。比如说, <resources><string name"app_name">exampleNewProject</string> <…...

【Flutter入门到进阶】Dart进阶篇---DartVM单线程设计原理
1 虚拟机的指令执行设计 1.1 虚拟机的分类 基于栈的虚拟机,比如JVM虚拟机 基于寄存器的虚拟机,比如Dalvik虚拟机 1.2 虚拟机的概念 首先问一个基本的问题,作为一个虚拟机,它最基本的要实现哪些功能? 他应该能够模拟…...
Dem和NvM(NVRAM Manager)的交集
NVRAM(NvM)提供了在NVRAM中存储数据Block的机制。 NVRAM Block(最大大小取决于配置)被分配给Dem,并由Dem实现事件状态信息和相关数据的永久存储(例如通电复位)。 ECU 状态管理器(Ec…...
AI神经网络CNN/RNN/DNN/SNN的区别对比
@版权声明: 本文由 ChatGpt 创作; BiliBili: https://www.bilibili.com/video/BV17D4y1P7pM/?share_source=copy_web&vd_source=6d217e0ff6387a749dc570aba51d36fd 引言 随着人工智能技术的发展,神经网络作为人工智能的核心技术之一,被广泛应用于图像识别、语音识别、…...

【JavaWeb】一文学会JPA
✅✅作者主页:🔗孙不坚1208的博客 🔥🔥精选专栏:🔗JavaWeb从入门到精通(持续更新中) 📋📋 本文摘要:本篇文章主要介绍JPA的概念、注解实现ORM规范…...

【安卓逆向】APK修改与反编译回编译
【安卓逆向】反编译修改APK回编译使用工具流程步骤Apktool相关安装与使用常用命令备查APK签名命令备查实战练习反编译查看修改的地方使用Apktool反编译得到产物文件夹并进行修改回编APK实用场景在日常开发我们可能需要替换某些资源或者修改某些代码,但是我们没有源码…...
【计组笔记04】计算机组成原理之多模块存储器、Cache高速缓存存储器、Cache地址映射
这篇文章,主要介绍计算机组成原理之多模块存储器、Cache高速缓存存储器、Cache地址映射。 目录 一、双口RAM和多模块存储器 1.1、存取周期 1.2、双口RAM 1.3、多模块存储器...
英语基础-状语的应用
1. 非谓语动词作状语 1. 试着翻译下列句子 当他是一个小孩子的时候,他很喜欢玩电脑游戏。 When he was a child, he liked playing computer games. 如果他通过考试,他妈妈就会给他买一台新电脑。 If he passes the examination, his mother will b…...
发表论文需要注意的两点(建议收藏)
在学习人工智能的过程中,论文有着重要的作用,无论是深入学术科研,还是毕业找工作,都离不开发表论文这一步骤,所以今天就和大家分享一些关于论文发表的经验,希望对大家有所帮助。 为什么要早点发表论文&…...
ISTQB-TM-大纲
1. 测试过程 1.1 简介 在 ISTQB 软件测试基础级认证大纲中已描述了基本的测试过程包括以下活动: 计划和控制分析和设计实施和执行评估出口准则和报告测试结束活动 基础级大纲认同这些活动虽然有逻辑顺序,但过程中的某些活动可能重叠,或并行…...

CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...

label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)
概述 在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过,在涉及到多个子类派生于基类进行多态模拟的场景下,…...

高频面试之3Zookeeper
高频面试之3Zookeeper 文章目录 高频面试之3Zookeeper3.1 常用命令3.2 选举机制3.3 Zookeeper符合法则中哪两个?3.4 Zookeeper脑裂3.5 Zookeeper用来干嘛了 3.1 常用命令 ls、get、create、delete、deleteall3.2 选举机制 半数机制(过半机制࿰…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现
摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序,以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务,提供稳定高效的数据处理与业务逻辑支持;利用 uniapp 实现跨平台前…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...

SiFli 52把Imagie图片,Font字体资源放在指定位置,编译成指定img.bin和font.bin的问题
分区配置 (ptab.json) img 属性介绍: img 属性指定分区存放的 image 名称,指定的 image 名称必须是当前工程生成的 binary 。 如果 binary 有多个文件,则以 proj_name:binary_name 格式指定文件名, proj_name 为工程 名&…...

面向无人机海岸带生态系统监测的语义分割基准数据集
描述:海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而,目前该领域仍面临一个挑战,即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...

Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...
uniapp 实现腾讯云IM群文件上传下载功能
UniApp 集成腾讯云IM实现群文件上传下载功能全攻略 一、功能背景与技术选型 在团队协作场景中,群文件共享是核心需求之一。本文将介绍如何基于腾讯云IMCOS,在uniapp中实现: 群内文件上传/下载文件元数据管理下载进度追踪跨平台文件预览 二…...