1、go中make和new区别
new:
- 分配内存。内存里存的值是对应类型的零值。
- 只有一个参数。参数是分配的内存空间所存储的变量类型,Go语言里的任何类型都可以是new的参数,比如int, 数组,结构体,甚至函数类型都可以。
- 返回的是指针。
make:
- 分配和初始化内存。
- 只能用于slice, map和chan这3个类型,不能用于其它类型。如果是用于slice类型,make函数的第2个参数表示slice的长度,这个参数必须给值。
- 返回的是原始类型,也就是slice, map和chan,不是返回指向slice, map和chan的指针。
2、进程、线程、协程区别
进程:一个具有特定功能的程序运行在一个数据集上的一次动态过程。是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。(QQ、微信等等)
线程:一个进程可以有多个线程,每个线程会共享父进程的资源(创建线程开销占用比进程小很多,可创建的数量也会很多),有时被称为轻量级进程(Lightwight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。
协程:一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
3、go中channel底层是什么
type hchan struct {
qcount uint // 队列中的总数据
dataqsiz uint // 循环队列的大小
buf unsafe.Pointer // 指向环形队列的元素数组
elemsize uint16
closed uint32
elemtype *_type // 元素类型
sendx uint // 发送指数
recvx uint // 收到指数
recvq waitq // 接收列表
sendq waitq // 发生列表
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
4、go derfer执行顺序
1、多个defer的执行顺序为“后进先出”;
2、defer、return、返回值三者的执行逻辑应该是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出。
我们知道defer后面一定要接一个函数的,所以defer的数据结构跟一般函数类似,也有栈地址、程序计数器、函数地址等等。
与函数不同的一点是它含有一个指针,可用于指向另一个defer,每个goroutine数据结构中实际上也有一个defer指针,该指针指向一个defer的单链表,每次声明一个defer时就将defer插入到单链表表头,每次执行defer时就从单链表表头取出一个defer执行。
5、go如何用两个协程交替打印出123456
6、go中slice和数组的区别
1. 数组定长,定义的时候就需要确定。切片长度不定,append时会自动扩容
2. 相同大小数组可以赋值,会拷贝全部内容。slice赋值和指针一样。数组和slice之间不能相互赋值。当然slice有自己的copy函数
3. 数组也可以进行切片,返回值是一个slice,改变slice时会同步修改数组内容,相当于取得了这个数组的指针
7、go中channel在你们项目中使用的场景举例
1、消息传递、消息过滤
2、信号广播
3、事件订阅与广播
4、请求、响应转发
5、任务分发
8、go中使用chan要注意什么
例如:for x := range ch {
//
}
if v, ok := <-chan;ok{
///
}
select {
case <-ch:
//
default:
//
}
func WorkWithOutTime(t time.Duration) (int, error) {
select {
case ret := <-Work():
return ret, nil
case <-time.After(t):
return 0, errors.New("timeout")
}
}
func Work() <-chan int {
ch := make(chan int)
go func() {
//
}()
return ch
}
func OnlyRead(ch chan int) {
select {
case <-ch:
//
default:
//
}
}
9、说一下go中的CSP
CSP(communicating sequential processes)并发模型:Go语言特有且推荐使用的。
Go的CSP并发模型,是通过goroutine和channel来实现的。
不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。
普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通信都是通过共享内存的方式来进行的。非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问,因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”。
Go的CSP并发模型,是通过goroutine和channel来实现的。
goroutine 是Go语言中并发的执行单位。可以理解为用户空间的线程。
channel是Go语言中不同goroutine之间的通信机制,即各个goroutine之间通信的”管道“,有点类似于Linux中的管道。
M:是内核线程
P : 是调度协调,用于协调M和G的执行,内核线程只有拿到了 P才能对goroutine继续调度执行,一般都是通过限定P的个数来控制golang的并发度
G : 是待执行的goroutine,包含这个goroutine的栈空间
Gn : 灰色背景的Gn 是已经挂起的goroutine,它们被添加到了执行队列中,然后需要等待网络IO的goroutine,当P通过 epoll查询到特定的fd的时候,会重新调度起对应的,正在挂起的goroutine。
Golang为了调度的公平性,在调度器加入了steal working 算法 ,在一个P自己的执行队列,处理完之后,它会先到全局的执行队列中偷G进行处理,如果没有的话,再会到其他P的执行队列中抢G来进行处理
csp很好的例子:生产者和消费者。
10、说一下go中channel是不是线程安全的,什么情况下会变成线程不安全
Channel是Go中的一个核心类型,可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication),Channel也可以理解是一个先进先出的队列,通过管道进行通信。
Golang的Channel,发送一个数据到Channel 和 从Channel接收一个数据 都是 原子性的。而且Go的设计思想就是:不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。也就是说,设计Channel的主要目的就是在多任务间传递数据的,这当然是安全的
11、说一下map是有序的吗?如何实现有序
var keys []string
for key := range maps {
keys = append(keys, key)
}
sort.Strings(keys) //内值排序
for _, key := range keys {
fmt.Printf("%s:%v\n", key, maps[key])
}
12、如下程序添加一段代码让程序不报panic异常。
func test() {
//添加一段程序捕获panic异常
}
func main() {
defer test()
panic(1)
}
答案:
func test() {
//添加一段程序捕获panic异常
recover()
}
func main() {
defer test()
panic(1)
}
13、使用go语言将数组中12300045变成12345000
func MobileNnm(arr []int) []int {
arr2 := make([]int, 0)
oneCount := 0
for _, v := range arr {
if v == 0 {
oneCount++
} else {
arr2 = append(arr2, v)
}
}
for i := 0; i < oneCount; i++ {
arr2 = append(arr2, 0)
}
return arr2
}
func main() {
//使用go语言将12300045变成12345000
val1 := []int{1, 2, 3, 0, 0, 0, 4, 5}
val2 := MobileNnm(val1)
fmt.Println(val2)
}
结果:[1 2 3 4 5 0 0 0]
14、算法题
题目:1、(( 结果是false
2、(())() 是true
3、((()))())是false 用go实现
思路:压栈找到匹配的出栈,找不到还放入栈中,直到栈为空,代表都匹配上了。
代码如下:(仅供参考)
func main() {
//题目:1、(( 结果是false
//2、(())() true
//3、((()))())false 用go实现
s0 := []string{"(", "("}
fmt.Println(IsMatch(s0))//false
s1 := []string{"(", "(", ")", ")", "(", ")"}
fmt.Println(IsMatch(s1)) //true
s2 := []string{"(", "(", ")", ")", "(", ")", ")"}
fmt.Println(IsMatch(s2))//false
}
func IsMatch(str []string) bool {
//压栈的思想
if len(str) == 0 {
return false
}
stack := NewStack()
for _, v := range str {
if stack.IsEmpty() {
stack.Push(v)
} else {
sp := stack.Pop()
if sp == "(" && string(v) == ")" {
continue
} else {
stack.Push(sp)
stack.Push(v)
}
}
}
if stack.IsEmpty() {
return true
}
return false
}
type Element interface{} //可存入任何类型
type Stack struct {
list []Element
}
//初始化栈
func NewStack() *Stack {
return &Stack{
list: make([]Element, 0),
}
}
//判断栈是否空
func (s *Stack) IsEmpty() bool {
if len(s.list) == 0 {
return true
} else {
return false
}
}
//入栈
func (s *Stack) Push(x interface{}) {
s.list = append(s.list, x)
}
//出栈
func (s *Stack) Pop() Element {
if len(s.list) <= 0 {
fmt.Println("Stack is Empty")
return nil
} else {
ret := s.list[len(s.list)-1]
s.list = s.list[:len(s.list)-1]
return ret
}
}
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、算法题(在线求答案)
题目:
输入文件构成规则如下:
1. 每行代表一条记录,字段之间以逗号(,)分隔
2. 若字段内容包含逗号(,),则以双引号包围该字段
3. 若字段内容包含双引号("),则以双引号包围该字段,字段内的双引号由一个变两个
请参照上面三条规则,编写一个解析程序,将解析后的记录内容按行输出,字段之间以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
求输出!
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)
mysql数据库事务的隔离级别有4个,而默认的事务处理级别就是【REPEATABLE-READ】,也就是可重复读
隔离级别 脏读 不可重复读 幻读 READ UNCOMITTED √ √ √ READ COMMITTED × √ √ REPEATABLE READ × × √ SERIALIZABLE × × ×
21、 B树、B+tree、Hash有什么区别
B树是一种多路自平衡搜索树,它类似普通的二叉树,但是B书允许每个节点有更多的子节点。B树示意图如下:
B树的特点:
- 所有键值分布在整个树中
- 任何关键字出现且只出现在一个节点中
- 搜索有可能在非叶子节点结束
- 在关键字全集内做一次查找,性能逼近二分查找算法
- 树深度会很深,因为树顶放到元素比较少导致,检索元素比较慢。
缺点:
业务数据的大小可能远远超过了索引数据的大小,每次为了查找对比计算,需要把数据加载到内存以及 CPU 高速缓存中时,都要把索引数据和无关的业务数据全部查出来。本来一次就可以把所有索引数据加载进来,现在却要多次才能加载完。如果所对比的节点不是所查的数据,那么这些加载进内存的业务数据就毫无用处,全部抛弃。
B+Tree:
![]()
从图中也可以看到,B+树与B树的不同在于:
- 所有关键字存储在叶子节点,非叶子节点不存储真正的data
- 为所有叶子节点增加了一个链指针
- 树顶可以放很多元素,树的深度比较矮,检索元素比较快。
缺点:
仍然有一个致命的缺陷,那就是它的索引数据与业务绑定在一块,而业务数据的大小很有可能远远超过了索引数据,这会大大减小一次 I/O 有用数据的获取,间接的增加 I/O 次数去获取有用的索引数据
Hash:
![]()
特点:数组+链表
1、查询单条数据很快,先解析出hash值,根据hash找到链表,然后找到索引最后根据索引找到数据。
缺点:
- 容易hash碰撞
- Hash索引仅仅能满足“=”,“IN”,“<=>”查询,不能使用范围查询
- 联合索引中,Hash索引不能利用部分索引键查询。
- Hash索引无法避免数据的排序操作
- Hash索引任何时候都不能避免表扫描
- Hash索引遇到大量Hash值相等的情况后性能会下降
22、 Mysql 中 MyISAM 和 InnoDB 的区别有哪些?
- InnoDB支持事务,MyISAM不支持
- 对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
- InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;
- InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。
- 但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此主键不应该过大,因为主键太大,其他索引也都会很大。
- 而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
- InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
- 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并发模型有几种
func main() {
ch := make(chan instruct{})
go func() {
ch <- struct{}{}
}()
fmt.Println(<-ch)
}
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()
}
// 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{}
}
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)
}
25、go分布式锁有几种
想要得到正确的结果的话,要把对计数器(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
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{}{}
}
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!
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")
}
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")
}
}
26、定时器的实现原理
1、 时间堆
最常见的时间堆一般用小顶堆实现,小顶堆其实就是一种特殊的二叉树,见图6-4
小顶堆的好处是什么呢?实际上对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所
有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。定时检查的时间复
杂度是 O(1) 。
当我们发现堆顶的元素小于当前时间时,那么说明可能已经有一批事件已经开始过期了,这时进行正常
的弹出和堆调整操作就好。每一次堆调整的时间复杂度都是 O(LgN) 。
Go自身的内置定时器就是用时间堆来实现的,不过并没有使用二叉堆,而是使用了扁平一些的四叉堆。
在最近的版本中,还加了一些优化,我们先不说优化,先来看看四叉的小顶堆长什么样:
2、时间轮
用时间轮来实现定时器时,我们需要定义每一个格子的“刻度”,可以将时间轮想像成一个时钟,中心有
秒针顺时针转动。每次转动到一个刻度时,我们就需要去查看该刻度挂载的任务列表是否有已经到期的
任务。
从结构上来讲,时间轮和哈希表很相似,如果我们把哈希算法定义为:触发时间%时间轮元素大小。那
么这就是一个简单的哈希表。在哈希冲突时,采用链表挂载哈希冲突的定时器。3、任务分发
每一个实例每隔一小时,会去数据库里把下一个小时需要处理的定时任务捞出来,捞取的时候只要取那
些 task_id % shard_count = shard_id 的那些任务即可。
当这些定时任务被触发之后需要通知用户侧,有两种思路:
1. 将任务被触发的信息封装为一条消息,发往消息队列,由用户侧对消息队列进行监听。
2. 对用户预先配置的回调函数进行调用。
两种方案各有优缺点,如果采用1,那么如果消息队列出故障会导致整个系统不可用,当然,现在的消
息队列一般也会有自身的高可用方案,大多数时候我们不用担心这个问题。其次一般业务流程中间走消
息队列的话会导致延时增加,定时任务若必须在触发后的几十毫秒到几百毫秒内完成,那么采用消息队列就会有一定的风险。如果采用2,会加重定时任务系统的负担。我们知道,单机的定时器执行时最害
怕的就是回调函数执行时间过长,这样会阻塞后续的任务执行。在分布式场景下,这种忧虑依然是适用
的。一个不负责任的业务回调可能就会直接拖垮整个定时任务系统。所以我们还要考虑在回调的基础上
增加经过测试的超时时间设置,并且对由用户填入的超时时间做慎重的审核。
27、负载均衡有几种方式
func shuffle(n int) []int {
b := rand.Perm(n)
return b
}
由于篇幅太长转到第二篇文章