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

前言

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

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

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

  由于后期必然会演变成分布式架构,而各个结构又是集群,因此如果当前情况下就把项目构架得太多复杂,投入再多的人力也是很难完成的,因此要先简化结构,一步步实现,至于先集群还是先分布,看个人喜好了。

实现

  实现的主要目标就是保证任意时刻,只能有一个线程可以得到操作的权利。

  首先来定义锁的接口,可以提供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);
}

结束语

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

posted @ 2016-01-08 09:08  ahl5esoft  阅读(1148)  评论(3编辑  收藏  举报