1. 语言层面的锁
乐观锁:
原子操作中的比较并交换简称CAS(Compare And Swap),在sync/atomic包中,这类原子操作由名称以CompareAndSwap为前缀的若干个函数提供
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer,old, new unsafe.Pointer) (swapped bool)
使用AddInt32函数对int32值执行添加原子操作:
func main() {
var n int32
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
atomic.AddInt32(&n, 1)
wg.Done()
}()
}
wg.Wait()
fmt.Println(atomic.LoadInt32(&n)) // output:1000
}
golang中原子操作CompareAndSwap:
CompareAndSwap函数会先判断参数addr指向的操作值与参数old的值是否相等,仅当此判断得到的结果是true之后,才会用参数new代表的新值替换掉原先的旧值,否则操作就会被忽略。atmoic原子操作总是假设被操作值未曾被改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换。在被操作值被频繁变更的情况下,CAS操作并不那么容易成功所以需要不断进行尝试,直到成功为止。
互斥锁:
golang中互斥锁的一个经典实现就是sync包下的sync.mutex,下面以并发访问slice为例:
slice是对数组一个连续片段的引用,当 slice 长度增加的时候,可能底层的数组会被换掉。当在换底层数组之前,切片同时被多个 goroutine 拿到,并执行 append 操作。那么很多 goroutine 的 append 结果会被覆盖,导致 n 个 gouroutine append 后,长度小于n,互斥锁解决并发访问slice的场景:
func main() {
slc := make([]int, 0, 1000)
var wg sync.WaitGroup
var lock sync.Mutex
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(a int) {
defer wg.Done()
// 加锁
lock.Lock()
defer lock.Unlock()
slc = append(slc, a)
}(i)
}
wg.Wait()
fmt.Println(len(slc))
}
缺点:分布式部署环境下锁会失效
2. mysql数据库实现锁
方案一:使用数据库的唯一性来实现资源锁定,比如主键和唯一索引等;建立一个字段为唯一索引,加入一条数据即表示加锁了,删除这条数据就解锁了;具体,使用很简单,具体实现就不再阐述
数据库中,为了实现高并发的数据访问,对数据进行多版本处理,并通过事务的可见性来保证事务能看到自己应该看到的数据版本,
方案二:select for update解决并发数据查询更新的问题
SET AUTOCOMMIT=0;
BEGIN WORK;
SELECT category_id FROM blog_article WHERE id=3 FOR UPDATE;
UPDATE blog_article SET category_id = 3;
# 在commit前其它事物无法对此行数据进行修改
COMMIT WORK;
在另外一个窗口执行新的事物修改:
UPDATE blog_article SET category_id = 2 WHERE id = 3;
会发现事物无法立即执行,会等待for update那条事物commit,如果此时长时间未commit则会超时:
[SQL]UPDATE blog_article SET category_id = 2 WHERE id = 3;
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction
缺点:单机mysql负载能力有限,mysql锁性能低下,select for update加锁如果where条件后的字段非主键则"表锁",如果是主键则为"行锁"
3. zookeeper、etcd实现分布式锁
zookeeper实现分布式锁:
利用 ZooKeeper 支持临时顺序节点的特性,可以实现分布式锁;当客户端对某个方法加锁时,在 ZooKeeper