mysql lock table/unlock table 表锁/解锁

探讨了在并发环境下商品库存减少操作可能导致的问题,并提出了通过使用数据库锁表机制确保数据一致性的解决方案。

在某个地方看到有个例子,具体描述类似如下:商店现在某商品只有1件库存,然后A与B在网上进行下订,A与B几乎同时(或许也就差几毫秒,A比B快那么一点点)进行。

很明显是只有A才能成功下单的,B则会收到库存不足的提示,但是作为放置在服务端的那个页面(或者称为脚本程序)我们得怎样去处理这个问题呢?或者我先放出一段代码吧。

 代码如下 复制代码

    $sql = "select number from goods where id=1";
    $number = intval( $db->result( $db->query( $sql ), 0 ) );
    if ( $number > 0 ) {
    sleep( 2 );
    $sql = "update goods set number=number-1 where id = 1";
    if ( $db->query( $sql ) ) {
    echo 'Ok!Here you are!';
    } else {
    echo 'Sorry!Something Go wrong!Try it again.';
    }
    } else {
    echo 'No more!you are so late!';
    }

这部分代码除了缺少一定注释外都写得没错,当然$db是一个操作数据库的类,我只是将大部分方法封装了,这里的逻辑也是很明显了。

先获取id为1这个东东的库存数,看看是否为0,如果为0就订购不成功了,如果大于0则将库存减1然后提示ok。这确实没有任何错误,逻辑也对。如果请求是一个接一个地产生的,那么什么问题都没有,但当一些并发情况出现时就可能出现一些无厘头的问题了。你想啊,是不是可能存在一种情况,A刚发出请求,脚本处理到update之前B又发出请求,那么现在库存依然还有1,因为A的update还没有执行呢,所以$number不少于0,这次完了,B也下单了,于是库存变成-1了(假设原来只有1件),确实是一个荒谬而且比较搞笑的结果。

出现问题的原因很明显,就是忽略了这种并发情况的考虑,处理下订应该是种队列方式,也就是先来先得,就是说在执行这个下订动作是要排队的,前面的那个先下订然后后者才能下订,当然当后者下订前才再判断库存的数量。那么怎样解决这个问题呢,在程序层面上貌似真的没有方法去解决这个问题,所以在此才提到锁表的概念,上面出现这个问题的归根于没有控制一个select number的先后顺序(或者可以这么说吧),因为在A执行update之前你又允许B去查询库存,当然结果还是1,至少要等待A更新库存后才允许其他人的任何操作,也就是对goods表进行一个排队操作,对goods表进行锁定。

说到这里,请不要以为锁表有多么高深,其实它就是一条sql

    LOCK TABLE `table` [READ|WRITE]

解锁

    UNLOCK TABLES;

引用专业的描述是

LOCK TABLES为当前线程锁定表。 UNLOCK TABLES释放被当前线程持有的任何锁。当线程发出另外一个LOCK TABLES时,或当服务器的连接被关闭时,当前线程锁定的所有表会自动被解锁。 

如果一个线程获得在一个表上的一个READ锁,该线程和所有其他线程只能从表中读。 如果一个线程获得一个表上的一个WRITE锁,那么只有持锁的线程READ或WRITE表,其他线程被阻止。

已经是有种队列的味道,对不,所以解决方案很简单嘛,在select前加锁,执行完后面逻辑代码后解锁。或许有没有人会有一个疑问,就是如果万一锁表后线程就断掉了那么是不是就一直锁表了,这个确实是可能存在但是既然你想到了那么数据库的设计人员也一定考虑到了,可以告诉你关于unlock的一些资料:当线程发出另一个 LOCK TABLES,或当与服务器的连接被关闭时,被当前线程锁定的所有表将被自动地解锁。这下放心了吧。

好,看下改进后的代码。

   

 代码如下 复制代码
$db->lock( 'goods', 2 );
    $sql = "select number from goods where id=1";
    $number = intval( $db->result( $db->query( $sql ), 0 ) );
    if ( $number > 0 ) {
    sleep( 2 );
    $sql = "update goods set number=number-1 where id = 1";
    if ( $db->query( $sql ) ) {
    echo 'Ok!Here you are!';
    } else {
    echo 'Sorry!Something go wrong!Try it again.';
    }
    } else {
    echo 'No more!you are so late!';
    }
    $db->unlock();

只加了两行代码,不过也不能这么说,因为paperen我修改了自己那个操作数据库的类,加了两个方法lock与unlock,其实这两个方法也很简单。

 

 代码如下 复制代码
   /**
    * 锁表
    * @param string $table 表名
    * @param int $type 读锁1还是写锁2
    */
    public function lock( $table, $type = 1 ) {
    $type = ( $type == 1 ) ? 'READ' : 'WRITE';
    $this->query( "LOCK TABLE `$table` $type" );
    }
     
    /**
    * 解锁
    */
    public function unlock() {
    $this->query( "UNLOCK TABLES" );
    }
<think>我们正在解决的是MySQL存储过程或函数中出现的错误提示:'unlock is not allowed in stored procedures' 根据错误信息,可知在存储过程中不允许执行UNLOCK操作。在MySQL中,存储过程和函数有特定的操作限制。 参考之前对话中关于的讨论(特别是共享部分),我们知道在存储过程中可以使用LOCK TABLES和UNLOCK TABLES来管理。但是,根据MySQL官方文档,存储过程和函数中不允许使用UNLOCK TABLES,除非该存储过程或函数中已经执行了LOCK TABLES。 具体规则如下: 1. 在存储过程中,可以使用LOCK TABLES来,但必须在同一个存储过程中使用UNLOCK TABLES来解锁(在存储过程结束前解锁)。 2. 在存储函数中,不允许执行LOCK TABLES或UNLOCK TABLES语句。 因此,出现这个错误的原因可能是: 情况1:在存储函数中使用了UNLOCK TABLES(或LOCK TABLES),这是不允许的。 情况2:在存储过程中,UNLOCK TABLES出现在LOCK TABLES之前,或者没有对应的LOCK TABLES。 解决方案: 情况1:如果是存储函数,则不能使用显式的。考虑使用其他方式避免定,或者改用存储过程。 情况2:如果是存储过程,确保: - 在LOCK TABLES之后使用UNLOCK TABLES。 - 在同一个存储过程中,每一个LOCK TABLES都必须有对应的UNLOCK TABLES(不能嵌套,除非多次定同一个,但MySQL会合并请求)。 - 注意:不能在存储过程中解锁一个未在该存储过程中定的。 示例代码(存储过程): ```sql CREATE PROCEDURE example_procedure() BEGIN -- 必须先 LOCK TABLES table1 READ, table2 WRITE; -- 执行一些操作... -- 在结束前解锁 UNLOCK TABLES; END; ``` 但是,如果我们在存储过程中使用了LOCK TABLES,然后在未解锁的情况下再次定其他,或者忘记解锁,都可能导致问题。 另外,在存储过程中使用可能会导致并发性能下降,因此建议: - 尽量使用行级(通过SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE)来减少的粒度。 - 确保事务尽可能短,避免长时间持有。 注意:在存储过程中,使用行级(InnoDB)必须在事务中,并且事务的提交或回滚会释放所有的行。 因此,如果我们遇到在存储函数中需要的情况,由于存储函数不允许使用LOCK TABLES/UNLOCK TABLES,我们可以考虑: - 将函数改为存储过程(如果允许)。 - 使用GET_LOCK()和RELEASE_LOCK()函数来管理命名(这些是用户级别的,不是,但可以用于同步)。 例如: SELECT GET_LOCK('my_lock', 10); -- 获取名为'my_lock'的,等待10秒 ... 执行操作 ... SELECT RELEASE_LOCK('my_lock'); - 或者,在存储函数中避免使用,通过调整业务逻辑来实现。 总结步骤: 1. 确认错误发生的上下文:是在存储过程还是存储函数中? 2. 如果是存储函数: - 移除LOCK TABLES和UNLOCK TABLES语句。 - 考虑使用命名(GET_LOCK/RELEASE_LOCK)或者改用存储过程。 3. 如果是存储过程: - 检查LOCK TABLES和UNLOCK TABLES是否成对出现,且UNLOCK TABLES之前必须有LOCK TABLES。 - 确保没有在未定的情况下解锁。 - 确保在存储过程的所有出口(包括异常处理)都解锁。 4. 考虑使用事务和行级(InnoDB引擎)代替: - 在存储过程中,使用START TRANSACTION开始事务。 - 使用SELECT ... FOR UPDATE(排他)或SELECT ... LOCK IN SHARE MODE(共享)来定需要修改或读取的行。 - 在事务结束时(COMMIT或ROLLBACK)会自动释放。 示例(使用行级): ```sql CREATE PROCEDURE update_example() BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; -- 其他清理操作 END; START TRANSACTION; -- 对需要更新的行加排他 SELECT * FROM table1 WHERE id = 1 FOR UPDATE; -- 执行更新操作 UPDATE table1 SET column1 = 'new_value' WHERE id = 1; COMMIT; END; ``` 这样,我们就不需要显式的,从而避免在存储过程中使用LOCK TABLES和UNLOCK TABLES。 因此,针对错误'unlock is not allowed in stored procedures',我们应根据具体情况调整代码结构,避免在存储过程中不匹配的操作,并在存储函数中完全避免。 相关问题: 1. 如何在MySQL存储过程中正确使用? 2. 存储函数中可以使用哪些机制? 3. 行级在存储过程中的性能差异? 4. 如何利用MySQL的命名(GET_LOCK)实现并发控制? 5. 在存储过程中使用可能导致哪些常见问题? 注意:在存储过程中使用需要格外谨慎,避免死和长时间等待。 </think>### 解决 MySQL 存储过程中出现 'unlock is not allowed in stored procedures' 错误的方法 该错误示在存储过程/函数中尝试执行了不允许的 `UNLOCK` 操作。根本原因是 **MySQL 不允许在存储函数中显式调用操作**(但存储过程有限支持)。以下是完整解决方案: --- #### 一、错误原因分析 1. **存储函数 vs 存储过程**区别: - ✅ 存储过程支持 `LOCK TABLES` 和 `UNLOCK TABLES` - ❌ 存储函数**禁止**任何显式操作(设计约束)[^1] ```sql CREATE FUNCTION demo_func() RETURNS INT BEGIN UNLOCK TABLES; -- 此处触发错误 RETURN 1; END; ``` 2. **事务混淆**: - 行级(`SELECT ... FOR UPDATE`)属于事务,在存储过程/函数中允许使用 - (`LOCK TABLES/UNLOCK TABLES`)在存储函数中被禁止 --- #### 二、解决方案 ##### 方案1:将函数改为存储过程(推荐) ```sql -- 1. 删除原函数 DROP FUNCTION IF EXISTS your_function; -- 2. 改为存储过程 CREATE PROCEDURE your_procedure() BEGIN -- 允许使用(需成对出现) LOCK TABLES table_name READ; -- 业务逻辑... UNLOCK TABLES; -- 此处不再报错 END; ``` ##### 方案2:改用事务替代 ```sql CREATE FUNCTION your_function() RETURNS INT BEGIN DECLARE result INT; START TRANSACTION; -- 使用行级(允许在函数中使用) SELECT counter INTO result FROM counters WHERE id = 1 FOR UPDATE; UPDATE counters SET counter = result + 1 WHERE id = 1; COMMIT; RETURN result; END; ``` ##### 方案3:避免显式操作 使用乐观或原子操作: ```sql CREATE FUNCTION safe_update() RETURNS INT BEGIN -- 原子操作示例(无) UPDATE orders SET total = total + 100 WHERE order_id = 123; RETURN ROW_COUNT(); END; ``` --- #### 三、关键设计规范 1. **使用原则**: - 存储函数中只允许**隐式**(如事务中的 `FOR UPDATE`) - 必须在存储过程中**成对出现**: ```sql CREATE PROCEDURE valid_lock_demo() BEGIN LOCK TABLES t1 WRITE, t2 READ; -- 先定 -- 业务逻辑... UNLOCK TABLES; -- 后解锁 END; ``` 2. **事务边界控制**: ```mermaid graph LR A[开始事务] --> B[加行级] B --> C[业务操作] C --> D{成功?} D -->|是| E[提交] D -->|否| F[回滚] E & F --> G[释放] ``` --- #### 四、错误排查步骤 1. 检查对象类型: ```sql SELECT ROUTINE_TYPE FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_NAME = 'your_function'; -- 返回 FUNCTION 即说明问题 ``` 2. 搜索操作语句: ```sql SHOW CREATE FUNCTION your_function; -- 查找 LOCK/UNLOCK 关键字 ``` --- #### 五、最佳实践建议 1. 优先使用存储过程处理含逻辑 2. 在函数中用原子操作替代 3. 长事务拆分为短操作,减少持有时间 4. 使用监控工具检查争用: ```sql -- 查看当前状态 SELECT * FROM performance_schema.data_locks; ``` > **重要提示**:生产环境修改前务必在测试环境验证,避免升级导致死[^2]。 --- ### 相关问题 1. 如何在MySQL存储过程中正确实现行级? 2. 存储函数和存储过程的主要性能差异是什么? 3. 哪些情况下MySQL会触发隐式定? 4. 如何监控MySQL中的等待情况? 5. 在读写分离架构中,存储函数的行为会有什么变化? [^1]: MySQL 官方文档明确规定存储函数中禁止显式操作 [^2]: 不规范的操作可能导致死和性能下降
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值