深入理解【python3】分布式锁

image.png

分布式锁是一种用于协调多个进程或线程之间访问共享资源的机制,它可以避免多个进程或线程同时对共享资源进行修改而导致的数据不一致问题。在分布式系统中,由于数据的分散存储在不同的节点上,因此需要一种可靠的分布式锁机制。

分布式锁通常需要满足以下几个条件:

  1. 互斥性:在任何时刻,只能有一个进程或线程获得锁。
  2. 安全性:一旦一个进程或线程获得锁,其他进程或线程无法修改该锁的状态,只有锁的持有者可以释放锁。
  3. 高可用性:分布式锁应该具有高可用性,即当某个节点或进程故障时,其他节点或进程可以接管该锁。
  4. 性能:分布式锁应该具有高性能,即在高并发的情况下,锁的获取和释放应该尽量快速 当多个协程/线程/进程同时读写一个共享资源时,如果没有锁的情况下,会造成数据损坏。

例如一个秒杀活动中,商品123的库存都是100件,同时有2人参与秒杀(贱笑了*-*);假设有2个进程/线程/协程同一时刻对秒杀库存进行读写,各自将库存数目按照订单减库存,那么库存的中商品的数目最终会是多少呢?

并发下扣减库存

库存表

image.png

以下所有测试用例,均将数据还原至以上初始数据

实验环境

mysql 
redis
python3及peewee库

不加锁的情况

#! -*-conding=: UTF-8 -*-
# 2023/8/10 19:04
import random
import time
from datetime import datetime
import threading

from peewee import *
from playhouse.shortcuts import ReconnectMixin
from playhouse.pool import PooledMySQLDatabase

class ReconnectMySQLDatabase(ReconnectMixin, PooledMySQLDatabase):
    pass

db = ReconnectMySQLDatabase("inventory", host="192.168.91.1", port=3306, user="root", password="root")

# 删除 - 物理删除和逻辑删除 - 物理删除  -假设你把某个用户数据 - 用户购买记录,用户的收藏记录,用户浏览记录啊
# 通过save方法做了修改如何确保只修改update_time值而不是修改add_time
class BaseModel(Model):
    add_time = DateTimeField(default=datetime.now, verbose_name="添加时间")
    is_deleted = BooleanField(default=False, verbose_name="是否删除")
    update_time = DateTimeField(verbose_name="更新时间", default=datetime.now)

    def save(self, *args, **kwargs):
        # 判断这是一个新添加的数据还是更新的数据
        if self._pk is not None:
            # 这是一个新数据
            self.update_time = datetime.now()
        return super().save(*args, **kwargs)

    @classmethod
    def delete(cls, permanently=False):  # permanently表示是否永久删除
        if permanently:
            return super().delete()
        else:
            return super().update(is_deleted=True)

    def delete_instance(self, permanently=False, recursive=False, delete_nullable=False):
        if permanently:
            return self.delete(permanently).where(self._pk_expr()).execute()
        else:
            self.is_deleted = True
            self.save()

    @classmethod
    def select(cls, *fields):
        return super().select(*fields).where(cls.is_deleted == False)

    class Meta:
        database = db

class Inventory(BaseModel):
    # 商品的库存表
    # stock = PrimaryKeyField(Stock)
    goods = IntegerField(verbose_name="商品id", unique=True)
    stocks = IntegerField(verbose_name="库存数量", default=0)
    version = IntegerField(verbose_name="版本号", default=0)  # 用于分布式锁的乐观锁

def sell():
    # 多线程下的并发带来的数据不一致的问题
    goods_list = [(1, 10), (2, 20), (3, 30)]
    with db.atomic() as txn:
        # 超卖
        for goods_id, num in goods_list:
            # 查询库存
            goods_inv = Inventory.get(Inventory.goods == goods_id)
            print(f"商品{goods_id} 售出 {num}件")
            time.sleep(random.randint(1, 3))  # 增加并发问题的拟态实现

            if goods_inv.stocks < num:
                print(f"商品:{goods_id} 库存不足")
                txn.rollback()
                break
            else:
                goods_inv.stocks -= num
                goods_inv.save()

def create_data():
    db.create_tables([Inventory])
    for i in range(5):
        goods_inv = Inventory(goods=i, stocks=100)
        goods_inv.save()

if __name__ == "__main__":
    # create_data()

    t1 = threading.Thread(target=sell)
    t2 = threading.Thread(target=sell)
    t1.start()
    t2.start()

    t1.join()
    t2.join()

输出结果为:

商品1 售出 10件
商品1 售出 10件
商品2 售出 20件
商品3 售出 30件
商品2 售出 20件
商品3 售出 30件

image.png

  1. 数据库连接和配置: 代码中使用了 PooledMySQLDatabase 对象连接MySQL数据库,具体配置为连接到IP地址为 192.168.91.1 的MySQL服务器,使用用户名 root 和密码 root 连接到数据库 inventory
  2. 基础模型 BaseModel: 定义了一个基础模型 BaseModel,其中包含了添加时间、更新时间、是否删除等字段的定义。此模型有一些方法,如保存(save)、删除(delete)和查询(select)等。
  3. 库存模型 Inventory: 基于 BaseModel 定义了一个库存模型 Inventory,其中包含商品id、库存数量、版本号等字段的定义。
  4. 库存售卖函数 sell: 这个函数用于模拟售卖商品的操作,使用多线程处理多个商品的售卖。在售卖过程中,先查询库存,然后根据库存数量进行扣减,但存在超卖问题,因为并发情况下会导致库存不足。
  5. 创建数据函数 create_data: 这个函数用于创建初始的商品库存数据,将5种商品的库存数量都设置为100。
  6. 多线程处理售卖操作: 代码主要在 __name__ == "__main__" 的分支中运行。首先通过 create_data() 函数创建初始库存数据,然后使用两个线程并发运行 sell() 函数模拟售卖商品。但由于多线程并发问题,可能会导致库存不足和超卖等问题。

问题:应该在更新的时候根据当前的数据更新。

不加锁(根据实时数据扣减库存)

修改代码如下(修改了更新数据的逻辑):

#! -*-conding=: UTF-8 -*-
# 2023/8/10 19:04
import random
import time
from datetime import datetime
import threading

from peewee import *
from playhouse.shortcuts import ReconnectMixin
from playhouse.pool import PooledMySQLDatabase

class ReconnectMySQLDatabase(ReconnectMixin, PooledMySQLDatabase):
    pass

db = ReconnectMySQLDatabase("inventory", host="192.168.91.1", port=3306, user="root", password="root")

# 删除 - 物理删除和逻辑删除 - 物理删除  -假设你把某个用户数据 - 用户购买记录,用户的收藏记录,用户浏览记录啊
# 通过save方法做了修改如何确保只修改update_time值而不是修改add_time
class BaseModel(Model):
    add_time = DateTimeField(default=datetime.now, verbose_name="添加时间")
    is_deleted = BooleanField(default=False, verbose_name="是否删除")
    update_time = DateTimeField(verbose_name="更新时间", default=datetime.now)

    def save(self, *args, **kwargs):
        # 判断这是一个新添加的数据还是更新的数据
        if self._pk is not None:
            # 这是一个新数据
            self.update_time = datetime.now()
        return super().save(*args, **kwargs)

    @classmethod
    def delete(cls, permanently=False):  # permanently表示是否永久删除
        if permanently:
            return super().delete()
        else:
            return super().update(is_deleted=True)

    def delete_instance(self, permanently=False, recursive=False, delete_nullable=False):
        if permanently:
            return self.delete(permanently).where(self._pk_expr()).execute()
        else:
            self.is_deleted = True
            self.save()

    @classmethod
    def select(cls, *fields):
        return super().select(*fields).where(cls.is_deleted == False)

    class Meta:
        database = db

class Inventory(BaseModel):
    # 商品的库存表
    # stock = PrimaryKeyField(Stock)
    goods = IntegerField(verbose_name="商品id", unique=True)
    stocks = IntegerField(verbose_name="库存数量", default=0)
    version = IntegerField(verbose_name="版本号", default=0)  # 用于分布式锁的乐观锁

def sell():
    # 多线程下的并发带来的数据不一致的问题
    goods_list = [(1, 99), (2, 20), (3, 30)]
    with db.atomic() as txn:
        # 超卖
        for goods_id, num in goods_list:
            # 查询库存
            goods_inv = Inventory.get(Inventory.goods == goods_id)
            
            time.sleep(random.randint(1, 3))

            if goods_inv.stocks < num:
                print(f"商品:{goods_id} 库存不足")
                txn.rollback()
                break
            else:
                # 让数据库根据自己当前的值更新数据
                query = Inventory.update(stocks=Inventory.stocks - num).where(Inventory.goods == goods_id)
                ok = query.execute()
                print(f"商品{goods_id} 售出 {num}件")
                if ok:
                    print("更新成功")
                else:
                    print("更新失败")

def create_data():
    db.create_tables([Inventory])
    for i in range(5):
        goods_inv = Inventory(goods=i, stocks=100)
        goods_inv.save()

if __name__ == "__main__":
    # create_data()

    t1 = threading.Thread(target=sell)
    t2 = threading.Thread(target=sell)
    t1.start()
    t2.start()

    t1.join()
    t2.join()

输出结果为:

商品1 售出 99件
商品1 售出 99件
更新成功
商品2 售出 20件
更新成功
商品3 售出 30件
更新成功
更新成功
商品2 售出 20件
更新成功
商品3 售出 30件
更新成功

咦,商品售出99件后为啥还能售出第二次99件?还是出现超卖现象了!!读→更新这里不是原子的。

数据库里的数据页证明了超卖了:

image.png

这还是不能处理并发问题。

加锁

单实例锁

from datetime import datetime
import threading
import time
from random import randint

from peewee import *
from playhouse.shortcuts import ReconnectMixin
from playhouse.pool import PooledMySQLDatabase

class ReconnectMySQLDatabase(ReconnectMixin, PooledMySQLDatabase):
    pass

db = ReconnectMySQLDatabase("inventory", host="192.168.91.1", port=3306, user="root", password="root")

# 删除 - 物理删除和逻辑删除 - 物理删除  -假设你把某个用户数据 - 用户购买记录,用户的收藏记录,用户浏览记录啊
# 通过save方法做了修改如何确保只修改update_time值而不是修改add_time
class BaseModel(Model):
    add_time = DateTimeField(default=datetime.now, verbose_name="添加时间")
    is_deleted = BooleanField(default=False, verbose_name="是否删除")
    update_time = DateTimeField(verbose_name="更新时间", default=datetime.now)

    def save(self, *args, **kwargs):
        # 判断这是一个新添加的数据还是更新的数据
        if self._pk is not None:
            # 这是一个新数据
            self.update_time = datetime.now()
        return super().save(*args, **kwargs)

    @classmethod
    def delete(cls, permanently=False):  # permanently表示是否永久删除
        if permanently:
            return super().delete()
        else:
            return super().update(is_deleted=True)

    def delete_instance(self, permanently=False, recursive=False, delete_nullable=False):
        if permanently:
            return self.delete(permanently).where(self._pk_expr()).execute()
        else:
            self.is_deleted = True
            self.save()

    @classmethod
    def select(cls, *fields):
        return super().select(*fields).where(cls.is_deleted == False)

    class Meta:
        database = db

class Inventory(BaseModel):
    # 商品的库存表
    # stock = PrimaryKeyField(Stock)
    goods = IntegerField(verbose_name="商品id", unique=True)
    stocks = IntegerField(verbose_name="库存数量", default=0)
    version = IntegerField(verbose_name="版本号", default=0)  # 分布式锁的乐观锁

R = threading.Lock()

def sell():
    # 多线程下的并发带来的数据不一致的问题
    goods_list = [(1, 10), (2, 20), (3, 99)]
    with db.atomic() as txn:
        # 超卖
        for goods_id, num in goods_list:
            # 查询库存
            with R:
                goods_inv = Inventory.get(Inventory.goods == goods_id)

                time.sleep(randint(1, 3))
                if goods_inv.stocks < num:
                    print(f"商品:{goods_id} 库存不足")
                    txn.rollback()
                    break
                else:
                    # 让数据库根据自己当前的值更新数据, 这个语句能不能处理并发的问题
                    query = Inventory.update(stocks=Inventory.stocks - num).where(Inventory.goods == goods_id)
                    ok = query.execute()
                    print(f"商品{goods_id} 售出 {num}件")
                    if ok:
                        print("更新成功")
                    else:
                        print("更新失败")

def create_data():
    db.create_tables([Inventory])
    for i in range(5):
        goods_inv = Inventory(goods=i, stocks=100)
        goods_inv.save()

if __name__ == "__main__":
    # create_data()

    t1 = threading.Thread(target=sell)
    t2 = threading.Thread(target=sell)
    t1.start()
    t2.start()

    t1.join()
    t2.join()

输出结果为:

商品1 售出 10件
更新成功
商品2 售出 20件
更新成功
商品3 售出 99件
更新成功
商品1 售出 10件
更新成功
商品2 售出 20件
更新成功
商品:3 库存不足

单体服务中这样实现是可以的,但是在微服务中,普通的锁机制失效。

image.png

MySQL分布式锁

基于mysql的悲观锁实现

悲观锁适用于对并发要求不高但需要确保操作的一致性的场景

  • 悲观锁概念:顾名思义,就是对于数据的处理持悲观态度,总认为会发生并发冲突,获取和修改数据时,别人会修改数据;所以在整个数据处理过程中,需要将数据锁定

  • 悲观锁的实现:通常依靠数据库提供的锁机制实现,比如mysql的排他锁,select … for update来实现悲观锁;例如,商品秒杀过程中,库存数量的减少,避免出现超卖的情况

image.png

mysql中的悲观锁实现:for update
  • mysql请求一把锁for update

  • 使用for update的时候要注意:每个语句mysql都是默认提交的

  • 需要关闭autocommit:set autocommit=0;(注意这个只针对当前窗口有效,不是全局的);(查询select @@autocommit;

  • 具体执行逻辑:select * from inventary where goods=1 for update;

  • 释放锁:commit;

for update的本质
  • 其实是行锁,只会锁住满足条件的数据,where goods=1where goods=2这2个是不会触发锁的

  • 如果条件部分没有索引goods,那么行锁会升级成表锁

  • 锁只是锁住要更新的语句for update,普通的查询不会锁住

  • 如果没有满足条件,不会锁表

使用悲观锁来实现防止超卖的效果,可以使用数据库的行级锁来保证在读取库存时进行锁定,从而避免并发问题。以下是使用悲观锁来实现的示例代码:

#! -*-conding=: UTF-8 -*-
# 2023/8/11 15:07

from datetime import datetime
import threading
from peewee import *

# 定义数据库连接
db = MySQLDa
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值