项目演化系列--分布式锁

前言git

  项目初期的时候,通常会发布到一台主机上,当达到负载极限时,要想提高其性能,要么提高硬件,要么多台主机,然而成本上的花销,后者比前者便宜太多了,虽然便宜,可是却更加复杂。github

  大多数编程语言提供的各类锁只会对同一项目的同一主机的代码产生做用,当同一项目发布在多台主机的时候,这些主机中的项目要造成一个总体,所以原先同步访问共享资源的代码将会失去效果。redis

  因为共享资源多种多样,如:文件、业务的临时状态、数据库数据等,本章的同步锁主要解决的是不依赖于主机环境的共享资源,如:数据库数据;而共享资源依赖于项目环境时,想要同步访问共享资源,则当某主机共享资源变更时,须要将其同步到其余主机,也就是集群服务器了,若是不想要搭建集群服务器,可将相应的功能剥离出来成为单一的项目,也就是分布式结构。sql

  因为后期必然会演变成分布式架构,而各个结构又是集群,所以若是当前状况下就把项目构架得太多复杂,投入再多的人力也是很难完成的,所以要先简化结构,一步步实现,至于先集群仍是先分布,看我的喜爱了。数据库

实现编程

  实现的主要目标就是保证任意时刻,只能有一个线程能够获得操做的权利。api

  首先来定义锁的接口,能够提供2个方法:Lock、Unlock,也能够只提供Lock,而后返回Unlock,若是Unlock为null则表示加锁失败。缓存

  既然讲到惟一,若是不依赖其余的额外资源的状况下,不少人应该已经想到了,那就是数据库表的主键,所以实现思路就是加锁的时候向数据库中插入一条记录,那么成功插入的操做就获取到了锁,而后解锁时,删除这条记录便可,初步实现以下:安全

public delegate void UnlockDelegate();

public UnlockDelegate Lock(string key)
{
    using (var conn = new SqlConnection(this.connectionString))
    {
        conn.Open();
        using (var cmd = new SqlCommand(string.Empty, conn))
        {
            try
            {
                var createdRows = Create(cmd, key);
                if (createdRows > 0)
                {
                    return () =>
                    {
                        DeleteById(cmd, key);
                        conn.Close();
                    };
                }
            }
            catch
            {
                conn.Close();
            }
            return null;
        }
    }
}

private int Create(SqlCommand cmd, string key)
{
    cmd.Parameters.Clear();
    cmd.CommandText = this.insertSql;
    cmd.Parameters.AddWithValue("@id", key);
    return cmd.ExecuteNonQuery();
}

private int DeleteById(SqlCommand cmd, string key)
{
    cmd.Parameters.Clear();
    cmd.CommandText = this.deleteSql;
    cmd.Parameters.AddWithValue("@id", key);
    return cmd.ExecuteNonQuery();
}

  项目运行过程中没有绝对的安全,总有一些内因、外因致使项目出现错误,若是某个主机获取了锁之后,该主机由于某些缘由没有释放锁,那么其余的主机将会没法再获取到该锁了。服务器

  那么锁就须要一个过时时间,所以咱们须要在表中增长一个表示锁的建立时间,那么在建立锁以前就须要先根据key去获取锁是否存在,若是存在且已通过期,那么删除该记录才能继续建立锁。

  该处的删除跟解锁时的删除是不同的,由于在多线程、并发环境下,程序并不能保证只有惟一一个线程获取到了已存在的锁数据,有可能多个线程都获取到了锁数据,有的可能已经准备删除,而有的才刚刚获取到,所以此处的删除必须保证返回的影响行数大于0,不然直接返回null,重构后的代码以下:

public UnlockDelegate Lock(string key, int expires = 5)
{
    using (var conn = new SqlConnection(this.connectionString))
    {
        conn.Open();
        using (var cmd = new SqlCommand(string.Empty, conn))
        {
            var createdOn = GetCreatedOnById(cmd, key);
            if (createdOn > 0)
            {
                var nowOn = DateTime.Now.ToUnix();
                if (nowOn - createdOn > expires)
                {
                    var deletedRow = DeleteById(cmd, key);
                    if (deletedRow == 0)
                    {
                        conn.Close();
                        return null;
                    }
                }
            }

            try
            {
                var createdRows = Create(cmd, key, expires);
                if (createdRows > 0)
                {
                    return () =>
                    {
                        DeleteById(cmd, key);
                        conn.Close();
                    };
                }
            }
            catch
            {
                conn.Close();
            }
            return null;
        }
    }
}

  因为DateTime并无直接转换成时间戳的方法,所以该方法须要本身扩展,实现思路就是当前时间-1970年的总毫秒数,这里就不提供代码了,由于长时间都是依赖于orm来开发的,对sql已经很生疏了,所以各位要的是理解以上实现,不要太在乎代码。

简化

  使用数据库来实现虽然代码量很少,但须要数据库的支持,链接字符串、表、字段都是可变的,若是不写死的话,就须要提供很多的配置。

  因为项目必然会使用到缓存,如:redis、memcache等高性能的缓存系统,而redis中提供了SetNX、Expires这样的api,若是基于redis实现的话,只要几行代码即可完成。

  相应的库能够去redis官网查询,这里的例子使用的是Sider,代码以下:

private ThreadwisePool pool;

public RedisMutex(string host)
{
    this.pool = new ThreadwisePool(host);
}

public UnlockDelegate Lock(string key, int expires = 5)
{
    var client = this.pool.GetClient();
    var ok = client.SetNX(key, string.Empty);
    if (!ok)
        return null;

    client.Expire(key, new TimeSpan(0, 0, expires));
    return () => client.Del(key);
}

结束语

  那么今天分享的文章就到这里了,若是代码有错误或者有问题的话,请留言,谢谢。

相关文章
相关标签/搜索