数据库加锁,乐观锁、悲观锁

一、概念

悲观锁(Pessimistic Lock)

  • 每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁(读锁、写锁、行锁等),确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。

乐观锁(Optimistic Lock)

  • 每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。一般会使用版本号机制或CAS操作实现

  • version方式实现乐观锁:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

    • sql代码:
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};  

添加version字段,而不使用查到的目标值做判断,应为存在数值被改动,又被改回来的操作(如:同时有人买了某件商品,然后又退货,这个对于商品剩余的操作。),而用version每次加一,每次操作都是唯一的,,这样的结果更准确。

对于数据库的操作,是有返回值的(返回一个整数),可以根据返回值,判断是否执行成功(影响到0行。因为失败并不一定是错误,只是不符合条件的更新失败)

  • CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

使用场景

  • 悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

  • 乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

  • 总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。


二、具体实现方式

sql层面

例1:例如需要将用户id为1的用户资产增加100元,mysql并发操作加锁实现方式如下两种:

  • 1.悲观锁
-- 查询时直接加锁(强制锁定该条记录的操作,直到自己操作完成)
-- SELECT FROM ... FOR UPDATE--
SELECT amount FROM user WHERE id = 1 FOR UPDATE;
-- 更新用户资产
UPDATE user amout = amount+100 WHERE id = 1;

SELECT ... FOR UPDATE 的用法。由于InnoDB 预设是Row-Level Lock,所以只有「明确」的指定主键或者其他索引的键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则mysql 将会执行Table Lock (将整个数据表单给锁住)。
只锁住被选取的数据的好处是:多线程时,如果其他线程使用的是非锁住的数据,则不会受影响,无需等待解锁,更好的实现了并发。

  • 2.乐观锁

代码为展示sql逻辑

-- 查询用户资产
SELECT amount FROM user WHERE id = 1;
-- 将amount赋值给更新条件(这里为业务代码内设置临时变量,进行复制操作)
var amountTemp = amount;
-- 带条件更新用户资产
UPDATE user amout = amount+100 WHERE id = 1 AND amount = amountTemp;
  • 3.利用redis单线程模式加锁

Django层面

可以在manager层面对表数据设置锁,在操作时进行上锁、操作数据、解锁操作。

class LockingManager(models.Manager):
    """ Add lock/unlock functionality to manager.
    1.将lock/unlock功能添加到manager
    2.在建表时对其上锁(见下实例)

    """    

    def lock(self):
        """ Lock table. 
        锁定对象模型表,以便可以进行原子更新。
        模拟数据库访问请求挂起,直到锁解锁()为止。

        注意:如果需要锁定多个表,则需要把它们
        所有锁定在一个SQL子句,并且仅仅这样是不够的。 
        为了避免死锁,所有表必须以相同的顺序锁定。

        See http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html
        """
        cursor = connection.cursor()
        table = self.model._meta.db_table
        logger.debug("Locking table %s" % table)
        cursor.execute("LOCK TABLES %s WRITE" % table)
        row = cursor.fetchone()
        return row

    def unlock(self):
        """ Unlock the table. """
        cursor = connection.cursor()
        table = self.model._meta.db_table
        cursor.execute("UNLOCK TABLES")
        row = cursor.fetchone()
        return row 
        

应用实例:

class Job(models.Model):

    manager = LockingManager()  # 对表进行加锁

    counter = models.IntegerField(null=True, default=0)

    @staticmethod
    def do_atomic_update(job_id)
        ''' 更新job表下的counter这个integer, 保持它小于5 '''
        try:
            # 确保只有一个 HTTP 请求可以执行此次更新
            Job.objects.lock()

            job = Job.object.get(id=job_id)
            
            # 如果我们不锁定表,则两个同时发出的请求可能会使计数器超过5
            if job.counter < 5:
                job.counter += 1                                        
                job.save()

        finally:
            Job.objects.unlock()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章