Go里有哪些数据结构是并发安全的?
并发安全就是程序在并发的情况下执行的结果都是正确的;
Go中数据类型分为两大类:
- 基本数据类型:字节型、整型、布尔型、浮点型、复数型、字符串
- 复合数据类型:数组、切片、指针、结构体、字典、、函数、接口
字节型、布尔型、整型、浮点型取决于操作系统指令值,在64位的指令集架构中可以由一条机器指令完 成,不存在被细分为更小的操作单位,所以这些类型的并发赋值是安全的,但是这个也跟操作系统的位 数有关,比如int64在32位操作系统中,它的高32位和低32位是分开赋值的,此时是非并发安全的。
复数类型、字符串、结构体、数组,切片,字典,通道,接口, 这些底层都是struct,不同成员的赋值 都不是一起的,所以都不是并发安全的。
Go如何实现一个单例模式?
单例模式的作用是确保无论对象被实例化多少次,全局都只有一个实例存在。根据这一特性,我们可以将其应用到全局唯一性配置、数据库连接对象、文件访问对象等。
饿汉式
饿汉式实现单例模式非常简单,直接看代码:
package singleton
type singleton struct{}
var instance = &singleton{}
func GetSingleton() *singleton {
return instance
}
singleton 包在被导入时会自动初始化 instance 实例,使用时通过调用 singleton.GetSingleton() 函数即可获得 singleton 这个结构体的单例对象。
这种方式的单例对象是在包加载时立即被创建,所以这个方式叫作饿汉式。与之对应的另一种实现方式叫作懒汉式,懒汉式模式下实例会在第一次被使用时被创建。
需要注意的是,尽管饿汉式实现单例模式的方式简单,但大多数情况下并不推荐。因为如果单例实例化时初始化内容过多,会造成程序加载用时较长。
懒汉式
接下来我们再来看下如何通过懒汉式实现单例模式:
package singleton
type singleton struct{}
var instance *singleton
func GetSingleton() *singleton {
if instance == nil {
instance = &singleton{}
}
return instance
}
相较于饿汉式的实现,懒汉式将实例化 singleton 结构体部分的代码移到了 GetSingleton() 函数内部。这样能够将对象实例化的步骤延迟到 GetSingleton() 第一次被调用时。
不过通过 instance == nil 的判断来实现单例并不十分可靠,如果有多个 goroutine 同时调用 GetSingleton() 就无法保证并发安全。
- sync.map的底层实现
什么是sync.Map
Go 的内建 map 是不支持并发写操作的,原因是 map 写操作不是并发安全的,当你尝试多个 Goroutine 操作同一个 map,会产生报错:fatal error: concurrent map writes。
因此官方另外引入了 sync.Map 来满足并发编程中的应用。
sync.Map 的实现原理可概括为:
- 通过 read 和 dirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上
- 读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty
- 读取 read 并不需要加锁,而读或写 dirty 都需要加锁
- 另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read 上
- 对于删除数据则直接通过标记来延迟删除
数据结构
type Map struct {
// 加锁作用,保护 dirty 字段
mu Mutex
// 只读的数据,实际数据类型为 readOnly
read atomic.Value
// 最新写入的数据
dirty map[interface{}]*entry
// 计数器,每次需要读 dirty 则 +1
misses int
}
其中 readOnly 的数据结构为:
type readOnly struct {
// 内建 map
m map[interface{}]*entry
// 表示 dirty 里存在 read 里没有的 key,通过该字段决定是否加锁读 dirty
amended bool
}
entry 数据结构则用于存储值的指针:
type entry struct {
p unsafe.Pointer // 等同于 *interface{}
}
属性 p 有三种状态:
- p == nil: 键值已经被删除,且 m.dirty == nil
- p == expunged: 键值已经被删除,但 m.dirty!=nil 且 m.dirty 不存在该键值(expunged 实际是空接口指针)
- 除以上情况,则键值对存在,存在于 m.read.m 中,如果 m.dirty!=nil 则也存在于 m.dirty
Map 常用的有以下方法:
- Load:读取指定 key 返回 value
- Store:存储(增或改)key-value
- Delete:删除指定 key
channel在什么情况下会panic?
- 关闭为nil的channel
- 关闭一个已经关闭的通道
- 向一个已经关闭的通道写数据
- 关闭通道导致发送阻塞的协程panic
redis有哪些数据结构,分别常用于哪些场合?
redis的基本数据结构有: 1、String(字符串);2、Hash(哈希);3、List(列表);4、Set(集合);5、zset(有序集合)。
String(字符串)
String 类型是 Redis 中最基本、最常用的数据类型,甚至被很多用户当成 Redis 少数的数据类型去使用。String 类型在 Redis 中是二进制安全(binary safe)的,这意味着 String 值关心二进制的字符串,不关心具体格式,你可以用它存储 json 格式或 JPEG 图片格式的字符串。
应用:
- 存储一些配置数据:在前后分离式开发中,有些数据虽然存储在数据库,但是更改特别少。比如有个全国地区表。当前端发起请求后,后台如果每次都从关系型数据库读取,会影响网站整体性能。我们可以在名列前茅次访问的时候,将所有地区信息存储到redis字符串中,再次请求,直接从数据库中读取地区的json字符串,返回给前端。
- 缓存对象:将对象转为json存储,比如商品信息,用户信息。
- 数据统计:redis整型可以用来记录网站访问量,某个文件的下载量,签到人数、视频访问量等等。(自增自减)
- 时间内限制请求次数:比如已登录用户请求短信验证码,验证码在5分钟内有效的场景。当用户首次请求了短信接口,将用户id存储到redis 已经发送短信的字符串中,并且设置过期时间为5分钟。当该用户再次请求短信接口,发现已经存在该用户发送短信记录,则不再发送短信。
- 订单号(全局少数):有时候你需要去生成一个全局少数值的时候可以通过redis生成。关键命令:incrby(原子自增)。
- 分布式session:当我们用nginx做负载均衡的时候,如果我们每个从服务器上都各自存储自己的session,那么当切换了服务器后,session信息会由于不共享而会丢失,我们不得不考虑第三应用来存储session。
Hash(哈希)
Hash的数据结构我们可以简单理解为java中的 Map,这种结构就特别适合存储对象,上面的String的类型确实也可以存储对象,但每次修改对象中的某一个属性,都要拿出整个json字符串在修改这个属性,之后在重新插入,而hash的接口特点让我们可以只修改该对象的某一个属性。
hash数据类型在存储上述类型的数据时具有比 String 类型更灵活、更快的优势,具体的说,使用 String 类型存储,必然需要转换和解析 json 格式的字符串,即便不需要转换,在内存开销方面,还是 hash 占优势。
应用:
- Redisson分布式锁:Redisson在实现分布式锁的时候,内部的用的数据就是hash而不是String。因为Redisson为了实现可重入加锁机制。所以在hash中存入了当前线程ID。
- 购物车列表:以用户id为key,商品id为field,商品数量为value,恰好构成了购物车的3个要素。
- 缓存对象:hash类型的 (key, field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
List(列表)
List类型是按照插入顺序排序的字符串链表,一个列表非常多可以存储2^32-1个元素。我们可以简单理解为就相当于java中的LinkesdList。和数据结构中的普通链表一样,我们可以在其头部(left)和尾部(right)添加新的元素。在插入时,如果该键并不存在,Redis将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移除,那么该键也将会被从数据库中删除。
应用:
- 消息队列:lpop和rpush(或者反过来,lpush和rpop)能实现队列的功能。
Set(集合)
Redis 中的 set和Java中的HashSet 有些类似,它内部的键值对是无序的、少数的。它的内部实现相当于一个特殊的字典,字典中所有的value都是一个值 NULL。当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。
应用:
- 抽奖活动:存储某活动中中奖的用户ID ,因为有去重功能,可以保证同一个用户不会中奖两次。
zset(有序集合)
Sorted-Sets中的每一个成员都会有一个分数(score)与之关联,Redis正是通过分数来为集合中的成员进行从小到大的排序。成员是少数的,但是分数(score)却是可以重复的。
应用: 作为有序的,不可重复的列表,可以做一些排行榜相关的场景:
- 排行榜(商品销量,视频评分,用户游戏分数)
- 新闻热搜
说下缓存击穿,缓存穿透,缓存雪崩有什么区别?
缓存击穿
当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩
缓存击穿
我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿。
缓存穿透
当发生缓存雪崩或击穿时,数据库中还是保存了应用要访问的数据,一旦缓存恢复相对应的数据,就可以减轻数据库的压力,而缓存穿透就不一样了。
当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
主键索引和唯一索引的区别
- 主键是一种约束,唯一索引是一种索引,两者在本质上是不同的。
- 主键创建后一定包含一个唯一性索引,唯一性索引并不一定就是主键。
- 唯一性索引列允许空值,而主键列不允许为空值。
- 主键可以被其他表引用为外键,而唯一索引不能。
- 一个表最多只能创建一个主键,但可以创建多个唯一索引。
- 主键更适合那些不容易更改的唯一标识,如自动递增列、身份证号等。
- 在RBO模式下,主键的执行计划优先级要高于唯一索引。两者可以提高查询的速度。
约束主要有:主键约束、外键约束、非空约束、检査约束(bentwen and ,大于、小于、等于、不等于)、唯一约束。
索引为什么使用B+树,而不使用跳表?
B+树是多叉树结构,每个结点都是一个16k的数据页,能存放较多索引信息,所以扇出很高。三层左右就可以存储2kw左右的数据(知道结论就行,想知道原因可以看之前的文章)。也就是说查询一次数据,如果这些数据页都在磁盘里,那么最多需要查询三次磁盘IO。
跳表是链表结构,一条数据一个结点,如果最底层要存放2kw数据,且每次查询都要能达到二分查找的效果,2kw大概在2的24次方左右,所以,跳表大概高度在24层左右。最坏情况下,这24层数据会分散在不同的数据页里,也即是查一次数据会经历24次磁盘IO。
因此存放同样量级的数据,B+树的高度比跳表的要少,如果放在mysql数据库上来说,就是磁盘IO次数更少,因此B+树查询更快。
而针对写操作,B+树需要拆分合并索引数据页,跳表则独立插入,并根据随机函数确定层数,没有旋转和维持平衡的开销,因此跳表的写入性能会比B+树要好。
计算机网络的多层模型简要介绍
- 应用层(Application):为用户的应用程序提供网络服务
- 表示层(Presentation):将信息表示为一定形式和格式的数据流
- 会话层(Session):负责通信主机之间会话的建立、管理和拆除,协调通信双方的会话
- 传输层(Transport):负责通信主机间端到端的连接
- 网络层(Network):负责将分组从源机送到目的机,包括寻址和最优路径选择等
- 数据链路层(Data Link):提供可靠的帧传递,实现差错控制、流控等等
- 物理层(Physical):提供透明的比特流(01流)传递
http2.0相比与http1.1的优化
HTTP2.0(Hypenext TransferProtocol version2)是超文本传输协议的第二版,HTTP2.0相比于HTTP1x,大幅度的提升了web性能,同时向下兼容HTTP1.X协议版 本。
主要核心优势有
1、采用二进制格式传输数据,而非htp1.1文本格式,二进制格式在协议的解析和优化扩展上带来了跟多的优势和可能
2、对消息头采用Hpack进行压缩传输,能够节省消息头占用的网络流量,htp1.1每次请求,都会携带大量冗余的头信息,浪费了很多宽带资源,
3、异步连接多路复用
4、Server Push,服务器端能够更快的把资源推送到客户端。
5、保持与HTTP 1.1语义的向后兼容性也是该版本的一个关键
本文转载自微信公众号「王中阳Go」,作者「王中阳Go」,可以通过以下二维码关注。
转载本文请联系「王中阳Go」公众号。