锁可以分为正常的进程内锁和分布式的进程间的锁。
正常锁
我们一般所说的锁,就是指单进程多线程的锁机制。在单进程中,如果有多个线程并发访问某个某个全局资源,存在并发修改的问题。如果要避免这个问题,我们需要对资源进行同步,同步其实就是可以加一个锁来保证同一时刻只有一个线程能操作这个资源。
具体的锁可以看go的锁,当然在golang中主要是使用channel来实现进程内通信和共享。
分布式锁
涉及到分布式环境,以集群为例,就是多个实例,也就是多个进程,而且这些进程完全可能不在同一个机器上。我们知道多线程可以共享父进程的资源,包括内存。所以多线程可以看见锁,但是多进程之间无法共享资源,甚至都不在一台机器上,所以这时候分布式环境下,就需要其他的方式来让所有进程都可以知道这个锁,来控制对全局资源的并发修改。
为了解决分布式的问题,我们可以把这个锁放入所有进程都可以访问的地方,比如数据库,redis,memcached或者是zookeeper。这些也是目前实现分布式锁的主要实现方式。
基于数据库表实现分布式锁
我们可以将我们分布式要操作的资源都定义成表,然后对表进行查询数据,如果查到了没有数据,可以进行update,否则,说明该锁被其他线程持有,还没有释放
缺点:
更新之前会多一次查询,增加了数据库的操作
数据库链接资源宝贵,如果并发量太大,数据库的性能有影响
如果单个数据库存在单点问题,所以最好是高可用的。
基于Redis实现分布式锁
通过Redis的setnx key命令,如果不存在某个key就设置值,设置成功表示获取锁。
缺点:如果设置成功后,还没有释放锁,对应的业务节点就挂掉了,那么这时候锁就没有释放。其他业务节点也无法获取这个锁。
使用setnx设置命令成功后,则使用expire命令设置到期时间,就算业务节点还没有释放锁就挂掉了,但是我们还是可以保证这个锁到期就会释放。
缺点:
setnx 和 expire不是原子操作,即设置了setnx还没有来得及设置到期时间,业务节点就挂了。
而且key到期了,业务节点业务还没有执行完,怎么办?
使用set命令 我们知道set命令格式如下:
set key value [EX seconds] [PX milliseconds][NX|XX]
即首先可以根据这个key不存在,则设置值,即使用NX。然后可以设置到期时间,EX表示秒数,PX表示毫秒数,这个操作就是原子性的,解决了上述问题。
set id EX 10 NX
zookeeper,memcached
利用Memcached的add命令。此命令是原子性操作,只有在key不存在的情况下,才能add成功,也就意味着线程得到了锁。
利用Zookeeper的顺序临时节点,来实现分布式锁和等待队列。Zookeeper设计的初衷,就是为了实现分布式锁服务的。
实例
1、简单实例
package main
import (
"fmt"
"log"
"time"
"github.com/garyburd/redigo/redis"
)
type Lock struct {
resource string
token string
conn redis.Conn
timeout int
}
func (lock *Lock) tryLock() (ok bool, err error) {
_, err = redis.String(lock.conn.Do("SET", lock.key(), lock.token, "EX", int(lock.timeout), "NX"))
if err == redis.ErrNil {
// The lock was not successful, it already exists.
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
func (lock *Lock) Unlock() (err error) {
_, err = lock.conn.Do("del", lock.key())
return
}
func (lock *Lock) key() string {
return fmt.Sprintf("redislock:%s", lock.resource)
}
func (lock *Lock) AddTimeout(ex_time int64) (ok bool, err error) {
ttl_time, err := redis.Int64(lock.conn.Do("TTL", lock.key()))
fmt.Println(ttl_time)
if err != nil {
log.Fatal("redis get failed:", err)
}
if ttl_time > 0 {
fmt.Println(11)
_, err := redis.String(lock.conn.Do("SET", lock.key(), lock.token, "EX", int(ttl_time+ex_time)))
if err == redis.ErrNil {
return false, nil
}
if err != nil {
return false, err
}
}
return false, nil
}
func TryLock(conn redis.Conn, resource string, token string, DefaulTimeout int) (lock *Lock, ok bool, err error) {
return TryLockWithTimeout(conn, resource, token, DefaulTimeout)
}
func TryLockWithTimeout(conn redis.Conn, resource string, token string, timeout int) (lock *Lock, ok bool, err error) {
lock = &Lock{resource, token, conn, timeout}
ok, err = lock.tryLock()
if !ok || err != nil {
lock = nil
}
return
}
func main() {
fmt.Println("start")
DefaultTimeout := 10
conn, err := redis.Dial("tcp", "localhost:6379")
lock, ok, err := TryLock(conn, "xiaoru.cc", "token", int(DefaultTimeout))
if err != nil {
log.Fatal("Error while attempting lock")
}
if !ok {
log.Fatal("Lock")
}
lock.AddTimeout(100)
time.Sleep(time.Duration(DefaultTimeout) * time.Second)
fmt.Println("end")
defer lock.Unlock()
}
这段golang代码运行后的正常结果是:
$ go run lock.go
start
10
11
end
如果同时起多个进程去测试,会遇到这么一个结果:
$ go run lock.go
start
2016/03/23 01:23:22 Lock
exit status 1
2、promes告警事件进行加锁
后台是分布式的admin程序,分布在几台机器上,同时去消费kafka中的告警信息事件,对于同一个id的有着不同的事件,对于同一个id可能在不同的实例进行事件的处理,比如一个发生事件,一个恢复事件,这个时候就要使用分布式锁了。
set id EX 10 NX
Redis分布式锁实现乐观锁、悲观锁
乐观锁的实现
乐观锁实现中的锁就是商品的键值对。使用jedis的watch方法监视商品键值对,如果事务提交exec时发现监视的键值对发生变化,事务将被取消,商品数目不会被改动。
1).multi,开启Redis的事务,置客户端为事务态。
2).exec,提交事务,执行从multi到此命令前的命令队列,置客户端为非事务态。
3).discard,取消事务,置客户端为非事务态。
4).watch,监视键值对,作用时如果事务提交exec时发现监视的监视对发生变化,事务将被取消。
悲观锁实现
悲观锁中的锁是一个唯一标识的锁lockKey和该锁的过期时间。首先确定缓存中有商品,然后在拿数据(商品数目改动)之前先获取到锁,之后对商品数目进行减一操作,操作完成释放锁,一个秒杀操作完成。这个锁是基于redis的setNX操作实现的阻塞式分布式锁。