MySQL锁机制和PHP锁机制


PHP中的文件锁 (锁的是文件,不是表)

文件锁的文件与表有什么关系?:一点关系也没有,与令牌相似,谁拿到谁操作。所以表根本没锁。
测试时,有个文件就行,叫什么名无所谓

bool flock ( int handle, int operation [, int &wouldblock] );
flock() 操作的 handle 必须是一个已经打开的文件指针。operation 可以是以下值之一:

  • 要取得共享锁定(读取程序),将 operation 设为 LOCK_SH(PHP 4.0.1 以前的版本设置为 1)
  • 要取得独占锁定(写入程序),将 operation 设为 LOCK_EX(PHP 4.0.1 以前的版本中设置为 2)
  • 要释放锁定(无论共享或独占),将 operation 设为 LOCK_UN(PHP 4.0.1 以前的版本中设置为 3)
  • 如果你不希望 flock() 在锁定时堵塞,则给 operation 加上 LOCK_NB(PHP 4.0.1 以前的版本中设置为4)

建两个文件

(1) a.php
$file = "temp.txt";   
$fp = fopen($file , 'w');   
if(flock($fp , LOCK_EX)){   
     fwrite($fp , "abc\n");   
     sleep(10);   
     fwrite($fp , "123\n");   
    flock($fp , LOCK_UN);   
}   
fclose($fp);  
(2) b.php
$file = "temp.txt";   
$fp = fopen($file , 'r');   
echo fread($fp , 100);   
fclose($fp);  

运行 a.php 后,马上运行 b.php ,可以看到输出:

abc

等 a.php 运行完后运行 b.php ,可以看到输出:

abc
123

显然,当 a.php 写文件时数据太大,导致时间比较长时,这时 b.php 读取数据不完整

修改 b.php 为:

$file = "temp.txt";   
$fp = fopen($file , 'r');   
if(flock($fp , LOCK_EX)){   
    echo fread($fp , 100);   
    flock($fp , LOCK_UN);   
} else{   
    echo "Lock file failed...\n";   
}   
fclose($fp);  

运行 a.php 后,马上运行 b.php ,可以发现 b.php 会等到 a.php 运行完成后(即 10 秒后)才显示:

abc
123

读取数据完整,但时间过长,他要等待写锁释放。

修改 b.php 为:

$file = "temp.txt";   
$fp = fopen($file , 'r');   
if(flock($fp , LOCK_SH | LOCK_NB)){   
    echo fread($fp , 100);   
    flock($fp , LOCK_UN);   
} else{   
    echo "Lock file failed...\n";   
}   
fclose($fp);  

运行 a.php 后,马上运行 b.php ,可以看到输出:
Lock file failed…

证明可以返回锁文件失败状态,而不是向上面一样要等很久。

结论:

建议作文件缓存时,选好相关的锁,不然可能导致读取数据不完整,或重复写入数据。
file_get_contents 好像选择不了锁,不知道他默认用的什么锁,反正和不锁得到的输出一样,是不完整的数据。

我是要做文件缓存,所以只需要知道是否有写锁存在即可,有的话就查数据库就可以了。
测试环境:Linux(Ubuntu 6) , PHP 5.1.2 , Apache 2

再转:

文件锁有两种:共享锁和排他锁,也就是读锁(LOCK_SH)和写锁(LOCK_EX)
文件的锁一般这么使用:

$fp = fopen("filename", "a");   
flock($fp, LOCK_SH) or die("lock error")   
$str = fread($fp, 1024);   
flock($fp, LOCK_UN);   
fclose($fp);  

注意fwrite之后,文件立即就被更新了,而不是等fwrite然后fclose之后文件才会更新,这个可以通过在fwrite之后fclose之前读取这个文件进行检查

但是什么时候使用lock_ex什么时候使用lock_sh呢?

读的时候:

如果不想出现dirty数据,那么最好使用lock_sh共享锁。可以考虑以下三种情况:

  1. 如果读的时候没有加共享锁,那么其他程序要写的话(不管这个写是加锁还是不加锁)都会立即写成功。如果正好读了一半,然后被其他程序给写了,那么读的后一半就有可能跟前一半对不上(前一半是修改前的,后一半是修改后的)
  2. 如果读的时候加上了共享锁(因为只是读,没有必要使用排他锁),这个时候,其他程序开始写,这个写程序没有使用锁,那么写程序会直接修改这个文件,也会导致前面一样的问题
  3. 最理想的情况是,读的时候加锁(lock_sh),写的时候也进行加锁(lock_ex),这样写程序会等着读程序完成之后才进行操作,而不会出现贸然操作的情况
写的时候:

如果多个写程序不加锁同时对文件进行操作,那么最后的数据有可能一部分是a程序写的,一部分是b程序写的
如果写的时候加锁了,这个时候有其他的程序来读,那么他会读到什么东西呢?

  1. 如果读程序没有申请共享锁,那么他会读到dirty的数据。比如写程序要写a,b,c三部分,写完a,这时候读读到的是a,继续写b,这时候读读到的是ab,然后写c,这时候读到的是abc.
  2. 如果读程序在之前申请了共享锁,那么读程序会等写程序将abc写完并释放锁之后才进行读。
总结:

项目中应该只使用PHP中的文件锁,尽量避免锁表,因为如果表被锁定了,那么整个网站中所有和这个表相关的功能都被拖慢了(例如:前台很多用户一直下订单,商品表mysql锁表,其他与商品表相关的操作一直处于阻塞状态【读不出来商品表】,因为一个功能把整个网站速度拖慢)。

比如在一个O2O外卖项目中,中午12-2点,晚上6点都是订单高并发时,这种情况下,MySQL锁显然是不考虑的,用户体验太差。其实根据实际的需求,外卖可以不用设计库存量的,当然除了秒杀活动模块还是需要php文件锁的。

应用场景:

  1. 高并发下单时,减库存量时要加锁
  2. 高并发抢单、抢票时要使用

MySQL锁示例代码:

<?php 
/**
模拟秒杀活动-- 商品100件
CREATE TABLE a
(
    id int comment '模拟100件活动商品的数量'
);
INSERT INTO a VALUES(100);
模仿:以10的并发量访问这个脚本!    使用apache自带的ab.exe软件
 */ 
error_reporting(0); 
mysql_connect('localhost','root','admin123'); 
mysql_select_db('test'); 

# mysql 锁 
mysql_query('LOCK TABLE a WRITE');// 只有一个客户端可以锁定表,其他客户端阻塞在这 
$rs = mysql_query('SELECT id FROM a'); 
$id = mysql_result($rs, 0, 0); 
if($id > 0) 
{ 
    --$id; 
    mysql_query('UPDATE a SET id='.$id); 
} 

# mysql 解锁 
mysql_query('UNLOCK TABLES');

PHP文件锁示例代码:

<?php 
/**
模拟秒杀活动-- 商品100件
CREATE TABLE a
(
    id int comment '模拟100件活动商品的数量'
);
INSERT INTO a VALUES(100);
模仿:以10的并发量访问这个脚本!    使用apache自带的ab.exe软件
 */ 
error_reporting(0); 
mysql_connect('localhost','root','admin123'); 
mysql_select_db('test'); 
# php中的文件锁 
$fp = fopen('./a.lock', 'r'); // php的文件锁和表没关系,随便一个文件即可 
flock($fp, LOCK_EX);// 排他锁 

$rs = mysql_query('SELECT id FROM a'); 
$id = mysql_result($rs, 0, 0); 
if($id > 0) 
{ 
    --$id; 
    mysql_query('UPDATE a SET id='.$id); 
} 
# php的文件锁,释放锁 
flock($fp, LOCK_UN); 
fclose($fp);

MYSQL中的锁:

语法 :
LOCK TABLE 表名1 READ|WRITE, 表名2 READ|WRITE ……………… 【锁表】
UNLOCK TABLES 【释放表】

  • Read:读锁|共享锁 : 所有的客户端只能读这个表不能写这个表
  • Write:写锁|排它锁: 所有当前锁定客户端可以操作这个表,其他客户端只能阻塞

注意:在锁表的过程中只能操作被锁定的表,如果要操作其他表,必须把所有要操作的表都锁定起来!

1.表级锁定(table-level)

表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大打折扣。使用表级锁定的主要是MyISAM等一些非事务性存储引擎。

2.行级锁定(row-level)

行级锁定最大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。
虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。使用行级锁定的主要是InnoDB存储引擎。

3.页级锁定(page-level)

页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。
在数据库实现资源锁定的过程中,随着锁定资源颗粒度的减小,锁定相同数据量的数据所需要消耗的内存数量是越来越多的,实现算法也会越来越复杂。不过,随着锁定资源颗粒度的减小,应用程序的访问请求遇到锁等待的可能性也会随之降低,系统整体并发度也随之提升。

总结:
  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;
  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

适用:从锁的角度来说,表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。

二、表级锁定

在mysql中,MyISAM引擎使用的锁定机制完全是mysql的表级锁定,下面将以MYISAM引擎作为示例

1.MySQL表级锁的模式

MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。

  • 兼容性:
    • 对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;
    • 对MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;
    • MyISAM表的读操作与写操作之间,以及写操作之间是串行的。当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。其他线程的读、写操作都会等待,直到锁被释放为止。
2.加锁

MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。

3.MyISAM锁的优化

对于MyISAM存储引擎,虽然使用表级锁定在锁定实现的过程中比实现行级锁定或者页级锁所带来的附加成本都要小,锁定本身所消耗的资源也是最少。但是由于锁定的颗粒度比较到,所以造成锁定资源的争用情况也会比其他的锁定级别都要多,从而在较大程度上会降低并发处理能力。所以,在优化MyISAM存储引擎锁定问题的时候,最关键的就是如何让其提高并发度。由于锁定级别是不可能改变的了,所以我们首先需要尽可能让锁定的时间变短,然后就是让可能并发进行的操作尽可能的并发。

  • (1)查询表锁争用情况
1
2
3
4
5
6
7
mysql> show status like 'table%';
+----------------------------+---------+
| Variable_name | Value |
+----------------------------+---------+
| Table_locks_immediate | 100 |
| Table_locks_waited | 11 |
+----------------------------+---------+

这里有两个状态变量记录MySQL内部表级锁定的情况,两个变量说明如下:

  • Table_locks_immediate:产生表级锁定的次数;
  • Table_locks_waited:出现表级锁定争用而发生等待的次数;

两个状态值都是从系统启动后开始记录,出现一次对应的事件则数量加1。如果这里的Table_locks_waited状态值比较高,那么说明系统中表级锁定争用现象比较严重,就需要进一步分析为什么会有较多的锁定资源争用了

  • (2)缩短锁定时间

如何让锁定时间尽可能的短呢?唯一的办法就是让我们的Query执行时间尽可能的短。

+ a)尽两减少大的复杂Query,将复杂Query分拆成几个小的Query分布进行;  
+ b)尽可能的建立足够高效的索引,让数据检索更迅速;  
+ c)尽量让MyISAM存储引擎的表只存放必要的信息,控制字段类型;  
+ d)利用合适的机会优化MyISAM表数据文件
  • (3)分离并行的操作
坚持原创技术分享,您的支持将鼓励我继续创作!