阅读 357

浅入浅出分布式锁

写在前面

以前懵懂无知的时候,偶然接触到分布式锁一词,颇感兴趣。搜索引擎一气呵成,点开瞅了瞅几篇高赞文章,要么开篇畅谈分布式理论与CAP,要么对比着各种技术栈大显神通。虽是精华,只可惜年少无知只能望而却步,默默点个收藏后就关掉了浏览器。后续多多少少也用到分布式锁,也并未去大刀阔斧的去深入研究,只是刚好业务需要。本文将以一个简单的场景为例,叙述一下个人粗浅的认知。
注:诣在科普,浅入浅出。

场景引入

当一个新项目上线时,有时候会有一些需要执行或者调度一次的操作。举个例子,新上线项目,大部分时候数据表都是空的,一般情况下会准备一些预置数据,在服务启动前,将这些预置数据插入到数据库中。部分支持DDL的orm框架如JPAgorm,小项目甚至会使用其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)做限制,从而实现分布式锁。

实现分布式锁的方式有很多,只是根据业务场景进行技术的选型的问题。分布式锁本质上也是为了解决数据一致性的问题。

文章分类
后端
文章标签