Galxe - 二面 - 杭州 面经 已oc

(一个小时二十分钟)

1. 介绍一下最近三个月做的事情。

2. 对于 rocksdb 了解吗?(参与的开源项目中用到了)

3. 讲讲在这个项目中的用法。(对实现不太了解,从设计相关的角度讲了一下)

4. 假设你有一个集群,有一批大量的 kv(key 很大(无规律),value很小)需要写入,你想想这里对读/写分别怎么优化?(大概从集群结构,然后梳理到单节点,之后讨论读写优化,讲异步写的时候提到了 WAL)

5. 项目中 WAL 怎么用的?(拉了一个具体的实现场景出来讲,ZSet SkipList优化实现相关,讲了流程)

6. 集群分片这里怎么实现?(上一轮面试和另一个面试官聊过,大概讲了一下)

7. 我看你这里有做集群的扩缩容,讲讲怎么做的。(本节点存一份连接信息,新增Voter,然后广播出去)

8. 我看你这里有一些内存泄漏的排查经验,可以讲讲平时怎么排查吗?(还是拉了一件具体做过的事情讲了整个排查到确认问题的过程)

9. 我看你这里Golang比较熟悉,做个题吧。(并发安全的LRUCache)

遇到两次了,贴个代码

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"unsafe"
)

type Node struct {
	key  string
	val  string
	next *Node
	prev *Node
}

type LRUCache struct {
	capacity int
	size atomic.Int64
	cache sync.Map // key -> *Node
	head  *Node
	tail  *Node
}

func NewLRUCache(capacity int) *LRUCache {
	lru := &LRUCache{
		capacity: capacity,
		head:     &Node{},
		tail:     &Node{},
	}

	lru.head.next = lru.tail
	lru.tail.prev = lru.head

	return lru
}

func (c *LRUCache) Get(key string) (string, bool) {
	if node, ok := c.cache.Load(key); ok {
		c.moveToHead(node.(*Node))
		return node.(*Node).val, true
	}

	return "", false
}

func (c *LRUCache) Put(key string, value string) {
	if node, ok := c.cache.Load(key); ok {
		node.(*Node).val = value
		c.moveToHead(node.(*Node))
		return
	}

	newNode := &Node{key: key, val: value}
	c.cache.Store(key, newNode)
	c.addToHead(newNode)
	c.size.Add(1)

	if c.size.Load() > int64(c.capacity) {
		removed := c.removeTail()
		c.cache.Delete(removed.key)
		c.size.Add(-1)
	}
}

func (c *LRUCache) addToHead(n *Node) {
	atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&n.prev)), unsafe.Pointer(n.prev), unsafe.Pointer(c.head))
	atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&n.next)), unsafe.Pointer(n.next), unsafe.Pointer(c.head.next))
	atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.head.next.prev)), unsafe.Pointer(c.head.next.prev), unsafe.Pointer(n))
	atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.head.next)), unsafe.Pointer(c.head.next), unsafe.Pointer(n))
}

func (c *LRUCache) moveToHead(n *Node) {
	c.removeNode(n)
	c.addToHead(n)
}

func (c *LRUCache) removeNode(n *Node) {
	atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&n.prev.next)), unsafe.Pointer(n.prev.next), unsafe.Pointer(n.next))
	atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&n.next.prev)), unsafe.Pointer(n.next.prev), unsafe.Pointer(n.prev))
}

func (c *LRUCache) removeTail() *Node {
	tail := c.tail.prev
	c.removeNode(tail)
	return tail
}

func main() {
	// 创建一个容量为 2 的 LRUCache
	c := NewLRUCache(2)

	// 放入 k1, k2
	c.Put("k1", "v1")
	c.Put("k2", "v2")

	// 再次获取 k1
	if val, ok := c.Get("k1"); ok {
		fmt.Println("Get k1:", val) // 期望输出 "v1"
	} else {
		fmt.Println("k1 不存在")
	}

	// 插入 k3,此时容量超出,应当淘汰最久未使用的元素 (k2 或 k1)
	c.Put("k3", "v3")

	// 检查 k2 是否被淘汰
	if val, ok := c.Get("k2"); ok {
		fmt.Println("Get k2:", val)
	} else {
		fmt.Println("k2 不存在,已经被淘汰")
	}

	// 继续查看 k3
	if val, ok := c.Get("k3"); ok {
		fmt.Println("Get k3:", val) // 期望输出 "v3"
	} else {
		fmt.Println("k3 不存在")
	}
}

10. 继续做题。(假设在区块链中每次交易是一个最小事务,我现在需要保证并发进行的结果和串行的结果是一致的,怎么实现)

结构定义:

    type Transaction struct {
    	ID string
    	WriteSet map[string]string // 准备写入的 key->newValue
    }

测试数据:

txs := []Transaction{
    		{
    			ID: "Tx1",
    			WriteSet: map[string]string{
    				"A": "100",
    				"B": "200",
    			},
    		},
    		{
    			ID: "Tx2",
    			WriteSet: map[string]string{
    				"C": "300",
    				"B": "200",
    			},
    		},
    		{
    			ID: "Tx3",
    			WriteSet: map[string]string{
    				"A": "200",
    			},
    		},
    	}

大致意思就是现在串行的结果是 A = 200, B = 200, C = 300

需要修改为并发,并且并发执行的结果要和串行的一致。

思路:注意到 ID 的性质,可以根据 ID 确认哪个值更新,考虑类似多版本的思想,将 ID 作为结果的版本,并发执行时保存所有的版本,最终结果即每个key最新版本的值。

维护版本使用大根堆即可。如下:

 	type TxPair struct {
    	ID    string
    	Value string
    }
    
    type TxPairHeap []TxPair
    
    func (h TxPairHeap) Len() int { return len(h) }
    
    func (h TxPairHeap) Less(i, j int) bool { return h[i].ID > h[j].ID }
    
    func (h TxPairHeap) Swap(i, j int) {
    	h[i], h[j] = h[j], h[i]
    }
    
    func (h *TxPairHeap) Push(x interface{}) {
    	*h = append(*h, x.(TxPair))
    }
    
    func (h *TxPairHeap) Pop() interface{} {
    	old := *h
    	n := len(old)
    	x := old[n-1]
    	*h = old[0 : n-1]
    	return x
    }


剩下的 goroutine + WaitGroup,不断Push到堆就行

	func main() {
    	txs := []Transaction{
    		{
    			ID: "Tx1",
    			WriteSet: map[string]string{
    				"A": "100",
    				"B": "200",
    			},
    		},
    		{
    			ID: "Tx2",
    			WriteSet: map[string]string{
    				"C": "300",
    				"B": "200",
    			},
    		},
    		{
    			ID: "Tx3",
    			WriteSet: map[string]string{
    				"A": "200",
    			},
    		},
    	}
    
    	res := make(map[string]*TxPairHeap)
    
    	var wg sync.WaitGroup
    	mu := &sync.Mutex{}
    
    	for _, tx := range txs {
    		wg.Add(1)
    		go func(t Transaction) {
    			defer wg.Done()
    
    			mu.Lock()
    			defer mu.Unlock()
    
    			txID := t.ID
    			for k, v := range t.WriteSet {
    				var pairHeap *TxPairHeap
    				if _, ok := res[k]; ok {
    					pairHeap = res[k]
    				} else {
    					pairHeap = &TxPairHeap{}
    					res[k] = pairHeap
    					heap.Init(pairHeap)
    				}
    
    				pairHeap.Push(TxPair{ID: txID, Value: v})
    			}
    		}(tx)
    	}
    
    	wg.Wait()
    
    	for k, hp := range res {
    		fmt.Println("Key = ", k, "Value = ", hp.Pop().(TxPair).Value)
    	}
    }

11. 这里如果我每个交易都需要一个前提条件的完成后才能进行,怎么办,讲讲思路。(引入拓扑,维护拓扑性质的前提下维护堆性质)

---

结束。

有些问题忘了(

---

2.24 update: oc

#面经#
全部评论
记录一下:和群友聊天的时候才发现 lru 并发有点问题(,还是得加大锁,优化的话考虑分桶
1 回复 分享
发布于 02-22 23:36 宁夏
太强了,佬
点赞 回复 分享
发布于 03-14 14:54 湖南
都是本地idea写的吗
点赞 回复 分享
发布于 02-25 10:32 香港

相关推荐

1)手撕:给定字符串,求不含重复字符的最长子串长度,并打印这个子串//哈希Set配合双指针private static String findLongestSubstring(String s) {        int n = s.length();        int left = 0;        int maxLength = 0;        String longestSubstring = "";        Set<Character> charSet = new HashSet<>();        for(int right = 0 ; right < n ; right ++){            while(charSet.contains(s.charAt(right))){                charSet.remove(s.charAt(left));                left++;            }            charSet.add(s.charAt(right));            if(maxLength < right - left + 1){                maxLength = right - left + 1;                longestSubstring = s.substring(left , right + 1);            }        }        return longestSubstring;    }2)如何设计一个秒杀系统?从以下角度考虑:1.高性能架构;采用分布式架构,消息队列来削峰填谷,服务的降级和熔断 2.高并发的处理能力:商品库存扣减的多线程安全问题,采用redisson分布式锁,缓存预热3.用户体验升级:websocket实现秒杀倒计时同步,消息队列实现秒杀结果实时反馈,针对ip地址,设备指纹和访问频率的限制实现防作弊系统4.数据一致性保障;数据库分库分表,本地消息表5.监控报警:监控系统,报警系统,日志系统,异常日志收集,分布式追踪系统6.安全防护、成本控制3)String StringBuffer StringBuilder区别String是不可变类,线程安全,每次修改字符串都会创建新的字符串,效率比较低StringBuffer是可变类,直接在原字符串上修改,使用了Synchronized实现同步,效率也比较低,适合多线程场景StringBuilder是可变类,线程不安全,效率比较高,适合单线程场景4)数据库字段char和varchar区别char:定长字符串,存储长度为1~255个字符,存储空间固定为255字节,不足用空格补,适合固定长度的字段,便于数据库读取和优化varchar:可变字符串,存储长度为1~65535个字符,存储空间为实际长度+长度字节5)索引失效的情况索引失效是指数据库在查询过程中无法有效利用已建立的索引,导致查询性能下降,甚至退化为全表扫描的情况。查询条件中使用了函数或表达式对索引列进行操作;使用了OR条件且未对所有分支列建立索查询条件中使用了NOT、<>、!=等否定操作符;对索引列进行了模糊查询(如LIKE '%abc%'),且通配符位于开头;查询条件中列的顺序与复合索引的列顺序不匹配;或者查询时数据类型不匹配导致索引无法使用。6)数据库的事务隔离级别读未提交:允许读取尚未提交的数据,可能导致脏读、幻读、不可重复读读已提交:允许读取已提交的数据,不能保证数据一致,可能导致幻读和不可重复读可重复读:允许读取已提交数据,可能导致幻读串行化:保证数据一致性,但是并发度和性能低7)Redis的常用数据类型,分别存储哪些东西?String:存储字符串,比如用户名、密码和验证码等哈希:哈希表,可以存储用户信息,商品信息等List:存储有序的元素,比如消息队列和日志记录Set:集合,可以做去重排序或求交集等Zset:带得分排序的集合,可以做用户或者流量等的排行榜8)Redis的锁机制基于SETNX命令,将锁名称作为键,客户端唯一标识(UUID)作为键值,使用完锁后DEL释放锁    因不可冲入可能存在死锁和不及时释放锁的情况,可以释放锁时检查锁值是否为自己的UUID以及添加过期时间基于Lua脚本,使用原子SET命令和Lua脚本的事务性,但仍存在锁续期困难和业务超时锁释放风险基于Redisson的分布式锁,支持可冲入锁和自动续期,提供公平锁、联锁和红锁9)HTTP1.0 2.0 3.0 区别HTTP1.0:默认为短连接,每次请求都需要建立TCP连接,并通过Connection: keep-alive头来实现持久连接,不支持管道    化,主要使用If-Modified-Since/Expires来做为缓存判断的标准;HTTP2.0:采用二进制格式而非文本格式,解析更加高效,支持多路复用允许单个TCP交错发送多个请求和响应,引入HPA    CK压缩算法,对请求和响应的头部信息进行压缩,消除冗余,允许客户端为请求设置优先级HTTP3.0: 最新的HTTP协议,基于QUIC协议,QUIC使用udp传输数据,不存在队头阻塞问题,首次连接后具备0RTT优        势,减少延迟,允许网络切换时,将连接迁移到新的IP地址,默认采用TLS加密,保证数据传输的安全性10) TCP的三次握手和四次挥手,为什么需要?三次握手:客户端向服务器发送SYN表示请求同步,服务器向客户端发送SYN+ACK表示确认收到同步请求,可以确保客户    端的发送能力正常,客户端向服务器发送ACK表示确认,可以确认服务器的发送和接收能力以及客户端的接收能力正常,   连接建立,通过三次握手能够保证通信双方的接收发送能力正常四次挥手:客户端发送FIN+x序列号表示请求关闭连接,服务器发送ACK+x+1表示确认收到,客户端向服务器的通道关        闭,服务器发送FIN+y序列号表示请求关闭连接,客户端发送ACK+y+1表示收到,等待2MSL没有收到回复后关闭TCP连接,因为TCP是全双工的,双向链路分别需要发送和接收两次,所以是需要四次挥手。11) 从输入网址,到最后访问页面的全过程首先输入URL,进行URL解析,准备发送http请求在请求之前,先本地查看浏览器缓存,如果缓存有该资源,直接返回,否则继续准备请求发送请求之前,进行DNS域名解析,按照本地缓存,本地HOST,路由器缓存,DNS服务器,DNS根服务器顺序,直到查        询到URL对应的IP地址三次握手建立TCP连接构建请求并发送,包括请求行,请求头,请求体,并把和该域名相关的cookie放入请求头,构建HTTP请求,如果是https        还要进行加密服务器处理请求,生成对应的响应并返回相应资源四次握手关闭TCP连接浏览器接收到响应后进行解析处理,如果是字节流可能是下载管理器进行下载,如果是html页面就是进行渲染生成页面。
查看11道真题和解析
点赞 评论 收藏
分享
评论
8
12
分享

创作者周榜

更多
牛客网
牛客企业服务