网站上地图是怎样做的,企业网站开发心得体会,网站改版的方式大致为,湖南火电建设有限公司招标网站0.对原教程的一些见解
其回顾完请求流程就是抽象了两个接口#xff0c;PeerPicker和PeerGetter。这样操作#xff0c;读者阅读时可能很难快速明白其含义#xff0c;不好理解为什么就创建出两个接口#xff0c;感觉会比较疑惑。原教程的评论中也有讨论这点。 本教程就先不创…0.对原教程的一些见解
其回顾完请求流程就是抽象了两个接口PeerPicker和PeerGetter。这样操作读者阅读时可能很难快速明白其含义不好理解为什么就创建出两个接口感觉会比较疑惑。原教程的评论中也有讨论这点。 本教程就先不创建接口而是使用struct方式这样可能好理解点。
1.节点请求处理的流程
先弄清楚我们查询缓存的逻辑。
单节点
客户发送查询请求到节点A该节点有缓存就立即返回若是没有就执行用户设置的回调函数获取值并添加到缓存中然后返回。
分布式节点
客户端发送查询请求到某个缓存节点该节点会判断该key是否在本地若是不在本地使用一致性哈希选择节点若不是在远程节点则就退回到本地节点处理若在远程节点该节点会发送请求去访问其他 node 节点。不是客户端再去访问其他节点
从这可以看出一个node要处理两种请求一个是来自客户端的外部请求一个是来自其他远端节点的内部请求。
为了清晰划分职责我们可以在一个node中启动两种HTTP服务一个处理客户端请求(APIServer), 一个处理节点之间的请求(CacheServer)。 2.HTTP客户端
之前我们为 HTTPPool 实现了服务端功能通信不仅需要服务端还需要客户端因此我们接下来先实现客户端的功能。这个客户端是节点作为客户端去访问其他节点。
baseURL 表示将要访问的远程节点的地址例如 http://example.com/geecache/。
type httpGetter struct {baseURL string
}func (h *httpGetter) Get(group string, key string) ([]byte, error) {//QueryEscape 对字符串进行转义以便可以将其安全地放置在 URL 查询中。u : fmt.Sprintf(%v/%v/%v, h.baseURL,url.QueryEscape(group),url.QueryEscape(key))res, err : http.Get(u)if err ! nil {return nil, err}defer res.Body.Close()if res.StatusCode ! http.StatusOK {return nil, fmt.Errorf(server returned: %v, res.Status)}bytes, err : io.ReadAll(res.Body)if err ! nil {return nil, fmt.Errorf(reading response body: %v, err)}return bytes, nil
}
3.回顾上一章节实现的单节点的访问流程
func (g *Group) Get(key string) (ByteView, error) {//现在本地查询if v, ok : g.mainCache.get(key); ok {return v, nil}return g.load(key)
}func (g *Group) load(key string) (ByteView, error) {bytes, err : g.getter.Get(key)if err ! nil {return ByteView{}, err}value : ByteView{b: cloneByte(bytes)}g.mainCache.add(key, value)return value, nil
}
那很明显是需要修改load方法让其可以去访问远程节点。
在load方法中伪代码如下。
func func (g *Group) load(key string) (ByteView, error){if 有远程节点 {if 找到key所在的远程节点 {本地作为客户端去访问该远程节点}}没有远程节点只能在本地调用回调函数去源地方获取
}要想在Group中访问节点那么就要在Group中存储节点集合。
节点结合结构体Peers
那节点集合是不是又要创建一个结构体那先试试创建一个结构体Peers。
因为 hash 环的 map 不是线程安全的,所以这里要加锁。
成员变量 httpGetters映射远程节点与对应的 httpGetter。httpGetter就是个客户端是一个节点作为客户端每一个远程节点对应一个 httpGetter因为 httpGetter 与远程节点的地址 baseURL 有关map的key是远程节点的地址比如http://localhost:10000
type Peers struct {addr string //这个是用于进行选择节点时用来判断是不是本地节点basePath stringmutex sync.Mutex //guards peersHashRing and httpGetterspeersHashRing *consistenthash.HashRinghttpGetters map[string]*httpGetter
}//这是HTTP服务端章节的HTTPPool,这是很相似的
type HTTPPool struct {addr stringbasePath string
}
那么该结构体Peers就要有添加远程节点和通过key去获取远程节点的方法。
增添远程节点方法Set
通过该方法可以知道其map的key是远程节点的地址。
// 使用用例Set(http://localhost:8001,http://localhost:8002)
func (p *Peers) Set(peers ...string) {p.mutex.Lock()defer p.mutex.Unlock()p.peersHashRing consistenthash.NewHash(50, nil)p.peersHashRing.Add(peers...) //在 hash 环上添加真实节点和虚拟节点//存储远端节点信息p.httpGetters make(map[string]*httpGetter)for _, peer : range peers {p.httpGetters[peer] httpGetter{baseURL: peer p.basePath}}
}
通过key去获取远程节点的方法PickPeer
Peers结构体中的变量addr在这里派上用场了返回的地址要是等于本身addr那就返回false,不用自己作为客户端再去访问自己。
func (p *Peers) PickPeer(key string) (*httpGetter, bool) {p.mutex.Lock()defer p.mutex.Unlock()//这里返回的peer是个地址可以查看(Peers).Set函数中的参数if peer : p.peersHashRing.Get(key); peer ! peer ! p.addr {fmt.Println(pick peer , peer)return p.httpGetters[peer], true}return httpGetter{}, false
}
Peers这个结构体就实现了可以看到其与HTTPPool是很相似的。对比HTTPPool,就是成员变量添加了一些方法也添加了一些也没有改变HTTPPool原有的逻辑只是扩张了。所以可以把Peers的内容添加到HTTPPool中去具体的代码就不在这里显示了。
type HTTPPool struct {addr stringbasePath string//新添加的把Peers内容增添到HTTPPool中mutex sync.MutexpeersHashRing *consistenthash.HashRinghttpGetters map[string]*httpGetter
}
4.集成实现主流程
最后我们需要将上述新增的功能集成在主流程(geecache.go)中。
在Group结构体中有改变。
新增 RegisterPeers() 方法将 peers 注入到 Group 中。
type Group struct {name stringmainCache cachegetter Getterpeers *Peers //添加了节点集合
}// 往分组内注册节点集合
func (g *Group) RegisterPeers(peers *Peers) {if g.peers ! nil {panic(RegisterPeerPicker called more than once)}g.peers peers
}
最终再回到load函数这个函数是需要修改的。
func (g *Group) load(key string) (value ByteView, err error) {if g.peers ! nil { //有远程节点的情况if peer, ok : g.peers.PickPeer(key); ok { //通过key找到该远程节点if value, err g.getFromPeer(peer, key); err nil {return value, nil //找到值}log.Println([GeeCache] Failed to get from peer, err)}}return g.getLocally(key) //回到本地处理
}func (g *Group) getFromPeer(peer *httpGetter, key string) (ByteView, error) {bytes, err : peer.Get(g.name, key)if err ! nil {return ByteView{}, err}return ByteView{b: bytes}, nil
}func (g *Group) getLocally(key string) (ByteView, error) {bytes, err : g.getter.Get(key)if err ! nil {return ByteView{}, err}value : ByteView{b: cloneByte(bytes)}g.mainCache.add(key, value)return value, nil
}
新增 getFromPeer() 方法使用httpGetter 访问远程节点获取缓存值。修改 load 方法使用 PickPeer() 方法选择节点若非本机节点则调用 getFromPeer() 从远程获取。若是本机节点或失败则回退到 getLocally()。
5. 测试
总结——缓存节点启动的流程
创建 Group 对象.(用于存储我们的缓存数据)启动缓存 http 服务.(创建 HTTPPool添加节点信息注册到缓存分组中)启动 API 服务.(用于与客户端进行交互) 测试代码
var db map[string]string{Tom: 630,Jack: 589,Sam: 567,
}func main() {var port intvar api boolflag.IntVar(port, port, 8001, Geecache server port)flag.BoolVar(api, api, false, Start a api server?)flag.Parse()apiAddr : http://localhost:9999addrMap : map[int]string{8001: http://localhost:8001,8002: http://localhost:8002,8003: http://localhost:8003,}var addrs []stringfor _, v : range addrMap {addrs append(addrs, v)}gee : createGroup()if api {go startAPIServer(apiAddr, gee)}startCacheServer(addrMap[port], addrs, gee)time.Sleep(time.Second * 1000)
}func createGroup() *cache.Group {return cache.NewGroup(scores, 210, cache.GetterFunc(func(key string) ([]byte, error) {if v, ok : db[key]; ok {return []byte(v), nil}return nil, fmt.Errorf(%s not exit, key)}))
}func startCacheServer(addr string, addrs []string, groups *cache.Group) {//HTTPPool是节点结合和HTTP服务端peers : cache.NewHTTPPool(addr, cache.DefaultBasePath)peers.Set(addrs...) //添加节点groups.RegisterPeers(peers) //注册节点集合log.Println(geecache is running at, addr)http.ListenAndServe(addr[7:], peers)
}func startAPIServer(apiAddr string, groups *cache.Group) {http.HandleFunc(/api, func(w http.ResponseWriter, r *http.Request) {key : r.URL.Query().Get(key)view, err : groups.Get(key)if err ! nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}w.Header().Set(Content-Type, application/octet-stream)w.Write(view.ByteSlice())})log.Println(fontend server is running at, apiAddr)http.ListenAndServe(apiAddr[7:], nil)
}
为了方便我们将启动的命令封装为一个 shell 脚本
我们开启了三个节点(都是在同一个台机器上的只是用不同端口来当做一个节点进行区分。
在端口8003的节点上开启APIServer用户去访问时候都是访问端口8003的那个节点。
#!/bin/bash#trap 命令用于在 shell 脚本退出时删掉临时文件结束在该shell脚本运行的后台程序
trap rm server;kill 0 EXITgo build -o server
./server -port8001
./server -port8002
./server -port8003 -api1 sleep 2
echo start test
curl http://localhost:9999/api?keyTom
curl http://localhost:9999/api?keyTom
curl http://localhost:9999/api?keyTom wait
结果
测试的时候我们并发了 3 个请求 ?keyTom从日志中可以看到三次均选择了节点 8001这是一致性哈希算法的功劳。 但是会有一个问题同时向 8001 发起了 3 次请求。试想假如有 10 万个在并发请求该数据呢那就会向 8001 同时发起 10 万次请求如果 8001 又同时向数据库发起 10 万次查询请求很容易导致缓存被击穿。
三次请求的结果是一致的对于相同的 key能不能只向 8001 发起一次请求这个问题下一次解决。
6.多节点的访问流程图 完整代码https://github.com/liwook/Go-projects/tree/main/go-cache/5-multi-nodes