写在前面
以前懵懂无知的时候,偶然接触到分布式锁一词,颇感兴趣。搜索引擎一气呵成,点开瞅了瞅几篇高赞文章,要么开篇畅谈分布式理论与CAP,要么对比着各种技术栈大显神通。虽是精华,只可惜年少无知只能望而却步,默默点个收藏后就关掉了浏览器。后续多多少少也用到分布式锁,也并未去大刀阔斧的去深入研究,只是刚好业务需要。本文将以一个简单的场景为例,叙述一下个人粗浅的认知。
注:诣在科普,浅入浅出。
场景引入
当一个新项目上线时,有时候会有一些需要执行或者调度一次的操作。举个例子,新上线项目,大部分时候数据表都是空的,一般情况下会准备一些预置数据,在服务启动前,将这些预置数据插入到数据库中。部分支持DDL的orm框架如JPA
、gorm
,小项目甚至会使用其auto migrate
的功能,上线的时候自动建表。
以前者为例,提供一个PrepareData
的方法,大致代码如下
func PrepareData() {
// 获取MySQL连接
conn, err := GetMySQLConnection()
if err != nil {
panic("Get MySQL Connection Fail")
}
// 读取预置数据的SQL文件
sql, err := IOReader.Read(FILE_PATH)
if err != nil {
panic("Read SQL File Fail")
}
// 执行
conn.Exec(sql)
}
复制代码
然后在服务启动前调用一次PrepareData
即可。
在以前,上述流程是没有问题的,但互联网演进到今天,为了服务高可用,很少会单机器单实例地去部署服务(毕竟挂了就没了),一般会选择集群并行部署,再由上层转发服务器去做负载均衡。这种情况下上述流程就不适用了,每部署一个机器就会执行一次PrepareData
,最后会导致脏数据或者数据库报错的情况。
此时就需要用到分布式锁,部署时所有机器去竞争这个锁,拿到锁才去执行PrepareData
,保证只执行一次。
锁
回想一下之前大学课堂学习线程的时候,开启多线程对一个初始值为0的变量做同等次数的+1和-1,结果不为0的例子,这种情况下就需要加锁去处理。锁往往和资源紧密结合,当资源不可抢占时,并发访问的情况下需要使用锁来限制对资源的访问,以此来保护资源。
上述是一台计算机多进程多线程情况使用的锁。当锁的场景上升到多服务器的情况下,也就是所谓的分布式应用,不同机器的进程线程去竞争资源的时候,锁就需要升级为分布式锁。
redis分布式锁
redis的set命令有几个option,完整的redis命令如下
SET key value [EX seconds] [PX milliseconds] [NX|XX]
复制代码
当使用NX选项时,表示当key不存在时,该命令才会执行成功,如果key已存在则不做任何处理。
redis命令原子性的特点,我们可以基于此来实现分布式锁,服务器访问时调用redis执行该命令,如果成功则为抢锁成功。
远古时期,redis有这一条命令
SETNX key value 复制代码
功能类似但是该命令不能同时设置超时时间,极端情况可能会出现系统死锁。比如取到锁,业务处理完成后,需要解锁操作,但在业务处理过程中服务器宕机了,这种情况就死锁了。
在我们引入的场景比较简单,不需要解锁的操作,使用分布式锁后代码大致修改如下
func getLock(lockName string) error {
// 获取redis连接
conn, err := GetRedisConnection()
if err != nil {
logger.Error("Get Redis Connection Fail")
return err
}
if err := conn.SetNx(lockName, 1, LOCK_TIMEOUT); err != nil {
logger.Info("Get Lock Fail")
return err
}
return nil
}
func PrepareData() {
if err := getLock(PREPARE_DATA_LOCK); err != nil {
return
}
// 获取MySQL连接
conn, err := GetMySQLConnection()
if err != nil {
panic("Get MySQL Connection Fail")
}
// 读取预置数据的SQL文件
sql, err := IOReader.Read(FILE_PATH)
if err != nil {
panic("Read SQL File Fail")
}
// 执行
conn.Exec(sql)
}
复制代码
小结
通篇只介绍了基于redis的分布式锁的方法,主流还有zookeeper、etcd等实现等(然而我也只用过redis)。只要是一种中间存储介质,理论上都可以实现分布式锁,如果业务场景能够接受,甚至可以用MySQL的行锁来实现。稍微延伸一点思考,甚至可以手动部署一个http或rpc服务,当需要锁时调用服务,访问该服务内的全局变量,全局变量使用互斥锁(如用Java里的synchronized)做限制,从而实现分布式锁。
实现分布式锁的方式有很多,只是根据业务场景进行技术的选型的问题。分布式锁本质上也是为了解决数据一致性的问题。