day2 单机并发缓存
文章目录
- 1 sync.Mutex
- 2 支持并发读写
- 3 主体结构 Group
- 3.1 回调 Getter
- 3.2 Group 的定义
- 3.3 Group 的 Get 方法
- 4 测试
本文代码地址: https://gitee.com/lymgoforIT/gee-cache/tree/master/day2-single-node
本文是7天用Go从零实现分布式缓存GeeCache
的第二篇。
- 介绍
sync.Mutex
互斥锁的使用,并实现LRU
缓存的并发控制。 - 实现
GeeCache
核心数据结构Group
,缓存不存在时,调用回调函数获取源数据,代码约150
行
1 sync.Mutex
多个协程(goroutine
)同时读写同一个变量,在并发度较高的情况下,会发生冲突。确保一次只有一个协程(goroutine
)可以访问该变量以避免冲突,这称之为互斥,互斥锁可以解决这个问题。
sync.Mutex
是一个互斥锁,可以由不同的协程加锁和解锁。
sync.Mutex
是 Go
语言标准库提供的一个互斥锁,当一个协程(goroutine
)获得了这个锁的拥有权后,其它请求锁的协程(goroutine
) 就会阻塞在 Lock()
方法的调用上,直到调用 Unlock()
锁被释放。
接下来举一个简单的例子,假设有10
个并发的协程打印了同一个数字100
,为了避免重复打印,实现了printOnce(num int)
函数,使用集合 set
记录已打印过的数字,如果数字已打印过,则不再打印。
var set = make(map[int]bool, 0)func printOnce(num int) {if _, exist := set[num]; !exist {fmt.Println(num)}set[num] = true
}func main() {for i := 0; i < 10; i++ {go printOnce(100)}time.Sleep(time.Second)
}
我们运行 go run .
会发生什么情况呢?
$ go run .
100
100
有时候打印 2
次,有时候打印 4
次,有时候还会触发 panic
,因为对同一个数据结构map
的访问冲突了。接下来用互斥锁的Lock()
和Unlock()
方法将冲突的部分包裹起来:
var m sync.Mutex
var set = make(map[int]bool, 0)func printOnce(num int) {m.Lock()if _, exist := set[num]; !exist {fmt.Println(num)}set[num] = truem.Unlock()
}func main() {for i := 0; i < 10; i++ {go printOnce(100)}time.Sleep(time.Second)
}
$ go run .
100
相同的数字只会被打印一次。当一个协程调用了 Lock()
方法时,其他协程被阻塞了,直到Unlock()
调用将锁释放。因此被包裹部分的代码就能够避免冲突,实现互斥。
Unlock()
释放锁还有另外一种写法:
func printOnce(num int) {m.Lock()defer m.Unlock()if _, exist := set[num]; !exist {fmt.Println(num)}set[num] = true
}
2 支持并发读写
上一篇文章 GeeCache
第一天 实现了 LRU
缓存淘汰策略。接下来我们使用 sync.Mutex
封装 LRU
的几个方法,使之支持并发的读写。在这之前,我们抽象了一个只读数据结构 ByteView
用来表示缓存值,是 GeeCache
主要的数据结构之一。
day2-single-node/geecache/byteview.go
package geecachetype ByteView struct {b []byte
}// 实现Value接口,从而可以作为lru缓存entry中的value值
func (v ByteView) Len() int {return len(v.b)
}func (v ByteView) ByteSlice() []byte {return cloneBytes(v.b)
}func (v ByteView) String() string {return string(v.b)
}// 用于复制一个切片返回出去,避免外部直接修改了缓存中的[]byte内容
func cloneBytes(b []byte) []byte {c := make([]byte, len(b))copy(c, b)return c
}
ByteView
只有一个数据成员,b []byte
,b
将会存储真实的缓存值。选择byte
类型是为了能够支持任意的数据类型的存储,例如字符串、图片等。- 实现
Len() int
方法,我们在lru.Cache
的实现中,要求被缓存对象必须实现Value
接口,即Len() int
方法,返回其所占的内存大小。 b
是只读的,使用ByteSlice()
方法返回一个拷贝,防止缓存值被外部程序修改。
接下来就可以为 lru.Cache
添加并发特性了。
day2-single-node/geecache/cache.go
package geecacheimport ("geecache/lru""sync"
)type cache struct {mu sync.Mutexlru *lru.CachecacheBytes int64
}func (c *cache) add(key string, value ByteView) {c.mu.Lock()defer c.mu.Unlock()if c.lru == nil {c.lru = lru.New(c.cacheBytes, nil)}// 底层实际办事的还是委托给了lruc.lru.Add(key, value)
}func (c *cache) get(key string) (value ByteView, ok bool) {c.mu.Lock()defer c.mu.Unlock()if c.lru == nil {return}if v, ok := c.lru.Get(key); ok {return v.(ByteView), ok}return
}
cache.go
的实现非常简单,实例化lru
,封装get
和add
方法(底层实际是委托的lru
在工作),并添加互斥锁mu
。- 在
add
方法中,判断了c.lru
是否为nil
,如果等于nil
再创建实例。这种方法称之为延迟初始化(Lazy Initialization
),一个对象的延迟初始化意味着该对象的创建将会延迟至第一次使用该对象时。主要用于提高性能,并减少程序内存要求。
3 主体结构 Group
Group
是 GeeCache
最核心的数据结构,负责与用户的交互,并且控制缓存值存储和获取的流程。
我们将在 geecache.go
中实现主体结构 Group
,那么 GeeCache
的代码结构的雏形已经形成了。
geecache/|--lru/|--lru.go // lru 缓存淘汰策略|--byteview.go // 缓存值的抽象与封装|--cache.go // 并发控制|--geecache.go // 负责与外部交互,控制缓存存储和获取的主流程
接下来我们将实现流程⑴
和 ⑶
,远程交互的部分后续再实现。
3.1 回调 Getter
我们思考一下,如果缓存不存在,应从数据源(文件,数据库等)获取数据并添加到缓存中。GeeCache
是否应该支持多种数据源的配置呢?不应该,一是数据源的种类太多,没办法一一实现;二是扩展性不好。如何从源头获取数据,应该是用户决定的事情,我们就把这件事交给用户好了。因此,我们设计了一个回调函数(callback
),在缓存不存在时,调用这个函数,得到源数据。
day2-single-node/geecache/geecache.go
// A Getter loads data for a key.
type Getter interface {Get(key string) ([]byte, error)
}// A GetterFunc implements Getter with a function.
// 接口型函数
type GetterFunc func(key string) ([]byte, error)// Get implements Getter interface function
func (f GetterFunc) Get(key string) ([]byte, error) {return f(key)
}
- 定义接口
Getter
和 回调函数Get(key string)([]byte, error)
,参数是key
,返回值是[]byte
。 - 定义函数类型
GetterFunc
,并实现Getter
接口的Get
方法。 - 函数类型实现某一个接口,称之为接口型函数,方便使用者在调用时既能够传入函数作为参数,也能够传入实现了该接口的结构体作为参数。
了解接口型函数的使用场景,可以参考 day2加餐 Go 接口型函数的使用场景
我们可以写一个测试用例来保证回调函数能够正常工作。
func TestGetter(t *testing.T) {var f Getter = GetterFunc(func(key string) ([]byte, error) {return []byte(key), nil})expect := []byte("key")if v, _ := f.Get("key"); !reflect.DeepEqual(v, expect) {t.Errorf("callback failed")}
}
在这个测试用例中,我们借助 GetterFunc
的类型转换,将一个匿名回调函数转换成了接口 f Getter
。
调用该接口的方法 f.Get(key string)
,实际上就是在调用匿名回调函数。
定义一个函数类型 F,并且实现接口 A 的方法,然后在这个方法中调用自己。这是 Go 语言中将其他函数(参数返回值定义与 F 一致)转换为接口 A 的常用技巧。从而方便使用者在调用时既能够传入函数作为参数,也能够传入实现了该接口的结构体作为参数
3.2 Group 的定义
接下来是最核心数据结构 Group
的定义:
day2-single-node/geecache/geecache.go
// A Group is a cache namespace and associated data loaded spread over
type Group struct {name stringgetter GettermainCache cache
}var (mu sync.RWMutex // 控制groups的并发安全groups = make(map[string]*Group)
)// NewGroup create a new instance of Group
func NewGroup(name string, cacheBytes int64, getter Getter) *Group {if getter == nil {panic("nil Getter")}mu.Lock()defer mu.Unlock()g := &Group{name: name,getter: getter,mainCache: cache{cacheBytes: cacheBytes},}groups[name] = greturn g
}// GetGroup returns the named group previously created with NewGroup, or
// nil if there's no such group.
func GetGroup(name string) *Group {mu.RLock()g := groups[name]mu.RUnlock()return g
}
- 一个
Group
可以认为是一个缓存的命名空间,每个Group
拥有一个唯一的名称name
。比如可以创建三个Group
,缓存学生的成绩命名为scores
,缓存学生信息的命名为info
,缓存学生课程的命名为courses
。 - 第二个属性是
getter Getter
,即缓存未命中时获取源数据的回调(callback
)。 - 第三个属性是
mainCache cache
,即一开始实现的并发缓存。 - 构建函数
NewGroup
用来实例化Group
,并且将group
存储在全局变量groups
中。 GetGroup
用来特定名称的Group
,这里使用了只读锁RLock()
,因为不涉及任何冲突变量的写操作。
3.3 Group 的 Get 方法
接下来是 GeeCache
最为核心的方法 Get
:
// Get value for a key from cache
func (g *Group) Get(key string) (ByteView, error) {if key == "" {return ByteView{}, fmt.Errorf("key is required")}// Group主要是与用户交互的,底层实际做事的是mainCache(cache),而cache底层实际做事的是lru.Cacheif v, ok := g.mainCache.get(key); ok {log.Println("[GeeCache] hit")return v, nil}return g.load(key)
}func (g *Group) load(key string) (value ByteView, err error) {return g.getLocally(key)
}func (g *Group) getLocally(key string) (ByteView, error) {bytes, err := g.getter.Get(key)if err != nil {return ByteView{}, err}value := ByteView{b: cloneBytes(bytes)}g.populateCache(key, value)return value, nil
}func (g *Group) populateCache(key string, value ByteView) {g.mainCache.add(key, value)
}
Get 方法实现了上述所说的流程 ⑴ 和 ⑶:
- 流程
⑴
:从mainCache
中查找缓存,如果存在则返回缓存值。
流程⑶
:缓存不存在,则调用load
方法,load
调用getLocally
(分布式场景下会调用getFromPeer
从其他节点获取),getLocally
调用用户回调函数g.getter.Get()
获取源数据,并且将源数据添加到缓存mainCache
中(通过populateCache
方法)
至此,这一章节的单机并发缓存就已经完成了。
4 测试
可以写测试用例,也可以写 main
函数来测试这一章节实现的功能。那我们通过测试用例来看一下,如何使用我们实现的单机并发缓存吧。
首先,用一个 map
模拟耗时的数据库。
var db = map[string]string{"Tom": "630","Jack": "589","Sam": "567",
}
创建 group
实例,并测试 Get
方法
func TestGet(t *testing.T) {loadCounts := make(map[string]int, len(db))gee := NewGroup("scores", 2<<10, GetterFunc(func(key string) ([]byte, error) {log.Println("[SlowDB] search key", key)if v, ok := db[key]; ok {if _, ok := loadCounts[key]; !ok {loadCounts[key] = 0}loadCounts[key] += 1return []byte(v), nil}return nil, fmt.Errorf("%s not exist", key)}))for k, v := range db {if view, err := gee.Get(k); err != nil || view.String() != v {t.Fatal("failed to get value of Tom")} // load from callback functionif _, err := gee.Get(k); err != nil || loadCounts[k] > 1 {t.Fatalf("cache %s miss", k)} // cache hit}if view, err := gee.Get("unknown"); err == nil {t.Fatalf("the value of unknow should be empty, but %s got", view)}
}
在这个测试用例中,我们主要测试了 2
种情况
1)在缓存为空的情况下,能够通过回调函数获取到源数据。
2)在缓存已经存在的情况下,是否直接从缓存中获取,为了实现这一点,使用 loadCounts
统计某个键调用回调函数的次数,如果次数大于1
,则表示调用了多次回调函数,没有缓存。
测试结果如下:
$ go test -run TestGet
=== RUN TestGet
2024/07/20 14:33:58 [SlowDB] search key Tom
2024/07/20 14:33:58 [GeeCache] hit
2024/07/20 14:33:58 [SlowDB] search key Jack
2024/07/20 14:33:58 [GeeCache] hit
2024/07/20 14:33:58 [SlowDB] search key Sam
2024/07/20 14:33:58 [GeeCache] hit
2024/07/20 14:33:58 [SlowDB] search key unknown
--- PASS: TestGet (0.00s)
PASS
可以很清晰地看到,缓存为空时,调用了回调函数,第二次访问时,则直接从缓存中读取。
原文地址:https://geektutu.com/post/geecache-day2.html
相关文章:
day2 单机并发缓存
文章目录 1 sync.Mutex2 支持并发读写3 主体结构 Group3.1 回调 Getter3.2 Group 的定义3.3 Group 的 Get 方法 4 测试 本文代码地址: https://gitee.com/lymgoforIT/gee-cache/tree/master/day2-single-node 本文是7天用Go从零实现分布式缓存GeeCache的第二篇。 …...
ECMP等价多路由机制,大模型训练负载均衡流量极化冲突原因,万卡(大规模)集群语言模型(LLM)训练流量拥塞特点
大规模集群,大语言模型(LLM)训练流量特点,ECMP(Equal-Cost Multi-Path Routing)流量极化拥塞原因。 视频分享在这: 2.1 ECMP等价多路由,大模型训练流量特点,拥塞冲突极化产生原因_哔哩哔哩_bi…...
Linux 注意事项
Linux 与 Windows 是两个相互独立的操作系统,两者有较大差距: 1.1 Linux 严格区分大小写(Windows不严格区分大小写); 1.2 Linux 中所有内容,硬件设备都以文件形式保存在 /dev 目录下(万物皆文件…...
力扣SQL50 指定日期的产品价格 双重子查询 coalesce
Problem: 1164. 指定日期的产品价格 coalesce 的使用 简洁版 👨🏫 参考题解 select distinct p1.product_id,coalesce((select p2.new_pricefrom Products p2where p2.product_id p1.product_id and p2.change_date < 2019-08-16order by p2.…...
MySQL8的备份方案——全量(完全)备份(CentOS)
MySQL8的全量备份 一、安装备份工具二、备份数据三、恢复备份 点击跳转增量备份 点击跳转差异备份 点击跳转压缩备份 一、安装备份工具 官网 下载地址 备份所用工具为percona-xtrabackup 如果下方安装工具的教程失效,请点击上方下载地址转到官方文档查看 下载该工…...
JVM监控及诊断工具-命令行篇--jcmd命令介绍
JVM监控及诊断工具-命令行篇5-jcmd:多功能命令行 一 基本情况二 基本语法jcmd -ljcmd pid helpjcmd pid 具体命令 一 基本情况 在JDK 1.7以后,新增了一个命令行工具jcmd。它是一个多功能的工具,可以用来实现前面除了jstat之外所有命令的功能…...
c++信号和槽机制的轻量级实现,sigslot 库介绍及使用
Qt中的信号与槽机制很好用,然而只在Qt环境中。在现代 C 编程中,对象间的通信是一个核心问题。为了解决这个问题,许多库提供了信号和槽(Signals and Slots)机制。今天推荐分享一个轻量级的实现:sigslot 库。…...
云原生项目纪事系列 - 项目管理的鲜活事例
大规模云原生系统的新颖性、建设性和挑战性,吸引着许多有数学思想、哲学意识和美学观念的系统架构师,老模也是其中一员。 老模即是文史家庭出身,又有理工学业背景,他基于平时记录的翔实细节,秉持客观原则,使…...
【Vite】快速入门及其配置
概述 Vite是前端构建工具。vite 相较于webpack,vite采用了不同的运行方式: 开发时,并不对代码打包,而是直接采用ESM的方式来运行项目在项目打包部署时,使用 rollup 对项目进行打包除了速度外,vite使用起来也更加方便…...
Armv8/Armv9架构的学习大纲-学习方法-自学路线-付费学习路线
本文给大家列出了Arm架构的学习大纲、学习方法、自学路线、付费学习路线。有兴趣的可以关注,希望对您有帮助。 如果大家有需要的,欢迎关注我的CSDN课程:https://edu.csdn.net/lecturer/6964 ARM 64位架构介绍 ARM 64位架构介绍 ARM架构概况…...
vue 中 ui 组件二次封装后 ref 怎么穿透到子组件里
情景:element-ui 二次封装了 el-table 组件,使用封装组件时,想要调用 el-table 组件内置的一些方法。只在封装组件上定义 ref 是拿不到 el-table 内置方法的。解决方法如下。 1. vue2 封装组件 <template><el-table ref"inn…...
sourcetree中常用功能使用方法及gitlab冲突解决
添加至缓存:等于git add 提交:等于git commit 拉取/获取:等于git pull ,在每次要新增代码或者提交代码前需要先拉取一遍服务器中最新的代码,防止服务器有其他人更新了代码,但我们自己本地的代码在我们更新前跟服务器不…...
SQL Server分布式查询:跨数据库的无缝数据探索
SQL Server分布式查询:跨数据库的无缝数据探索 在当今的企业环境中,数据往往分散在不同的数据库和服务器上。SQL Server的分布式查询功能提供了一种强大的手段,允许用户编写单一的查询来访问和操作分散在不同SQL Server实例中的数据。本文将…...
【字少图多剖析微服务】深入理解Eureka核心原理
深入理解Eureka核心原理 Eureka整体设计Eureka服务端启动Eureka三级缓存Eureka客户端启动 Eureka整体设计 Eureka是一个经典的注册中心,通过http接收客户端的服务发现和服务注册请求,使用内存注册表保存客户端注册上来的实例信息。 Eureka服务端接收的…...
如何在 Linux 中解压 ZIP 文件
ZIP 是一种常用的压缩文件格式,用于存储和传输多个文件。在 Linux 系统中,解压 ZIP 文件非常简单。 使用 unzip 命令 unzip 是一个专用于解压 ZIP 文件的命令行工具。要使用它,请打开终端并输入以下命令: 例如,要解…...
IDEA的APIPost接口测试插件详解
APIPOST官方网址 一、安装APIPost插件 打开IntelliJ IDEA: 启动您的IntelliJ IDEA开发环境。 导航到插件设置: 在Windows或Linux上,点击 File > Settings。在macOS上,点击 IntelliJ IDEA > Preferences。 搜索并安装APIPo…...
[经验] 驰这个汉字的拼音是什么 #学习方法#其他#媒体
驰这个汉字的拼音是什么 驰,是一个常见的汉字,其拼音为“ch”,音调为第四声。它既可以表示动词,也可以表示形容词或副词,意义广泛,经常出现在生活和工作中。下面就让我们一起来了解一下“驰”的含义和用法。…...
生成式人工智能落地校园与课堂的15个场景
生成式人工智能正在重塑教育行业,为传统教学模式带来了革命性的变化。随着AI的不断演进,更多令人兴奋的应用场景将逐一显现,为学生提供更加丰富和多元的学习体验。 尽管AI在教学中的应用越来越广泛,但教师们也不必担心会被完全替代…...
C# 中的事件
1.事件的概念 在C#中,事件是一种特殊的委托类型,用于在对象之间提供一种基于观察者模式的通知机制。事件的发送方定义了一个委托,委托类型的声明包含了事件的签名,即事件处理器方法的签名。事件的订阅者可以通过运算符来注册事件…...
一、单例模式
文章目录 1 基本介绍2 实现方式2.1 饿汉式2.1.1 代码2.1.2 特性 2.2 懒汉式 ( 线程不安全 )2.2.1 代码2.2.2 特性 2.3 懒汉式 ( 线程安全 )2.3.1 代码2.3.2 特性 2.4 双重检查2.4.1 代码2.4.2 特性 2.5 静态内部类2.5.1 代码2.5.2 特性 2.6 枚举2.6.1 代码2.6.2 特性 3 实现的要…...
B树:高效的数据存储结构
在计算机科学中,B树(B-Tree)是一种平衡多路查找树,它广泛应用于数据库和文件系统等需要高效数据存储和检索的场景。B树的设计旨在优化磁盘I/O操作,通过减少磁盘访问次数来提高数据检索的效率。本文将介绍B树的基本概念…...
[Vulnhub] TORMENT IRC+FTP+CUPS+SMTP+apache配置文件权限提升+pkexec权限提升
信息收集 IP AddressOpening Ports192.168.101.152TCP:21,22,25,80,111,139,143,445,631 $ nmap -p- 192.168.101.152 --min-rate 1000 -sC -sV PORT STATE SERVICE VERSION 21/tcp open ftp vsftpd 2.0.8 or later | ftp-anon: Anonymous FTP login a…...
<数据集>安全帽佩戴识别数据集<目标检测>
数据集格式:VOCYOLO格式 图片数量:3912张 图片分辨率:640640 标注数量(xml文件个数):3912 标注数量(txt文件个数):3912 标注类别数:2 标注类别名称:[no-helmet, helmet] 序号类别名称图片…...
[米联客-安路飞龙DR1-FPSOC] FPGA基础篇连载-21 VTC视频时序控制器设计
软件版本:Anlogic -TD5.9.1-DR1_ES1.1 操作系统:WIN10 64bit 硬件平台:适用安路(Anlogic)FPGA 实验平台:米联客-MLK-L1-CZ06-DR1M90G开发板 板卡获取平台:https://milianke.tmall.com/ 登录“米联客”FPGA社区 ht…...
记录uni-app横屏项目:自定义弹出框
目录 前言: 正文: 前言:横屏的尺寸问题 最近使用了uniapp写了一个横屏的微信小程序和H5的项目,也是本人首次写的横屏项目,多少是有点踩坑不太适应。。。 先说最让我一脸懵的点,尺寸大小,下面一…...
Linux Vim教程(二):基本命令和操作
目录 1. 进入和退出Vim 1.1 启动Vim 1.2 退出Vim 2. 模式切换 2.1 切换到插入模式 2.2 切换到普通模式 2.3 切换到命令模式 2.4 切换到可视模式 3. 移动光标 4. 编辑文本 4.1 插入和追加文本 4.2 删除文本 4.3 复制和粘贴文本 4.4 撤销和重做 5. 搜索和替换 5.…...
【大模型基础】4.1 数据挖掘(待)
一、什么是文本挖掘? 文本挖掘指的是从文本数据中获取有价值的信息和知识,它是数据挖掘中的一种方法。文本挖掘中最重要最基本的应用是实现文本的分类和聚类,前者是有监督的挖掘算法,后者是无监督的挖掘算法。 二、文本挖掘的作用是什么? 能够从文本数据中获取有价值的…...
Jupyter Notebook与机器学习:使用Scikit-Learn构建模型
Jupyter Notebook与机器学习:使用Scikit-Learn构建模型 介绍 Jupyter Notebook是一款强大的交互式开发环境,广泛应用于数据科学和机器学习领域。Scikit-Learn是一个流行的Python机器学习库,提供了简单高效的工具用于数据挖掘和数据分析。本…...
IMU提升相机清晰度
近期,一项来自北京理工大学和北京师范大学的团队公布了一项创新性的研究成果,他们将惯性测量单元(IMU)和图像处理算法相结合,显著提升了非均匀相机抖动下图像去模糊的准确性。 研究团队利用IMU捕捉相机的运动数据&…...
掌握SQL Server性能监控:自定义性能计数器的实现
掌握SQL Server性能监控:自定义性能计数器的实现 在数据库管理中,监控数据库性能是确保系统稳定运行的关键。SQL Server提供了丰富的性能监控工具,但有时这些工具可能无法满足特定的监控需求。这时,自定义性能计数器就显得尤为重…...
有经验的合肥网站建设/软件公司
XSS的原理分析与解剖 001 前言: 《xss攻击手法》一开始在互联网上资料并不多(都是现成的代码,没有从基础的开始),直到刺的《白帽子讲WEB安全》和cn4rry的《XSS跨站脚本攻击剖析与防御》才开始好转。 我这里就不说什么xss的历史什么东西了,xss…...
二手书网站建设日程表/天津seo托管
关键词 并行开发 代码复用 关注点分离经典的MVC架构模式MVC架构模式是经典设计模式中的经典,是一种编程的方法论。具有高度抽象的特征,经典MVC用简单的定义体现出解决复杂通用问题的办法,只有不断思考和体会才能用来解决不同情况下程序设计所…...
广西建设培训中心网站/软文自助发布平台系统
今天在用nobody跑 脚本时,报错:登录root su apache 报错: su: cannot set user id: 资源暂时不可用 以前也遇到过这种问题,apache账号突然不可用。当时找的原因是系统进程太多,kill几个进程就可以了。 根本原因是&…...
肇庆专业网站建设服务/爱站网关键词查询工具
在微信开放平台基础高级产品经理林兴演讲的当场,他爆料了微信小程序一个轰动性新能力:小程序很快可以支持各个 App 直接打开小程序!没错,你没有听错,简单来说,在不久以后,所有的 App 里面都可以…...
网站排名优化效果/seo的搜索排名影响因素有哪些
1. Peek View 可以在不新建TAB的情况下快速查看、编辑一个函数的代码。 用法:在光标移至某个函数下,按下altF12。 然后在Peek窗口里可以继续按altF12。然后按ctrlalt-,或者ctrlalt就可以前后跳转。按ESC关闭Peek窗体。 这下就不需要来回跳转了…...
广东品牌网站建设服务机构/上海专业的网络推广
初始学习如下: http://rdf4j.org/sesame/tutorials/getting-started.docbook?view 转载于:https://www.cnblogs.com/aze-003/p/4078492.html...