网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
}
}
**15、算法题**
题目:上楼梯有一阶和两阶
例如3层楼梯有如下种
1 2 1
1 1 2
1
n阶有多少种:
代码如下:(仅供参考)
func main() {
n := Upstairs(3)
fmt.Println(n)
}
var mapData = make(map[int]int, 0)
func Upstairs(n int) int {
if n == 1 {
return 1
}
if n == 2 {
return 2
}
if _, ok := mapData[n]; !ok {
mapData[n] = Upstairs(n-1) + Upstairs(n-2)
}
return mapData[n]
}
**16、用go实现单链表的反转**
题目:单链表12345678910反转成10987654321
代码如下:(仅供参考)
func main() {
//实现单链表的反转例如12345678910变成10987654321
var head = new(ListNode)
CreateNode(head, 10)
PrintNode(“前:”, head)
yyy := reverseList(head)
PrintNode(“后:”, yyy)
}
type ListNode struct {
data interface{}
next *ListNode
}
func reverseList(head *ListNode) *ListNode {
cur := head
var pre *ListNode
for cur != nil {
cur.next, pre, cur = pre, cur, cur.next
}
return pre
}
func CreateNode(node *ListNode, max int) {
cur := node
for i := 1; i <= max; i++ {
cur.next = &ListNode{}
cur.data = i
cur = cur.next
}
}
//打印链表的方法
func PrintNode(str string, node *ListNode) {
fmt.Print(str)
for cur := node; cur != nil; cur = cur.next {
if cur.data != nil {
fmt.Print(cur.data, " ")
}
}
fmt.Println()
}
结果:
前:1 2 3 4 5 6 7 8 9 10
后:10 9 8 7 6 5 4 3 2 1
**17、算法题(在线求答案)**
题目:
输入文件构成规则如下:
- 每行代表一条记录,字段之间以逗号(,)分隔
- 若字段内容包含逗号(,),则以双引号包围该字段
- 若字段内容包含双引号("),则以双引号包围该字段,字段内的双引号由一个变两个
请参照上面三条规则,编写一个解析程序,将解析后的记录内容按行输出,字段之间以TAB(\t)分隔,2小时内完成
示例:
John,33,“足球,摄影”,New York
John,33,“足球,”“摄影”,New York
输出:
John 33 足球,摄影 New York
John 33 足球,"摄影 New York
输入:
2,John,45,“足球,摄影”,New York
3,Carter Job,33,“”“健身”“,远足”,“河北,石家庄”
4,Steve,33,“大屏幕164"”“,“DC”“Home””"
5,“Jul,y”,33,Football,Canada
求输出!
package main
import (
“bufio”
“fmt”
“os”
“strings”
)
func parseRecord(line string) string {
var fields []string
var inQuote bool
var field string
for i := 0; i < len(line); i++ {
c := line[i]
if c == ',' && !inQuote {
fields = append(fields, field)
field = ""
} else if c == '"' {
inQuote = !inQuote
if i > 0 && line[i-1] == '"' {
field += "\""
}
} else {
field += string(c)
}
}
fields = append(fields, field)
return strings.Join(fields, "\t")
}
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(parseRecord(line))
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading standard input:", err)
}
}
**18、slice和map区别代码输出题**
题目一:
func main() {
s1 := []int{1, 2, 3, 4, 5}
changeslice(s1)
fmt.Println(s1)//?s1的结果会变化吗?
fmt.Println(len(s1), cap(s1))//?s1长度和容量会变化吗?
m1 := map[int]int{1: 1, 2: 2, 3: 3, 4: 4}
changeMap(m1)
fmt.Println(m1) //?m1中key等于1的值会变化吗?
fmt.Println(len(m1))//?m1长度和容量会变化吗?
}
func changeslice(s []int) {
s[0] = 2
s = append(s, 7, 8, 9, 10)
}
func changeMap(m map[int]int) {
m[1] = 2
m[5] = 5
m[6] = 6
}
答案:s1的值会变化,但是长度和容量不会变化。m1的值会变化,长度会变化。
切记:
map 没有容量限制,所以内置函数 cap 也不接受 map 类型
题目二:
func main() {
m := make(map[int]int)
mdMap(m)
fmt.Println(m) //输出结果
}
func mdMap(m map[int]int) {
m[1] = 100
m[2] = 200
}
func main() {
var m1 map[int]int
mdMap(m1)
fmt.Println(m1)//输出的结果
}
func mdMap(m map[int]int) {
m = make(map[int]int)
m[1] = 100
m[2] = 200
}
答案:m结果map[2:200 1:100] m1的结果map[]
**19、mysql有那几种存储引擎**
>
> MyISAM:
>
>
> 创建一个myisam存储引擎的表的时候回出现三个文件
>
>
> 1.tb\_demo.frm,存储表定义; 2.tb\_demo.MYD,存储数据; 3.tb\_demo.MYI,存储索引。
>
>
> MyISAM表无法处理事务,这就意味着有事务处理需求的表,不能使用MyISAM存储引擎。
>
>
> MyISAM存储引擎特别适合在以下几种情况下使用:
>
>
> 1.选择密集型的表。MyISAM存储引擎在筛选大量数据时非常迅速,这是它最突出的优点。
>
>
> 2.插入密集型的表。MyISAM的并发插入特性允许同时选择和插入数据。例如:MyISAM存储引擎很适合管理邮件或Web服务器日志数据。
>
>
>
> InnoDB:
>
>
> InnoDB是一个健壮的事务型存储引擎MySQL 5.6.版本以后InnoDB就是作为默认的存储引擎。
>
>
> InnoDB还引入了行级锁定和外键约束,在以下场合下,使用InnoDB是最理想的选择:
>
>
> 1. 更新密集的表。InnoDB存储引擎特别适合处理多重并发的更新请求。
>
>
> 2.事务。InnoDB存储引擎是支持事务的标准MySQL存储引擎。
>
>
> 3.自动灾难恢复。与其它存储引擎不同,InnoDB表能够自动从灾难中恢复。
>
>
> 4.外键约束。MySQL支持外键的存储引擎只有InnoDB。
>
>
> 5.支持自动增加列AUTO\_INCREMENT属性。
>
>
>
> MEMORY:
>
>
> 使用MySQL Memory存储引擎的出发点是速度。为得到最快的响应时间,采用的逻辑存储介质是系统内存。虽然在内存中存储表数据确实会提供很高的性能,但当mysqld守护进程崩溃时,所有的Memory数据都会丢失。获得速度的同时也带来了一些缺陷。它要求存储在Memory数据表里的数据使用的是长度不变的格式,这意味着不能使用BLOB和TEXT这样的长度可变的数据类型,VARCHAR是一种长度可变的类型,但因为它在MySQL内部当做长度固定不变的CHAR类型,所以可以使用。
>
>
> 一般在以下几种情况下使用Memory存储引擎:
>
>
> 1.目标数据较小,而且被非常频繁地访问。在内存中存放数据,所以会造成内存的使用,可以通过参数max\_heap\_table\_size控制Memory表的大小,设置此参数,就可以限制Memory表的最大大小。
>
>
> 2.如果数据是临时的,而且要求必须立即可用,那么就可以存放在内存表中。
>
>
> 3.存储在Memory表中的数据如果突然丢失,不会对应用服务产生实质的负面影响。Memory同时支持散列索引和B树索引。B树索引的优于散列索引的是,可以使用部分查询和通配查询,也可以使用<、>和>=等操作符方便数据挖掘。散列索引进行“相等比较”非常快,但是对“范围比较”的速度就慢多了,因此散列索引值适合使用在=和<>的操作符中,不适合在<或>操作符中,也同样不适合用在order by子句中
>
>
>
> MERGE:
>
>
> MERGE存储引擎是一组MyISAM表的组合,这些MyISAM表结构必须完全相同,尽管其使用不如其它引擎突出,但是在某些情况下非常有用。说白了,Merge表就是几个相同MyISAM表的聚合器;Merge表中并没有数据,对Merge类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的MyISAM表进行操作。Merge存储引擎的使用场景。对于服务器日志这种信息,一般常用的存储策略是将数据分成很多表,每个名称与特定的时间端相关。例如:可以用12个相同的表来存储服务器日志数据,每个表用对应各个月份的名字来命名。当有必要基于所有12个日志表的数据来生成报表,这意味着需要编写并更新多表查询,以反映这些表中的信息。与其编写这些可能出现错误的查询,不如将这些表合并起来使用一条查询,之后再删除Merge表,而不影响原来的数据,删除Merge表只是删除Merge表的定义,对内部的表没有任何影响。
>
>
>
> ARCHIVE:
>
>
> rchive是归档的意思,在归档之后很多的高级功能就不再支持了,仅仅支持最基本的插入和查询两种功能。在MySQL 5.5版以前,Archive是不支持索引,但是在MySQL 5.5以后的版本中就开始支持索引了。Archive拥有很好的压缩机制,它使用zlib压缩库,在记录被请求时会实时压缩,所以它经常被用来当做仓库使用。
>
>
>
**20、mysql中事务隔离级别有哪几种**
>
> * 读未提交(READ UNCOMITTED)
> * 读提交(READ COMMITTED)
> * 可重复读(REPEATABLE READ)
> * 串行化(SERIALIZABLE)
> * | 隔离级别 | 脏读 | 不可重复读 | 幻读 |
> | --- | --- | --- | --- |
> | READ UNCOMITTED | √ | √ | √ |
> | READ COMMITTED | × | √ | √ |
> | REPEATABLE READ | × | × | √ |
> | SERIALIZABLE | × | × | × |
>
> mysql数据库事务的隔离级别有4个,而默认的事务处理级别就是【REPEATABLE-READ】,也就是可重复读
>
>
>
**21、 B树、B+tree、Hash有什么区别**
>
> B树是一种多路自平衡搜索树,它类似普通的[二叉树](https://bbs.csdn.net/topics/618658159),但是B书允许每个节点有更多的子节点。B树示意图如下:
>
>
> ![](https://img-blog.csdnimg.cn/f81a30bc075544a889f59b74aa89bdcc.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASVRfemlsaWFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)
>
>
> B树的特点:
>
>
> 1. 所有键值分布在整个树中
> 2. 任何关键字出现且只出现在一个节点中
> 3. 搜索有可能在非叶子节点结束
> 4. 在关键字全集内做一次查找,性能逼近二分查找算法
> 5. 树深度会很深,因为树顶放到元素比较少导致,检索元素比较慢。
>
>
> 缺点:
>
>
> 业务数据的大小可能远远超过了索引数据的大小,每次为了查找对比计算,需要把数据加载到内存以及 CPU 高速缓存中时,都要把索引数据和无关的业务数据全部查出来。本来一次就可以把所有索引数据加载进来,现在却要多次才能加载完。如果所对比的节点不是所查的数据,那么这些加载进内存的业务数据就毫无用处,全部抛弃。
>
>
> B+Tree:
>
>
> ![](https://img-blog.csdnimg.cn/d9152a755e68475aa453081f57c89a20.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASVRfemlsaWFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)
>
>
> 从图中也可以看到,B+树与B树的不同在于:
>
>
> 1. 所有关键字存储在叶子节点,非叶子节点不存储真正的data
> 2. 为所有叶子节点增加了一个链指针
> 3. 树顶可以放很多元素,树的深度比较矮,检索元素比较快。
>
>
> 缺点:
>
>
> 仍然有一个致命的缺陷,那就是它的**索引数据与业务绑定在一块**,而业务数据的大小很有可能远远超过了索引数据,这会大大减小一次 I/O 有用数据的获取,间接的增加 I/O 次数去获取有用的索引数据
>
>
> Hash:
>
>
> ![](https://img-blog.csdnimg.cn/4d5a3a952e9f486a940401c12adb4c41.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASVRfemlsaWFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)
>
>
> 特点:数组+链表
>
>
> 1、查询单条数据很快,先解析出hash值,根据hash找到链表,然后找到索引最后根据索引找到数据。
>
>
> 缺点:
>
>
> 1. 容易hash碰撞
> 2. Hash索引仅仅能满足“=”,“IN”,“<=>”查询,不能使用范围查询
> 3. 联合索引中,Hash索引不能利用部分索引键查询。
> 4. Hash索引无法避免数据的排序操作
> 5. Hash索引任何时候都不能避免表扫描
> 6. Hash索引遇到大量Hash值相等的情况后性能会下降
>
>
>
**22、** **Mysql 中 MyISAM 和 InnoDB 的区别有哪些?**
>
> 1. InnoDB支持事务,MyISAM不支持
> 2. 对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
> 3. InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;
> 4. InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。
> 5. 但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此主键不应该过大,因为主键太大,其他索引也都会很大。
> 6. 而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
> 7. InnoDB不保存表的具体行数,执行select count(\*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
> 8. Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高
>
>
>
**23、 go向关闭的channel发送和读取数据是否报错**
package main
import “fmt”
//向已关闭的通道读取数不会报错
func main1() {
var ch = make(chan int)
go func() {
close(ch)
}()
fmt.Println(<-ch)
}
//向已关闭的通道发送数据报panic: send on closed channel
func main2() {
var ch = make(chan int)
go func() {
close(ch)
}()
ch <- 1
}
//关闭通道向有缓存区接收数据会报错
func main3() {
var ch = make(chan int, 10)
go func() {
close(ch)
}()
fmt.Println(<-ch)
}
//关闭通道向有缓冲区发送数据会报错panic:send on closed channel
func main4() {
var ch = make(chan int, 10)
go func() {
close(ch)
}()
ch <- 1
}
//关闭通道向有缓冲区循环发送数据会报错 panic: send on closed channel
func main() {
var ch = make(chan int, 10)
go func() {
close(ch)
}()
for {
ch <- 1
}
}
**24、Golang并发模型有几种**
>
> 控制并发有三种种经典的方式,一种是通过channel通知实现并发控制 一种是WaitGroup,另外一种就是Context。
>
>
> **1、无缓冲通道**
>
>
> 无缓冲的通道指的是通道的大小为0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送 `goroutine` 和接收 `goroutine` 同时准备好,才可以完成发送和接收操作。
>
>
> 从上面无缓冲的通道定义来看,发送 `goroutine` 和接收 `gouroutine` 必须是同步的,同时准备后,如果没有同时准备好的话,先执行的操作就会阻塞等待,直到另一个相对应的操作准备好为止。这种无缓冲的通道我们也称之为同步通道。
>
>
> 正式通过无缓冲通道来实现多 `goroutine` 并发控制
>
>
>
> ```
> func main() {
> ch := make(chan instruct{})
> go func() {
> ch <- struct{}{}
> }()
> fmt.Println(<-ch)
> }
> ```
>
> 当主 `goroutine` 运行到 `<-ch` 接受 `channel` 的值的时候,如果该 `channel` 中没有数据,就会一直阻塞等待,直到有值。 这样就可以简单实现并发控制
>
>
> #### 2. 通过sync包中的WaitGroup实现并发控制
>
>
> 在 `sync` 包中,提供了 `WaitGroup` ,它会等待它收集的所有 `goroutine` 任务全部完成,在主 `goroutine` 中 `Add(delta int)` 索要等待`goroutine` 的数量。在每一个 `goroutine` 完成后 `Done()` 表示这一个`goroutine` 已经完成,当所有的 `goroutine` 都完成后,在主 `goroutine` 中 `WaitGroup` 返回返回。
>
>
>
> ```
> func main() {
> var wg sync.WaitGroup
>
> // 开N个后台打印线程
> for i := 0; i < 10; i++ {
> wg.Add(1)
>
> go func() {
> fmt.Println("你好, 世界")
> wg.Done()
> }()
> }
>
> // 等待N个后台线程完成
> wg.Wait()
> }
> ```
>
> #### 3. 在Go 1.7 以后引进的强大的Context上下文,实现并发控制
>
>
> 3.1 简介
>
>
> 在一些简单场景下使用 `channel` 和 `WaitGroup` 已经足够了,但是当面临一些复杂多变的网络并发场景下 `channel` 和 `WaitGroup` 显得有些力不从心了。比如一个网络请求 `Request`,每个 `Request` 都需要开启一个 `goroutine` 做一些事情,这些 `goroutine` 又可能会开启其他的 `goroutine`,比如数据库和RPC服务。所以我们需要一种可以跟踪 `goroutine` 的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的 `Context`,称之为上下文非常贴切,它就是`goroutine` 的上下文。它是包括一个程序的运行环境、现场和快照等。每个程序要运行时,都需要知道当前程序的运行状态,通常Go 将这些封装在一个 `Context` 里,再将它传给要执行的 `goroutine` 。`context` 包主要是用来处理多个 `goroutine` 之间共享数据,及多个 `goroutine` 的管理。
>
>
> 3.2 package context
>
>
> `context` 包的核心是 `struct Context`,接口声明如下:
>
>
>
> ```
> // A Context carries a deadline, cancelation signal, and request-scoped values
> // across API boundaries. Its methods are safe for simultaneous use by multiple
> // goroutines.
> type Context interface {
> // Done returns a channel that is closed when this `Context` is canceled
> // or times out.
> Done() <-chan struct{}
>
> // Err indicates why this Context was canceled, after the Done channel
> // is closed.
> Err() error
>
> // Deadline returns the time when this Context will be canceled, if any.
> Deadline() (deadline time.Time, ok bool)
>
> // Value returns the value associated with key or nil if none.
> Value(key interface{}) interface{}
> }
> ```
>
> * `Done()` 返回一个只能接受数据的`channel`类型,当该**context关闭**或者**超时时间**到了的时候,该channel就会有一个**取消信号**
> * `Err()` 在`Done()` 之后,返回`context` 取消的原因。
> * `Deadline()` 设置该`context cancel`的时间点
> * `Value()` 方法允许 `Context` 对象携带`request`作用域的数据,该数据必须是线程安全的。
>
>
> `Context` 对象是线程安全的,你可以把一个 `Context` 对象传递给任意个数的 `gorotuine`,对它执行 取消 操作时,所有 `goroutine` 都会接收到取消信号。
>
>
> 一个 `Context` 不能拥有 `Cancel` 方法,同时我们也只能 `Done channel` 接收数据。
> 背后的原因是一致的:**接收取消信号的函数和发送信号的函数通常不是一个。**
> 一个典型的场景是:父操作为子操作操作启动 `goroutine`,子操作也就不能取消父操作。
>
>
> 3.4 context例子
>
>
> 当然,想要知道 Context 包是如何工作的,最好的方法是看一个例子。
>
>
>
> ```
> func childFunc(cont context.Context, num *int) {
> ctx, _ := context.WithCancel(cont)
> for {
> select {
> case <-ctx.Done():
> fmt.Println("child Done : ", ctx.Err())
> return
> }
> }
> }
>
> func main() {
> gen := func(ctx context.Context) <-chan int {
> dst := make(chan int)
> n := 1
> go func() {
> for {
> select {
> case <-ctx.Done():
> fmt.Println("parent Done : ", ctx.Err())
> return // returning not to leak the goroutine
> case dst <- n:
> n++
> go childFunc(ctx, &n)
> }
> }
> }()
> return dst
> }
>
> ctx, cancel := context.WithCancel(context.Background())
> for n := range gen(ctx) {
> fmt.Println(n)
> if n >= 5 {
> break
> }
> }
> cancel()
> time.Sleep(5 * time.Second)
> }
> ```
>
> 在上面的例子中,主要描述的是通过一个`channel`实现一个为循环次数为5的循环,
> 在每一个循环中产生一个`goroutine`,每一个`goroutine`中都传入`context`,在每个`goroutine`中通过传入`ctx`创建一个**子`Context`**,并且通过`select`一直监控该`Context`的运行情况,当在父`Context`退出的时候,代码中并没有明显调用子`Context`的`Cancel`函数,但是分析结果,子`Context`还是被正确合理的关闭了,这是因为,所有基于这个`Context`或者衍生的子`Context`都会收到通知,这时就可以进行清理操作了,最终释放`goroutine`,这就优雅的解决了`goroutine`启动后不可控的问题。
>
>
> 3.5 Context 使用原则
>
>
> * 不要把`Context`放在结构体中,要以参数的方式传递
> * 以`Context`作为参数的函数方法,应该把`Context`作为第一个参数,放在第一位。
> * 给一个函数方法传递`Context`的时候,不要传递nil,如果不知道传递什么,就使用`context.TODO`
> * `Context`的`Value`相关方法应该传递必须的数据,不要什么数据都使用这个传递
> * `Context`是线程安全的,可以放心的在多个`goroutine`中传递
>
>
>
**25、go分布式锁有几种**
>
> **1 、进程内加锁**
>
>
>
> ```
> 想要得到正确的结果的话,要把对计数器(counter)的操作代码部分加上锁:
> // ... 省略之前部分
> var wg sync.WaitGroup
> var l sync.Mutex
> for i := 0; i < 1000; i++ {
> wg.Add(1)
> go func () {
> defer wg.Done()
> l.Lock()
> counter++
> l.Unlock()
> }()
> }
> wg.Wait()
> println(counter)
> // ... 省略之后部分
> 这样就可以稳定地得到计算结果了:
> ❯❯❯ go run local_lock.go
> 1000
> ```
>
> **2、trylock**
>
>
> 在某些场景,我们只是希望一个任务有单一的执行者。而不像计数器场景一样,所有goroutine都执行
> 成功。后来的goroutine在抢锁失败后,需要放弃其流程。这时候就需要trylock了。
> trylock顾名思义,尝试加锁,加锁成功执行后续流程,如果加锁失败的话也不会阻塞,而会直接返回
> 加锁的结果。在Go语言中我们可以用大小为1的Channel来模拟trylock:
>
>
>
> ```
> package main
> import (
> "sync"
> )
> type Lock struct {
> c chan struct{}
> }
>
> // NewLock generate a try lock
> func NewLock() Lock {
> var l Lock
> l.c = make(chan struct{}, 1)
> l.c <- struct{}{}
> return l
> }
>
> // Lock try lock, return lock result
> func (l Lock) Lock() bool {
> lockResult := false
> select {
> case <-l.c:
> lockResult = true
> default:
> }
> return lockResult
> }
>
> // Unlock , Unlock the try lock
> func (l Lock) Unlock() {
> l.c <- struct{}{}
> }
> ```
>
> 因为我们的逻辑限定每个goroutine只有成功执行了 Lock 才会继续执行后续逻辑,因此
> 在 Unlock 时可以保证Lock结构体中的channel一定是空,从而不会阻塞,也不会失败。上面的代
> 码使用了大小为1的channel来模拟trylock,理论上还可以使用标准库中的CAS来实现相同的功能且
> 成本更低,读者可以自行尝试。
> 在单机系统中,trylock并不是一个好选择。因为大量的goroutine抢锁可能会导致CPU无意义的资源
> 浪费。有一个专有名词用来描述这种抢锁的场景:活锁。
> 活锁指的是程序看起来在正常执行,但实际上CPU周期被浪费在抢锁,而非执行任务上,从而程序整体
> 的执行效率低下。活锁的问题定位起来要麻烦很多。所以在单机场景下,不建议使用这种锁。
>
>
> **3、基于Redis的setnx**
>
>
> 在分布式场景下,我们也需要这种“抢占”的逻辑,这时候怎么办呢?我们可以使用Redis提供
> 的 setnx 命令:
>
>
>
> ```
>
> package main
>
> import (
> "fmt"
> "sync"
> "time"
>
> "github.com/go-redis/redis"
> )
>
> func incr() {
> client := redis.NewClient(&redis.Options{
> Addr: "localhost:6379",
> Password: "", // no password set
> DB: 0, // use default DB
> })
>
> var lockKey = "counter_lock"
> var counterKey = "counter"
> // lock
> resp := client.SetNX(lockKey, 1, time.Second*5)
> lockSuccess, err := resp.Result()
>
> if err != nil || !lockSuccess {
> fmt.Println(err, "lock result: ", lockSuccess)
> return
> }
>
> // counter ++
> getResp := client.Get(counterKey)
> cntValue, err := getResp.Int64()
> if err == nil {
> cntValue++
> resp := client.Set(counterKey, cntValue, 0)
> _, err := resp.Result()
> if err != nil {
> // log err
> println("set value error!")
> }
> }
> println("current counter is ", cntValue)
>
> delResp := client.Del(lockKey)
> unlockSuccess, err := delResp.Result()
> if err == nil && unlockSuccess > 0 {
> println("unlock success!")
> } else {
> println("unlock failed", err)
> }
> }
> func main() {
> var wg sync.WaitGroup
> for i := 0; i < 10; i++ {
> wg.Add(1)
> go func() {
> defer wg.Done()
> incr()
> }()
> }
> wg.Wait()
> }
> 看看运行结果:
> 1. ❯❯❯ go run redis_setnx.go
> 2. <nil> lock result: false
> 3. <nil> lock result: false
> 4. <nil> lock result: false
> 5. <nil> lock result: false
> 6. <nil> lock result: false
> 7. <nil> lock result: false
> 8. <nil> lock result: false
> 9. <nil> lock result: false
> 10. <nil> lock result: false
> 11. current counter is 2028
> 12. unlock success!
> ```
>
> 通过代码和执行结果可以看到,我们远程调用 setnx 实际上和单机的trylock非常相似,如果获取
> 锁失败,那么相关的任务逻辑就不应该继续向前执行。
> setnx 很适合在高并发场景下,用来争抢一些“唯一”的资源。比如交易撮合系统中卖家发起订单,
> 而多个买家会对其进行并发争抢。这种场景我们没有办法依赖具体的时间来判断先后,因为不管是用户
> 设备的时间,还是分布式场景下的各台机器的时间,都是没有办法在合并后保证正确的时序的。哪怕是
> 我们同一个机房的集群,不同的机器的系统时间可能也会有细微的差别。
> 所以,我们需要依赖于这些请求到达Redis节点的顺序来做正确的抢锁操作。如果用户的网络环境比较
> 差,那也只能自求多福了。
>
>
> **4、基于ZooKeeper**
>
>
>
> ```
> package main
>
> import (
> "time"
>
> "github.com/samuel/go-zookeeper/zk"
> )
>
> func main() {
> c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second) //*10)
> if err != nil {
> panic(err)
> }
> l := zk.NewLock(c, "/lock", zk.WorldACL(zk.PermAll))
> err = l.Lock()
> if err != nil {
> panic(err)
> }
> println("lock succ, do your business logic")
>
> time.Sleep(time.Second * 10)
>
> // do some thing
> l.Unlock()
> println("unlock succ, finish business logic")
> }
>
> ```
>
> 基于ZooKeeper的锁与基于Redis的锁的不同之处在于Lock成功之前会一直阻塞,这与我们单机场景
> 中的 mutex.Lock 很相似。
> 其原理也是基于临时Sequence节点和watch API,例如我们这里使用的是 /lock 节点。Lock会在
> 该节点下的节点列表中插入自己的值,只要节点下的子节点发生变化,就会通知所有watch该节点的程
> 序。这时候程序会检查当前节点下最小的子节点的id是否与自己的一致。如果一致,说明加锁成功了。
> 这种分布式的阻塞锁比较适合分布式任务调度场景,但不适合高频次持锁时间短的抢锁场景。按照
> Google的Chubby论文里的阐述,基于强一致协议的锁适用于 粗粒度 的加锁操作。这里的粗粒度指
> 锁占用时间较长。我们在使用时也应思考在自己的业务场景中使用是否合适。
>
>
> **5、基于etcd**
>
>
> etcd是分布式系统中,功能上与ZooKeeper类似的组件,这两年越来越火了。上面基于ZooKeeper我
> 们实现了分布式阻塞锁,基于etcd,也可以实现类似的功能:
>
>
>
> ```
> package main
>
> import (
> "log"
>
> "github.com/zieckey/etcdsync"
> )
>
> func main() {
> m, err := etcdsync.New("/lock", 10, []string{"http://127.0.0.1:2379"})
> if m == nil || err != nil {
> log.Printf("etcdsync.New failed")
> return
> }
> err = m.Lock()
> if err != nil {
> log.Printf("etcdsync.Lock failed")
> return
> }
>
> log.Printf("etcdsync.Lock OK")
> log.Printf("Get the lock. Do something here.")
>
> err = m.Unlock()
> if err != nil {
> log.Printf("etcdsync.Unlock failed")
> } else {
> log.Printf("etcdsync.Unlock OK")
> }
> }
>
> ```
>
> etcd中没有像ZooKeeper那样的Sequence节点。所以其锁实现和基于ZooKeeper实现的有所不同。
> 在上述示例代码中使用的etcdsync的Lock流程是:
> 1. 先检查 /lock 路径下是否有值,如果有值,说明锁已经被别人抢了
> 2. 如果没有值,那么写入自己的值。写入成功返回,说明加锁成功。写入时如果节点被其它节点写
> 入过了,那么会导致加锁失败,这时候到 3
> 3. watch /lock 下的事件,此时陷入阻塞
> 4. 当 /lock 路径下发生事件时,当前进程被唤醒。检查发生的事件是否是删除事件(说明锁被持有
> 者主动unlock),或者过期事件(说明锁过期失效)。如果是的话,那么回到 1,走抢锁流程。
>
>
>
**26、定时器的实现原理**
>
> **1、 时间堆**
>
>
> 最常见的时间堆一般用小顶堆实现,小顶堆其实就是一种特殊的二叉树,见图6-4
>
>
> ![](https://img-blog.csdnimg.cn/ca28caf5f0bb4c59a500135a916e4613.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASVRfemlsaWFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)
![img](https://img-blog.csdnimg.cn/img_convert/889525e12d6f3dc8151b0682f5dd80a4.png)
![img](https://img-blog.csdnimg.cn/img_convert/0a77eeaaf6b5c3088baecdbddc7d9589.png)
![img](https://img-blog.csdnimg.cn/img_convert/6da050e9ee64e8cb5059cb1e90a4182e.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**
实现和基于ZooKeeper实现的有所不同。
> 在上述示例代码中使用的etcdsync的Lock流程是:
> 1. 先检查 /lock 路径下是否有值,如果有值,说明锁已经被别人抢了
> 2. 如果没有值,那么写入自己的值。写入成功返回,说明加锁成功。写入时如果节点被其它节点写
> 入过了,那么会导致加锁失败,这时候到 3
> 3. watch /lock 下的事件,此时陷入阻塞
> 4. 当 /lock 路径下发生事件时,当前进程被唤醒。检查发生的事件是否是删除事件(说明锁被持有
> 者主动unlock),或者过期事件(说明锁过期失效)。如果是的话,那么回到 1,走抢锁流程。
>
>
>
**26、定时器的实现原理**
>
> **1、 时间堆**
>
>
> 最常见的时间堆一般用小顶堆实现,小顶堆其实就是一种特殊的二叉树,见图6-4
>
>
> ![](https://img-blog.csdnimg.cn/ca28caf5f0bb4c59a500135a916e4613.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASVRfemlsaWFuZw==,size_20,color_FFFFFF,t_70,g_se,x_16)
[外链图片转存中...(img-r53nHvCU-1715789409018)]
[外链图片转存中...(img-p9oKwYWC-1715789409019)]
[外链图片转存中...(img-fIZM1W6X-1715789409019)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618658159)**