漫谈数据库锁

来源:互联网 发布:图标比例累加数据图 编辑:程序博客网 时间:2024/04/28 18:27

前言

       前段时间在线上和项目当中遇到了很多关于用mysqlGET_LOCK()函数获取锁导致的一些问题,主要有两类问题:

        1、一个连接中不能同时获取两把锁,因为获取后一个的时候会自动释放前面一把锁;另外如果获得锁或者释放锁所使用的connection不一样,假如获得锁的connection被连接池回收了,也可能会导致第一把锁自动释放,最终导致你的业务还没有处理完,别人也同时处理相同的业务,最终导致业务的不一致。

        2、为了解决第一个问题,很多同学把获得锁和释放锁放在一个事务当中,在获得锁和释放锁中间做了大量的耗时的业务逻辑(甚至这些业务逻辑根本就没有涉及到数据库操作),例如调用了几个外部系统的tr服务,或者批量处理功能;导致一个连接被占用过长时间,假如并发量一大,不但会导致接口性能下降,还会导致数据库连接不够,直接导致系统大面积瘫痪,非常危险。

以下是mysql官方文档对GET_LOCK()函数的详细说明,如果大家仔细并研究过这个函数,我相信大家一定不会轻易使用GET_LOCK()的场景。

        加锁:"SELECTGET_LOCK('{$key}', {$timeout}) AS get_lock";
        解锁:"SELECTRELEASE_LOCK('{$key}') AS release_lock";

  • GET_LOCK(str,timeout)

        设法使用字符串str 给定的名字得到一个锁, 超时为timeout 秒。若成功得到锁,则返回 1,若操作超时则返回0 (例如,由于另一个客户端已提前封锁了这个名字 ),若发生错误则返回NULL (诸如缺乏记忆或线程mysqladmin kill 被断开 )。假如你有一个用GET_LOCK()得到的锁,当你执行RELEASE_LOCK()或你的连接断开(正常或非正常)时,这个锁就会解除。

        这个函数可用于执行应用程序锁或模拟记录锁定。名称被锁定在服务器范围内。假如一个名字已经被一个客户端封锁,GET_LOCK() 会封锁来自另一个客户端申请封锁同一个名字的任何请求。这使对一个封锁名达成协议的客户端使用这个名字合作执行建议锁。然而要知道它也允许不在一组合作客户端中的一个客户端封锁名字,不论是故意的还是非故意的,这样阻止任何合作中的客户端封锁这个名字。一个减少这种情况发生的办法就是使用数据库特定的或应用程序特定的封锁名。例如,使用db_name.str或 app_name.str 形式的封锁名。

       mysql> SELECT GET_LOCK('lock1',10);

               -> 1

       mysql> SELECT IS_FREE_LOCK('lock2');

               -> 1

       mysql> SELECT GET_LOCK('lock2',10);

              -> 1

       mysql> SELECT RELEASE_LOCK('lock2');

               -> 1

       mysql> SELECT RELEASE_LOCK('lock1');

               -> NULL

        注意,第二个 RELEASE_LOCK()调用返回 NULL ,原因是锁'lock1' 被第二个GET_LOCK()调用解开。

使用另外一种办法解决和避免上面的问题

        mysql的GET_LOCK()函数除了上面的那个问题之外,还会存在以下问题:

  • 系统严重依赖了数据库类型,也就是严重依赖了mysql,假如以后数据库需要迁移到oracle或者其他数据库,整个应用层的代码全部都需要修改,这不是一个好的设计方案。

所以为了解决这些问题我们必须采用其他的解决方案,目前主要有两种解决方案:

        1、采用其他类似memcached之类的全局缓存锁

        2、采用CAS思想的基于数据库的乐观锁,但不需要依赖数据库类型

        但有时候,并不是所有的系统都需要tair或者memcached之类的缓存,如果仅仅为了锁而申请缓存,从而导致系统严重依赖了一个外部环境,所以并不经济划算,所以下面主要着重介绍第二种方案。

        其实我们可以为每个库设计一张非常简单的表,例如表名叫做lock

name

version

gmt_modified

lock1

0

2015-04-19 00:00:00

         表的结构非常简单,只有两列属性,一列代表锁的名称,一列代表锁当前的状态。

         假如我们要获取锁,只需要执行一条update lock set version=1 where name='lock1' and version=0,如果执行成功表示获得锁成功,否则表示获得锁失败;同样我们要释放锁,我们只需要执行一条update lock set version=0 where name='lock1' and version=1,如果执行成功表示释放锁成功,否则表示释放锁失败。

         在获取锁和释放锁的时候不需要求在同一个事务里面,也不需要是同一个连接,也不会出现获取另外一把锁同时会自动释放前面一把锁,你可以在获得锁和释放锁中间做大量的业务逻辑操作,不会导致占用数据连接过长时间,并且也没有强依赖数据库类型,以后做数据库迁移也不用改代码;并且lock也不会太多条数据,耗时基本上在毫秒级,性能上也得到了保证。

         当然,这种方案可能会出现系统发布或者其他异常情况下导致获得锁和释放锁中间断开,从而导致一直无法释放锁,但是如果我们能够做好补偿机制,例如获得锁不成功的次数超过一定量,我们自动释放锁;或者锁占用的时间超过了一定的时间,我们自动释放锁;或者监控报警,然后人工处理等等;这些补偿方案也能够达到我们的预期。

0 0
原创粉丝点击