MySQL锁机制详解

mysql 锁机制详解

锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。本章我们着重讨论MySQL锁机制的特点,常见的锁问题,以及解决MySQL锁问题的一些方法或建议。
MySQL锁概述
相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。比如,MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁;InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。
MySQL这3种锁的特性可大致归纳如下。
开销、加锁速度、死锁、粒度、并发性能
l         表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
l         行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
l         页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
从上述特点可见,很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适!仅从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。这一点在本书的“开发篇”介绍表类型的选择时,也曾提到过。下面几节我们重点介绍MySQL表锁和 InnoDB行锁的问题,由于BDB已经被InnoDB取代,即将成为历史,在此就不做进一步的讨论了。
MyISAM表锁
MyISAM存储引擎只支持表锁,这也是MySQL开始几个版本中唯一支持的锁类型。随着应用对事务完整性和并发性要求的不断提高,MySQL才开始开发基于事务的存储引擎,后来慢慢出现了支持页锁的BDB存储引擎和支持行锁的InnoDB存储引擎(实际 InnoDB是单独的一个公司,现在已经被Oracle公司收购)。但是MyISAM的表锁依然是使用最为广泛的锁类型。本节将详细介绍MyISAM表锁的使用。

查询表级锁争用情况

可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定争夺:
mysql> show status like ‘table%’;
+———————–+——-+
| Variable_name         | Value |
+———————–+——-+
| Table_locks_immediate | 2979  |
| Table_locks_waited    | 0     |
+———————–+——-+
2 rows in set (0.00 sec))
如果Table_locks_waited的值比较高,则说明存在着较严重的表级锁争用情况。

MySQL表级锁的锁模式

MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。锁模式的兼容性如表20-1所示。
表20-1                                            MySQL中的表锁兼容性
请求锁模式
         是否兼容
当前锁模式
None
读锁
写锁
读锁
写锁
可见,对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM表的读操作与写操作之间,以及写操作之间是串行的!根据如表20-2所示的例子可以知道,当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。其他线程的读、写操作都会等待,直到锁被释放为止。
表20-2                          MyISAM存储引擎的写阻塞读例子
session_1
session_2
获得表film_text的WRITE锁定
mysql> lock table film_text write;
Query OK, 0 rows affected (0.00 sec)
当前session对锁定表的查询、更新、插入操作都可以执行:
mysql> select film_id,title from film_text where film_id = 1001;
+———+————-+
| film_id | title       |
+———+————-+
| 1001    | Update Test |
+———+————-+
1 row in set (0.00 sec)
mysql> insert into film_text (film_id,title) values(1003,’Test’);
Query OK, 1 row affected (0.00 sec)
mysql> update film_text set title = ‘Test’ where film_id = 1001;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
其他session对锁定表的查询被阻塞,需要等待锁被释放:
mysql> select film_id,title from film_text where film_id = 1001;
等待
释放锁:
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
等待
Session2获得锁,查询返回:
mysql> select film_id,title from film_text where film_id = 1001;
+———+——-+
| film_id | title |
+———+——-+
| 1001    | Test  |
+———+——-+
1 row in set (57.59 sec)

如何加表锁

MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。在本书的示例中,显式加锁基本上都是为了方便而已,并非必须如此。
给MyISAM表显示加锁,一般是为了在一定程度模拟事务操作,实现对某一时间点多个表的一致性读取。例如,有一个订单表orders,其中记录有各订单的总金额total,同时还有一个订单明细表order_detail,其中记录有各订单每一产品的金额小计 subtotal,假设我们需要检查这两个表的金额合计是否相符,可能就需要执行如下两条SQL:
Select sum(total) from orders;
Select sum(subtotal) from order_detail;
这时,如果不先给两个表加锁,就可能产生错误的结果,因为第一条语句执行过程中,order_detail表可能已经发生了改变。因此,正确的方法应该是:
Lock tables orders read local, order_detail read local;
Select sum(total) from orders;
Select sum(subtotal) from order_detail;
Unlock tables;
要特别说明以下两点内容。
¡  上面的例子在LOCK TABLES时加了“local”选项,其作用就是在满足MyISAM表并发插入条件的情况下,允许其他用户在表尾并发插入记录,有关MyISAM表的并发插入问题,在后面的章节中还会进一步介绍。
¡  在用LOCK TABLES给表显式加表锁时,必须同时取得所有涉及到表的锁,并且MySQL不支持锁升级。也就是说,在执行LOCK TABLES后,只能访问显式加锁的这些表,不能访问未加锁的表;同时,如果加的是读锁,那么只能执行查询操作,而不能执行更新操作。其实,在自动加锁的情况下也基本如此,MyISAM总是一次获得SQL语句所需要的全部锁。这也正是MyISAM表不会出现死锁(Deadlock Free)的原因。
在如表20-3所示的例子中,一个session使用LOCK TABLE命令给表film_text加了读锁,这个session可以查询锁定表中的记录,但更新或访问其他表都会提示错误;同时,另外一个session可以查询表中的记录,但更新就会出现锁等待。
表20-3                     MyISAM存储引擎的读阻塞写例子
session_1
session_2
获得表film_text的READ锁定
mysql> lock table film_text read;
Query OK, 0 rows affected (0.00 sec)
当前session可以查询该表记录
mysql> select film_id,title from film_text where film_id = 1001;
+———+——————+
| film_id | title            |
+———+——————+
| 1001    | ACADEMY DINOSAUR |
+———+——————+
1 row in set (0.00 sec)
其他session也可以查询该表的记录
mysql> select film_id,title from film_text where film_id = 1001;
+———+——————+
| film_id | title            |
+———+——————+
| 1001    | ACADEMY DINOSAUR |
+———+——————+
1 row in set (0.00 sec)
当前session不能查询没有锁定的表
mysql> select film_id,title from film where film_id = 1001;
ERROR 1100 (HY000): Table ‘film’ was not locked with LOCK TABLES
其他session可以查询或者更新未锁定的表
mysql> select film_id,title from film where film_id = 1001;
+———+—————+
| film_id | title         |
+———+—————+
| 1001    | update record |
+———+—————+
1 row in set (0.00 sec)
mysql> update film set title = ‘Test’ where film_id = 1001;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1  Changed: 1  Warnings: 0
当前session中插入或者更新锁定的表都会提示错误:
mysql> insert into film_text (film_id,title) values(1002,’Test’);
ERROR 1099 (HY000): Table ‘film_text’ was locked with a READ lock and can’t be updated
mysql> update film_text set title = ‘Test’ where film_id = 1001;
ERROR 1099 (HY000): Table ‘film_text’ was locked with a READ lock and can’t be updated
其他session更新锁定表会等待获得锁:
mysql> update film_text set title = ‘Test’ where film_id = 1001;
等待
释放锁
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
等待
Session获得锁,更新操作完成:
mysql> update film_text set title = ‘Test’ where film_id = 1001;
Query OK, 1 row affected (1 min 0.71 sec)
Rows matched: 1  Changed: 1  Warnings: 0
当使用LOCK TABLES时,不仅需要一次锁定用到的所有表,而且,同一个表在SQL语句中出现多少次,就要通过与SQL语句中相同的别名锁定多少次,否则也会出错!举例说明如下。
(1)对actor表获得读锁:
mysql> lock table actor read;
Query OK, 0 rows affected (0.00 sec)
(2)但是通过别名访问会提示错误:
mysql> select a.first_name,a.last_name,b.first_name,b.last_name from actor a,actor b where a.first_name = b.first_name and a.first_name = ‘Lisa’ and a.last_name = ‘Tom’ and a.last_name <> b.last_name;
ERROR 1100 (HY000): Table ‘a’ was not locked with LOCK TABLES
(3)需要对别名分别锁定:
mysql> lock table actor as a read,actor as b read;
Query OK, 0 rows affected (0.00 sec)
(4)按照别名的查询可以正确执行:
mysql> select a.first_name,a.last_name,b.first_name,b.last_name from actor a,actor b where a.first_name = b.first_name and a.first_name = ‘Lisa’ and a.last_name = ‘Tom’ and a.last_name <> b.last_name;
+————+———–+————+———–+
| first_name | last_name | first_name | last_name |
+————+———–+————+———–+
| Lisa       | Tom       | LISA       | MONROE    |
+————+———–+————+———–+
1 row in set (0.00 sec)

并发插入(Concurrent Inserts)

上文提到过MyISAM表的读和写是串行的,但这是就总体而言的。在一定条件下,MyISAM表也支持查询和插入操作的并发进行。
MyISAM存储引擎有一个系统变量concurrent_insert,专门用以控制其并发插入的行为,其值分别可以为0、1或2。
l         当concurrent_insert设置为0时,不允许并发插入。
l         当concurrent_insert设置为1时,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。这也是MySQL的默认设置。
l         当concurrent_insert设置为2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。
在如表20-4所示的例子中,session_1获得了一个表的READ LOCAL锁,该线程可以对表进行查询操作,但不能对表进行更新操作;其他的线程(session_2),虽然不能对表进行删除和更新操作,但却可以对该表进行并发插入操作,这里假设该表中间不存在空洞。
表20-4              MyISAM存储引擎的读写(INSERT)并发例子
session_1
session_2
获得表film_text的READ LOCAL锁定
mysql> lock table film_text read local;
Query OK, 0 rows affected (0.00 sec)
当前session不能对锁定表进行更新或者插入操作:
mysql> insert into film_text (film_id,title) values(1002,’Test’);
ERROR 1099 (HY000): Table ‘film_text’ was locked with a READ lock and can’t be updated
mysql> update film_text set title = ‘Test’ where film_id = 1001;
ERROR 1099 (HY000): Table ‘film_text’ was locked with a READ lock and can’t be updated
其他session可以进行插入操作,但是更新会等待:
mysql> insert into film_text (film_id,title) values(1002,’Test’);
Query OK, 1 row affected (0.00 sec)
mysql> update film_text set title = ‘Update Test’ where film_id = 1001;
等待
当前session不能访问其他session插入的记录:
mysql> select film_id,title from film_text where film_id = 1002;
Empty set (0.00 sec)
释放锁:
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
等待
当前session解锁后可以获得其他session插入的记录:
mysql> select film_id,title from film_text where film_id = 1002;
+———+——-+
| film_id | title |
+———+——-+
| 1002    | Test  |
+———+——-+
1 row in set (0.00 sec)
Session2获得锁,更新操作完成:
mysql> update film_text set title = ‘Update Test’ where film_id = 1001;
Query OK, 1 row affected (1 min 17.75 sec)
Rows matched: 1  Changed: 1  Warnings: 0
可以利用MyISAM存储引擎的并发插入特性,来解决应用中对同一表查询和插入的锁争用。例如,将concurrent_insert系统变量设为2,总是允许并发插入;同时,通过定期在系统空闲时段执行 OPTIMIZE TABLE语句来整理空间碎片,收回因删除记录而产生的中间空洞。有关OPTIMIZE TABLE语句的详细介绍,可以参见第18章中“两个简单实用的优化方法”一节的内容。

MyISAM的锁调度

前面讲过,MyISAM存储引擎的读锁和写锁是互斥的,读写操作是串行的。那么,一个进程请求某个 MyISAM表的读锁,同时另一个进程也请求同一表的写锁,MySQL如何处理呢?答案是写进程先获得锁。不仅如此,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前!这是因为MySQL认为写请求一般比读请求要重要。这也正是MyISAM表不太适合于有大量更新操作和查询操作应用的原因,因为,大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。这种情况有时可能会变得非常糟糕!幸好我们可以通过一些设置来调节MyISAM 的调度行为。
¡  通过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。
¡  通过执行命令SET LOW_PRIORITY_UPDATES=1,使该连接发出的更新请求优先级降低。
¡  通过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,降低该语句的优先级。
虽然上面3种方法都是要么更新优先,要么查询优先的方法,但还是可以用其来解决查询相对重要的应用(如用户登录系统)中,读锁等待严重的问题。
另外,MySQL也提供了一种折中的办法来调节读写冲突,即给系统参数max_write_lock_count设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级降低,给读进程一定获得锁的机会。
上面已经讨论了写优先调度机制带来的问题和解决办法。这里还要强调一点:一些需要长时间运行的查询操作,也会使写进程“饿死”!因此,应用中应尽量避免出现长时间运行的查询操作,不要总想用一条SELECT语句来解决问题,因为这种看似巧妙的SQL语句,往往比较复杂,执行时间较长,在可能的情况下可以通过使用中间表等措施对SQL语句做一定的“分解”,使每一步查询都能在较短时间完成,从而减少锁冲突。如果复杂查询不可避免,应尽量安排在数据库空闲时段执行,比如一些定期统计可以安排在夜间执行。
InnoDB锁问题
InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。行级锁与表级锁本来就有许多不同之处,另外,事务的引入也带来了一些新问题。下面我们先介绍一点背景知识,然后详细讨论InnoDB的锁问题。

背景知识

1.事务(Transaction)及其ACID属性

事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。
l         原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
l         一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
l         隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
l         持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
银行转帐就是事务的一个典型例子。

2.并发事务处理带来的问题

相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况。
l  更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题--最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题。
l  脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做”脏读”。
l  不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
l  幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。

3.事务隔离级别

在上面讲到的并发事务处理带来的问题中,“更新丢失”通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。
“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。数据库实现事务隔离的方式,基本上可分为以下两种。
l  一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改。
l  另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度来看,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上 “串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问的能力。
为了解决“隔离”与“并发”的矛盾,ISO/ANSI SQL92定义了4个事务隔离级别,每个级别的隔离程度不同,允许出现的副作用也不同,应用可以根据自己的业务逻辑要求,通过选择不同的隔离级别来平衡 “隔离”与“并发”的矛盾。表20-5很好地概括了这4个隔离级别的特性。
表20-5                                             4种隔离级别比较
读数据一致性及允许的并发副作用
隔离级别
读数据一致性
脏读
不可重复读
幻读
未提交读(Read uncommitted)
最低级别,只能保证不读取物理上损坏的数据
已提交度(Read committed)
语句级
可重复读(Repeatable read)
事务级
可序列化(Serializable)
最高级别,事务级
最后要说明的是:各具体数据库并不一定完全实现了上述4个隔离级别,例如,Oracle只提供Read committed和Serializable两个标准隔离级别,另外还提供自己定义的Read only隔离级别;SQL Server除支持上述ISO/ANSI SQL92定义的4个隔离级别外,还支持一个叫做“快照”的隔离级别,但严格来说它是一个用MVCC实现的Serializable隔离级别。MySQL 支持全部4个隔离级别,但在具体实现时,有一些特点,比如在一些隔离级别下是采用MVCC一致性读,但某些情况下又不是,这些内容在后面的章节中将会做进一步介绍。

获取InnoDB行锁争用情况

可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况:
mysql> show status like ‘innodb_row_lock%’;
+——————————-+——-+
| Variable_name                 | Value |
+——————————-+——-+
| InnoDB_row_lock_current_waits | 0     |
| InnoDB_row_lock_time          | 0     |
| InnoDB_row_lock_time_avg      | 0     |
| InnoDB_row_lock_time_max      | 0     |
| InnoDB_row_lock_waits         | 0     |
+——————————-+——-+
5 rows in set (0.01 sec)
如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,还可以通过设置InnoDB Monitors来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因。
具体方法如下:
mysql> CREATE TABLE innodb_monitor(a INT) ENGINE=INNODB;
Query OK, 0 rows affected (0.14 sec)
然后就可以用下面的语句来进行查看:
mysql> Show innodb status\G;
*************************** 1. row ***************************
  Type: InnoDB
  Name:
Status:
————
TRANSACTIONS
————
Trx id counter 0 117472192
Purge done for trx’s n:o < 0 117472190 undo n:o < 0 0
History list length 17
Total number of lock structs in row lock hash table 0
LIST OF TRANSACTIONS FOR EACH SESSION:
—TRANSACTION 0 117472185, not started, process no 11052, OS thread id 1158191456
MySQL thread id 200610, query id 291197 localhost root
—TRANSACTION 0 117472183, not started, process no 11052, OS thread id 1158723936
MySQL thread id 199285, query id 291199 localhost root
Show innodb status
监视器可以通过发出下列语句来停止查看:
mysql> DROP TABLE innodb_monitor;
Query OK, 0 rows affected (0.05 sec)
设置监视器后,在SHOW INNODB STATUS的显示内容中,会有详细的当前锁等待的信息,包括表名、锁类型、锁定记录的情况等,便于进行进一步的分析和问题的确定。打开监视器以后,默认情况下每15秒会向日志中记录监控的内容,如果长时间打开会导致.err文件变得非常的巨大,所以用户在确认问题原因之后,要记得删除监控表以关闭监视器,或者通过使用“–console”选项来启动服务器以关闭写日志文件。

InnoDB的行锁模式及加锁方法

InnoDB实现了以下两种类型的行锁。
l  共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
l  排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
l  意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
l  意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
上述锁模式的兼容情况具体如表20-6所示。
表20-6                                            InnoDB行锁模式兼容性列表
请求锁模式
   是否兼容
当前锁模式
X
IX
S
IS
X
冲突
冲突
冲突
冲突
IX
冲突
兼容
冲突
兼容
S
冲突
冲突
兼容
兼容
IS
冲突
兼容
兼容
兼容
如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。
意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。
¡  共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE。
¡  排他锁(X):SELECT * FROM table_name WHERE … FOR UPDATE。
用SELECT … IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用SELECT… FOR UPDATE方式获得排他锁。
在如表20-7所示的例子中,使用了SELECT … IN SHARE MODE加锁后再更新记录,看看会出现什么情况,其中actor表的actor_id字段为主键。
表20-7  InnoDB存储引擎的共享锁例子
session_1
session_2
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select actor_id,first_name,last_name from actor where actor_id = 178;
+———-+————+———–+
| actor_id | first_name | last_name |
+———-+————+———–+
| 178      | LISA       | MONROE    |
+———-+————+———–+
1 row in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select actor_id,first_name,last_name from actor where actor_id = 178;
+———-+————+———–+
| actor_id | first_name | last_name |
+———-+————+———–+
| 178      | LISA       | MONROE    |
+———-+————+———–+
1 row in set (0.00 sec)
当前session对actor_id=178的记录加share mode 的共享锁:
mysql> select actor_id,first_name,last_name from actor where actor_id = 178lock in share mode;
+———-+————+———–+
| actor_id | first_name | last_name |
+———-+————+———–+
| 178      | LISA       | MONROE    |
+———-+————+———–+
1 row in set (0.01 sec)
其他session仍然可以查询记录,并也可以对该记录加share mode的共享锁:
mysql> select actor_id,first_name,last_name from actor where actor_id = 178lock in share mode;
+———-+————+———–+
| actor_id | first_name | last_name |
+———-+————+———–+
| 178      | LISA       | MONROE    |
+———-+————+———–+
1 row in set (0.01 sec)
当前session对锁定的记录进行更新操作,等待锁:
mysql> update actor set last_name = ‘MONROE T’ where actor_id = 178;
等待
其他session也对该记录进行更新操作,则会导致死锁退出:
mysql> update actor set last_name = ‘MONROE T’ where actor_id = 178;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
获得锁后,可以成功更新:
mysql> update actor set last_name = ‘MONROE T’ where actor_id = 178;
Query OK, 1 row affected (17.67 sec)
Rows matched: 1  Changed: 1  Warnings: 0
    当使用SELECT…FOR UPDATE加锁后再更新记录,出现如表20-8所示的情况。
表20-8 InnoDB存储引擎的排他锁例子
session_1
session_2
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select actor_id,first_name,last_name from actor where actor_id = 178;
+———-+————+———–+
| actor_id | first_name | last_name |
+———-+————+———–+
| 178      | LISA       | MONROE    |
+———-+————+———–+
1 row in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select actor_id,first_name,last_name from actor where actor_id = 178;
+———-+————+———–+
| actor_id | first_name | last_name |
+———-+————+———–+
| 178      | LISA       | MONROE    |
+———-+————+———–+
1 row in set (0.00 sec)
当前session对actor_id=178的记录加for update的排它锁:
mysql> select actor_id,first_name,last_name from actor where actor_id = 178 for update;
+———-+————+———–+
| actor_id | first_name | last_name |
+———-+————+———–+
| 178      | LISA       | MONROE    |
+———-+————+———–+
1 row in set (0.00 sec)
其他session可以查询该记录,但是不能对该记录加共享锁,会等待获得锁:
mysql> select actor_id,first_name,last_name from actor where actor_id = 178;
+———-+————+———–+
| actor_id | first_name | last_name |
+———-+————+———–+
| 178      | LISA       | MONROE    |
+———-+————+———–+
1 row in set (0.00 sec)
mysql> select actor_id,first_name,last_name from actor where actor_id = 178 for update;
等待
当前session可以对锁定的记录进行更新操作,更新后释放锁:
mysql> update actor set last_name = ‘MONROE T’ where actor_id = 178;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
其他session获得锁,得到其他session提交的记录:
mysql> select actor_id,first_name,last_name from actor where actor_id = 178 for update;
+———-+————+———–+
| actor_id | first_name | last_name |
+———-+————+———–+
| 178      | LISA       | MONROE T  |
+———-+————+———–+
1 row in set (9.59 sec)

InnoDB行锁实现方式

InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。下面通过一些实际例子来加以说明。
(1)在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁。
在如表20-9所示的例子中,开始tab_no_index表没有索引:
mysql> create table tab_no_index(id int,name varchar(10)) engine=innodb;
Query OK, 0 rows affected (0.15 sec)
mysql> insert into tab_no_index values(1,’1′),(2,’2′),(3,’3′),(4,’4′);
Query OK, 4 rows affected (0.00 sec)
Records: 4  Duplicates: 0  Warnings: 0
表20-9   InnoDB存储引擎的表在不使用索引时使用表锁例子
session_1
session_2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_no_index where id = 1 ;
+——+——+
| id   | name |
+——+——+
| 1    | 1    |
+——+——+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_no_index where id = 2 ;
+——+——+
| id   | name |
+——+——+
| 2    | 2    |
+——+——+
1 row in set (0.00 sec)
mysql> select * from tab_no_index where id = 1 for update;
+——+——+
| id   | name |
+——+——+
| 1    | 1    |
+——+——+
1 row in set (0.00 sec)
mysql> select * from tab_no_index where id = 2 for update;
等待
在如表20 -9所示的例子中,看起来session_1只给一行加了排他锁,但session_2在请求其他行的排他锁时,却出现了锁等待!原因就是在没有索引的情况下,InnoDB只能使用表锁。当我们给其增加一个索引后,InnoDB就只锁定了符合条件的行,如表20-10所示。
创建tab_with_index表,id字段有普通索引:
mysql> create table tab_with_index(id int,name varchar(10)) engine=innodb;
Query OK, 0 rows affected (0.15 sec)
mysql> alter table tab_with_index add index id(id);
Query OK, 4 rows affected (0.24 sec)
Records: 4  Duplicates: 0  Warnings: 0
表20-10   InnoDB存储引擎的表在使用索引时使用行锁例子
session_1
session_2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_with_index where id = 1 ;
+——+——+
| id   | name |
+——+——+
| 1    | 1    |
+——+——+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_with_index where id = 2 ;
+——+——+
| id   | name |
+——+——+
| 2    | 2    |
+——+——+
1 row in set (0.00 sec)
mysql> select * from tab_with_index where id = 1 for update;
+——+——+
| id   | name |
+——+——+
| 1    | 1    |
+——+——+
1 row in set (0.00 sec)
mysql> select * from tab_with_index where id = 2 for update;
+——+——+
| id   | name |
+——+——+
| 2    | 2    |
+——+——+
1 row in set (0.00 sec)
(2)由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。应用设计的时候要注意这一点。
在如表20-11所示的例子中,表tab_with_index的id字段有索引,name字段没有索引:
mysql> alter table tab_with_index drop index name;
Query OK, 4 rows affected (0.22 sec)
Records: 4  Duplicates: 0  Warnings: 0
mysql> insert into tab_with_index  values(1,’4′);
Query OK, 1 row affected (0.00 sec)
mysql> select * from tab_with_index where id = 1;
+——+——+
| id   | name |
+——+——+
| 1    | 1    |
| 1    | 4    |
+——+——+
2 rows in set (0.00 sec)
表20-11 InnoDB存储引擎使用相同索引键的阻塞例子
session_1
session_2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_with_index where id = 1 and name = ‘1’ for update;
+——+——+
| id   | name |
+——+——+
| 1    | 1    |
+——+——+
1 row in set (0.00 sec)
虽然session_2访问的是和session_1不同的记录,但是因为使用了相同的索引,所以需要等待锁:
mysql> select * from tab_with_index where id = 1 and name = ‘4’ for update;
等待
(3)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。
在如表20-12所示的例子中,表tab_with_index的id字段有主键索引,name字段有普通索引:
mysql> alter table tab_with_index add index name(name);
Query OK, 5 rows affected (0.23 sec)
Records: 5  Duplicates: 0  Warnings: 0
表20-12  InnoDB存储引擎的表使用不同索引的阻塞例子
session_1
session_2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from tab_with_index where id = 1 for update;
+——+——+
| id   | name |
+——+——+
| 1    | 1    |
| 1    | 4    |
+——+——+
2 rows in set (0.00 sec)
Session_2使用name的索引访问记录,因为记录没有被索引,所以可以获得锁:
mysql> select * from tab_with_index where name = ‘2’ for update;
+——+——+
| id   | name |
+——+——+
| 2    | 2    |
+——+——+
1 row in set (0.00 sec)
由于访问的记录已经被session_1锁定,所以等待获得锁。:
mysql> select * from tab_with_index where name = ‘4’ for update;
(4)即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。关于MySQL在什么情况下不使用索引的详细讨论,参见本章“索引问题”一节的介绍。
在下面的例子中,检索值的数据类型与索引字段不同,虽然MySQL能够进行数据类型转换,但却不会使用索引,从而导致InnoDB使用表锁。通过用explain检查两条SQL的执行计划,我们可以清楚地看到了这一点。
例子中tab_with_index表的name字段有索引,但是name字段是varchar类型的,如果where条件中不是和varchar类型进行比较,则会对name进行类型转换,而执行的全表扫描。
mysql> alter table tab_no_index add index name(name);
Query OK, 4 rows affected (8.06 sec)
Records: 4  Duplicates: 0  Warnings: 0
mysql> explain select * from tab_with_index where name = 1 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: tab_with_index
         type: ALL
possible_keys: name
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4
        Extra: Using where
1 row in set (0.00 sec)
mysql> explain select * from tab_with_index where name = ‘1’ \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: tab_with_index
         type: ref
possible_keys: name
          key: name
      key_len: 23
          ref: const
         rows: 1
        Extra: Using where
1 row in set (0.00 sec)

间隙锁(Next-Key锁)

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
举例来说,假如emp表中只有101条记录,其empid的值分别是 1,2,…,100,101,下面的SQL:
Select * from  emp where empid > 100 for update;
是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
InnoDB使用间隙锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求,对于上面的例子,要是不使用间隙锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;另外一方面,是为了满足其恢复和复制的需要。有关其恢复和复制对锁机制的影响,以及不同隔离级别下InnoDB使用间隙锁的情况,在后续的章节中会做进一步介绍。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。
还要特别说明的是,InnoDB除了通过范围条件加锁时使用间隙锁外,如果使用相等条件请求给一个不存在的记录加锁,InnoDB也会使用间隙锁!
在如表20-13所示的例子中,假如emp表中只有101条记录,其empid的值分别是1,2,……,100,101。
表20-13                InnoDB存储引擎的间隙锁阻塞例子
session_1
session_2
mysql> select @@tx_isolation;
+—————–+
| @@tx_isolation  |
+—————–+
| REPEATABLE-READ |
+—————–+
1 row in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+—————–+
| @@tx_isolation  |
+—————–+
| REPEATABLE-READ |
+—————–+
1 row in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
当前session对不存在的记录加for update的锁:
mysql> select * from emp where empid = 102 for update;
Empty set (0.00 sec)
这时,如果其他session插入empid为102的记录(注意:这条记录并不存在),也会出现锁等待:
mysql>insert into emp(empid,…) values(102,…);
阻塞等待
Session_1 执行rollback:
mysql> rollback;
Query OK, 0 rows affected (13.04 sec)
由于其他session_1回退后释放了Next-Key锁,当前session可以获得锁并成功插入记录:
mysql>insert into emp(empid,…) values(102,…);
Query OK, 1 row affected (13.35 sec)

恢复和复制的需要,对InnoDB锁机制的影响

MySQL通过BINLOG录执行成功的INSERT、UPDATE、DELETE等更新数据的SQL语句,并由此实现MySQL数据库的恢复和主从复制(可以参见本书“管理篇”的介绍)。MySQL的恢复机制(复制其实就是在Slave Mysql不断做基于BINLOG的恢复)有以下特点。
l  一是MySQL的恢复是SQL语句级的,也就是重新执行BINLOG中的SQL语句。这与Oracle数据库不同,Oracle是基于数据库文件块的。
l  二是MySQL的Binlog是按照事务提交的先后顺序记录的,恢复也是按这个顺序进行的。这点也与Oralce不同,Oracle是按照系统更新号(System Change Number,SCN)来恢复数据的,每个事务开始时,Oracle都会分配一个全局唯一的SCN,SCN的顺序与事务开始的时间顺序是一致的。
从上面两点可知,MySQL的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读,这已经超过了ISO/ANSI SQL92“可重复读”隔离级别的要求,实际上是要求事务要串行化。这也是许多情况下,InnoDB要用到间隙锁的原因,比如在用范围条件更新记录时,无论在Read Commited或是Repeatable Read隔离级别下,InnoDB都要使用间隙锁,但这并不是隔离级别要求的,有关InnoDB在不同隔离级别下加锁的差异在下一小节还会介绍。
另外,对于“insert  into target_tab select * from source_tab where …”和“create  table new_tab …select … From  source_tab where …(CTAS)”这种SQL语句,用户并没有对source_tab做任何更新操作,但MySQL对这种SQL语句做了特别处理。先来看如表20-14的例子。
表20-14                   CTAS操作给原表加锁例子
session_1
session_2
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from target_tab;
Empty set (0.00 sec)
mysql> select * from source_tab where name = ‘1’;
+—-+——+—-+
| d1 | name | d2 |
+—-+——+—-+
|  4 | 1    |  1 |
|  5 | 1    |  1 |
|  6 | 1    |  1 |
|  7 | 1    |  1 |
|  8 | 1    |  1 |
+—-+——+—-+
5 rows in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from target_tab;
Empty set (0.00 sec)
mysql> select * from source_tab where name = ‘1’;
+—-+——+—-+
| d1 | name | d2 |
+—-+——+—-+
|  4 | 1    |  1 |
|  5 | 1    |  1 |
|  6 | 1    |  1 |
|  7 | 1    |  1 |
|  8 | 1    |  1 |
+—-+——+—-+
5 rows in set (0.00 sec)
mysql> insert into target_tab select d1,name from source_tab where name = ‘1’;
Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0
mysql> update source_tab set name = ‘1’ where name = ‘8’;
等待
commit;
返回结果
commit;
在上面的例子中,只是简单地读 source_tab表的数据,相当于执行一个普通的SELECT语句,用一致性读就可以了。ORACLE正是这么做的,它通过MVCC技术实现的多版本数据来实现一致性读,不需要给source_tab加任何锁。我们知道InnoDB也实现了多版本数据,对普通的SELECT一致性读,也不需要加任何锁;但这里InnoDB却给source_tab加了共享锁,并没有使用多版本数据一致性读技术!
MySQL为什么要这么做呢?其原因还是为了保证恢复和复制的正确性。因为不加锁的话,如果在上述语句执行过程中,其他事务对source_tab做了更新操作,就可能导致数据恢复的结果错误。为了演示这一点,我们再重复一下前面的例子,不同的是在session_1执行事务前,先将系统变量 innodb_locks_unsafe_for_binlog的值设置为“on”(其默认值为off),具体结果如表20-15所示。
表20-15                   CTAS操作不给原表加锁带来的安全问题例子
session_1
session_2
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql>set innodb_locks_unsafe_for_binlog=’on’
Query OK, 0 rows affected (0.00 sec)
mysql> select * from target_tab;
Empty set (0.00 sec)
mysql> select * from source_tab where name = ‘1’;
+—-+——+—-+
| d1 | name | d2 |
+—-+——+—-+
|  4 | 1    |  1 |
|  5 | 1    |  1 |
|  6 | 1    |  1 |
|  7 | 1    |  1 |
|  8 | 1    |  1 |
+—-+——+—-+
5 rows in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from target_tab;
Empty set (0.00 sec)
mysql> select * from source_tab where name = ‘1’;
+—-+——+—-+
| d1 | name | d2 |
+—-+——+—-+
|  4 | 1    |  1 |
|  5 | 1    |  1 |
|  6 | 1    |  1 |
|  7 | 1    |  1 |
|  8 | 1    |  1 |
+—-+——+—-+
5 rows in set (0.00 sec)
mysql> insert into target_tab select d1,name from source_tab where name = ‘1’;
Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0
session_1未提交,可以对session_1的select的记录进行更新操作。
mysql> update source_tab set name = ‘8’ where name = ‘1’;
Query OK, 5 rows affected (0.00 sec)
Rows matched: 5  Changed: 5  Warnings: 0
mysql> select * from source_tab where name = ‘8’;
+—-+——+—-+
| d1 | name | d2 |
+—-+——+—-+
|  4 | 8    |  1 |
|  5 | 8    |  1 |
|  6 | 8    |  1 |
|  7 | 8    |  1 |
|  8 | 8    |  1 |
+—-+——+—-+
5 rows in set (0.00 sec)
更新操作先提交
mysql> commit;
Query OK, 0 rows affected (0.05 sec)
插入操作后提交
mysql> commit;
Query OK, 0 rows affected (0.07 sec)
此时查看数据,target_tab中可以插入source_tab更新前的结果,这符合应用逻辑:
mysql> select * from source_tab where name = ‘8’;
+—-+——+—-+
| d1 | name | d2 |
+—-+——+—-+
|  4 | 8    |  1 |
|  5 | 8    |  1 |
|  6 | 8    |  1 |
|  7 | 8    |  1 |
|  8 | 8    |  1 |
+—-+——+—-+
5 rows in set (0.00 sec)
mysql> select * from target_tab;
+——+——+
| id   | name |
+——+——+
| 4    | 1.00 |
| 5    | 1.00 |
| 6    | 1.00 |
| 7    | 1.00 |
| 8    | 1.00 |
+——+——+
5 rows in set (0.00 sec)
mysql> select * from tt1 where name = ‘1’;
Empty set (0.00 sec)
mysql> select * from source_tab where name = ‘8’;
+—-+——+—-+
| d1 | name | d2 |
+—-+——+—-+
|  4 | 8    |  1 |
|  5 | 8    |  1 |
|  6 | 8    |  1 |
|  7 | 8    |  1 |
|  8 | 8    |  1 |
+—-+——+—-+
5 rows in set (0.00 sec)
mysql> select * from target_tab;
+——+——+
| id   | name |
+——+——+
| 4    | 1.00 |
| 5    | 1.00 |
| 6    | 1.00 |
| 7    | 1.00 |
| 8    | 1.00 |
+——+——+
5 rows in set (0.00 sec)
从上可见,设置系统变量innodb_locks_unsafe_for_binlog的值为“on”后,InnoDB不再对source_tab加锁,结果也符合应用逻辑,但是如果分析BINLOG的内容:
……
SET TIMESTAMP=1169175130;
BEGIN;
# at 274
#070119 10:51:57 server id 1  end_log_pos 105   Query   thread_id=1     exec_time=0     error_code=0
SET TIMESTAMP=1169175117;
update source_tab set name = ‘8’ where name = ‘1’;
# at 379
#070119 10:52:10 server id 1  end_log_pos 406   Xid = 5
COMMIT;
# at 406
#070119 10:52:14 server id 1  end_log_pos 474   Query   thread_id=2     exec_time=0     error_code=0
SET TIMESTAMP=1169175134;
BEGIN;
# at 474
#070119 10:51:29 server id 1  end_log_pos 119   Query   thread_id=2     exec_time=0     error_code=0
SET TIMESTAMP=1169175089;
insert into target_tab select d1,name from source_tab where name = ‘1’;
# at 593
#070119 10:52:14 server id 1  end_log_pos 620   Xid = 7
COMMIT;
……
    可以发现,在BINLOG中,更新操作的位置在INSERT…SELECT之前,如果使用这个BINLOG进行数据库恢复,恢复的结果与实际的应用逻辑不符;如果进行复制,就会导致主从数据库不一致!
通过上面的例子,我们就不难理解为什么MySQL在处理“Insert  into target_tab select * from source_tab where …”和“create  table new_tab …select … From  source_tab where …”时要给source_tab加锁,而不是使用对并发影响最小的多版本数据来实现一致性读。还要特别说明的是,如果上述语句的SELECT是范围条件,InnoDB还会给源表加间隙锁(Next-Lock)。
因此,INSERT…SELECT…和 CREATE TABLE…SELECT…语句,可能会阻止对源表的并发更新,造成对源表锁的等待。如果查询比较复杂的话,会造成严重的性能问题,我们在应用中应尽量避免使用。实际上,MySQL将这种SQL叫作不确定(non-deterministic)的SQL,不推荐使用。
如果应用中一定要用这种SQL来实现业务逻辑,又不希望对源表的并发更新产生影响,可以采取以下两种措施:
¡  一是采取上面示例中的做法,将innodb_locks_unsafe_for_binlog的值设置为“on”,强制MySQL使用多版本数据一致性读。但付出的代价是可能无法用binlog正确地恢复或复制数据,因此,不推荐使用这种方式。
¡  二是通过使用“select * from source_tab … Into outfile”和“load data infile …”语句组合来间接实现,采用这种方式MySQL不会给source_tab加锁。

InnoDB在不同隔离级别下的一致性读及锁的差异

前面讲过,锁和多版本数据是InnoDB实现一致性读和ISO/ANSI SQL92隔离级别的手段,因此,在不同的隔离级别下,InnoDB处理SQL时采用的一致性读策略和需要的锁是不同的。同时,数据恢复和复制机制的特点,也对一些SQL的一致性读策略和锁策略有很大影响。将这些特性归纳成如表20-16所示的内容,以便读者查阅。
表20-16                                          InnoDB存储引擎中不同SQL在不同隔离级别下锁比较
隔离级别
        一致性读和锁
SQL
Read Uncommited
Read Commited
Repeatable Read
Serializable
SQL
条件
select
相等
None locks
Consisten read/None lock
Consisten read/None lock
Share locks
范围
None locks
Consisten read/None lock
Consisten read/None lock
Share Next-Key
update
相等
exclusive locks
exclusive locks
exclusive locks
Exclusive locks
范围
exclusive next-key
exclusive next-key
exclusive next-key
exclusive next-key
Insert
N/A
exclusive locks
exclusive locks
exclusive locks
exclusive locks
replace
无键冲突
exclusive locks
exclusive locks
exclusive locks
exclusive locks
键冲突
exclusive next-key
exclusive next-key
exclusive next-key
exclusive next-key
delete
相等
exclusive locks
exclusive locks
exclusive locks
exclusive locks
范围
exclusive next-key
exclusive next-key
exclusive next-key
exclusive next-key
Select … from … Lock in share mode
相等
Share locks
Share locks
Share locks
Share locks
范围
Share locks
Share locks
Share Next-Key
Share Next-Key
Select * from … For update
相等
exclusive locks
exclusive locks
exclusive locks
exclusive locks
范围
exclusive locks
Share locks
exclusive next-key
exclusive next-key
Insert into … Select …
(指源表锁)
innodb_locks_unsafe_for_binlog=off
Share Next-Key
Share Next-Key
Share Next-Key
Share Next-Key
innodb_locks_unsafe_for_binlog=on
None locks
Consisten read/None lock
Consisten read/None lock
Share Next-Key
create table … Select …
(指源表锁)
innodb_locks_unsafe_for_binlog=off
Share Next-Key
Share Next-Key
Share Next-Key
Share Next-Key
innodb_locks_unsafe_for_binlog=on
None locks
Consisten read/None lock
Consisten read/None lock
Share Next-Key
从表20-16可以看出:对于许多SQL,隔离级别越高,InnoDB给记录集加的锁就越严格(尤其是使用范围条件的时候),产生锁冲突的可能性也就越高,从而对并发性事务处理性能的影响也就越大。因此,我们在应用中,应该尽量使用较低的隔离级别,以减少锁争用的机率。实际上,通过优化事务逻辑,大部分应用使用Read Commited隔离级别就足够了。对于一些确实需要更高隔离级别的事务,可以通过在程序中执行SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ或SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE动态改变隔离级别的方式满足需求。

什么时候使用表锁

对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择InnoDB表的理由。但在个别特殊事务中,也可以考虑使用表级锁。
¡  第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
¡  第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。
当然,应用中这两种事务不能太多,否则,就应该考虑使用MyISAM表了。
在InnoDB下,使用表锁要注意以下两点。
(1)使用LOCK TABLES虽然可以给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层──MySQL Server负责的,仅当autocommit=0、innodb_table_locks=1(默认设置)时,InnoDB层才能知道MySQL加的表锁,MySQL Server也才能感知InnoDB加的行锁,这种情况下,InnoDB才能自动识别涉及表级锁的死锁;否则,InnoDB将无法自动检测并处理这种死锁。有关死锁,下一小节还会继续讨论。
(2)在用 LOCK TABLES对InnoDB表加锁时要注意,要将AUTOCOMMIT设为0,否则MySQL不会给表加锁;事务结束前,不要用UNLOCK TABLES释放表锁,因为UNLOCK TABLES会隐含地提交事务;COMMIT或ROLLBACK并不能释放用LOCK TABLES加的表级锁,必须用UNLOCK TABLES释放表锁。正确的方式见如下语句:
例如,如果需要写表t1并从表t读,可以按如下做:
SET AUTOCOMMIT=0;
LOCK TABLES t1 WRITE, t2 READ, …;
[do something with tables t1 and t2 here];
COMMIT;
UNLOCK TABLES;

关于死锁

上文讲过,MyISAM表锁是deadlock free的,这是因为MyISAM总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。但在InnoDB中,除单个SQL组成的事务外,锁是逐步获得的,这就决定了在InnoDB中发生死锁是可能的。如表20-17所示的就是一个发生死锁的例子。
表20-17 InnoDB存储引擎中的死锁例子
session_1
session_2
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from table_1 where where id=1 for update;
做一些其他处理…
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from table_2 where id=1 for update;
select * from table_2 where id =1 for update;
因session_2已取得排他锁,等待
做一些其他处理…
mysql> select * from table_1 where where id=1 for update;
死锁
在上面的例子中,两个事务都需要获得对方持有的排他锁才能继续完成事务,这种循环锁等待就是典型的死锁。
发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB并不能完全自动检测到死锁,这需要通过设置锁等待超时参数 innodb_lock_wait_timeout来解决。需要说明的是,这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。
通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。下面就通过实例来介绍几种避免死锁的常用方法。
(1)在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。在下面的例子中,由于两个session访问两个表的顺序不同,发生死锁的机会就非常高!但如果以相同的顺序来访问,死锁就可以避免。
表20-18        InnoDB存储引擎中表顺序造成的死锁例子
session_1
session_2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select first_name,last_name from actor where actor_id = 1 for update;
+————+———–+
| first_name | last_name |
+————+———–+
| PENELOPE   | GUINESS   |
+————+———–+
1 row in set (0.00 sec)
mysql> insert into country (country_id,country) values(110,’Test’);
Query OK, 1 row affected (0.00 sec)
mysql>  insert into country (country_id,country) values(110,’Test’);
等待
mysql> select first_name,last_name from actor where actor_id = 1 for update;
+————+———–+
| first_name | last_name |
+————+———–+
| PENELOPE   | GUINESS   |
+————+———–+
1 row in set (0.00 sec)
mysql>  insert into country (country_id,country) values(110,’Test’);
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
(2)在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。
表20-19        InnoDB存储引擎中表数据操作顺序不一致造成的死锁例子
session_1
session_2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select first_name,last_name from actor where actor_id = 1 for update;
+————+———–+
| first_name | last_name |
+————+———–+
| PENELOPE   | GUINESS   |
+————+———–+
1 row in set (0.00 sec)
mysql> select first_name,last_name from actor where actor_id = 3 for update;
+————+———–+
| first_name | last_name |
+————+———–+
| ED         | CHASE     |
+————+———–+
1 row in set (0.00 sec)
mysql> select first_name,last_name from actor where actor_id = 3 for update;
等待
mysql> select first_name,last_name from actor where actor_id = 1 for update;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
mysql> select first_name,last_name from actor where actor_id = 3 for update;
+————+———–+
| first_name | last_name |
+————+———–+
| ED         | CHASE     |
+————+———–+
1 row in set (4.71 sec)
(3)在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁,因为当用户申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。具体演示可参见20.3.3小节中的例子。
       (4)前面讲过,在REPEATABLE-READ隔离级别下,如果两个线程同时对相同条件记录用SELECT…FOR UPDATE加排他锁,在没有符合该条件记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可避免问题,如表20-20所示。
表20-20   InnoDB存储引擎中隔离级别引起的死锁例子1
session_1
session_2
mysql> select @@tx_isolation;
+—————–+
| @@tx_isolation  |
+—————–+
| REPEATABLE-READ |
+—————–+
1 row in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+—————–+
| @@tx_isolation  |
+—————–+
| REPEATABLE-READ |
+—————–+
1 row in set (0.00 sec)
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
当前session对不存在的记录加for update的锁:
mysql> select actor_id,first_name,last_name from actor where actor_id = 201 for update;
Empty set (0.00 sec)
其他session也可以对不存在的记录加for update的锁:
mysql> select actor_id,first_name,last_name from actor where actor_id = 201 for update;
Empty set (0.00 sec)
因为其他session也对该记录加了锁,所以当前的插入会等待:
mysql> insert into actor (actor_id , first_name , last_name) values(201,’Lisa’,’Tom’);
等待
因为其他session已经对记录进行了更新,这时候再插入记录就会提示死锁并退出:
mysql> insert into actor (actor_id, first_name , last_name) values(201,’Lisa’,’Tom’);
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
由于其他session已经退出,当前session可以获得锁并成功插入记录:
mysql> insert into actor (actor_id , first_name , last_name) values(201,’Lisa’,’Tom’);
Query OK, 1 row affected (13.35 sec)
(5)当隔离级别为READ COMMITTED时,如果两个线程都先执行SELECT…FOR UPDATE,判断是否存在符合条件的记录,如果没有,就插入记录。此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第1个线程提交后,第2个线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁!这时如果有第3个线程又来申请排他锁,也会出现死锁。
对于这种情况,可以直接做插入操作,然后再捕获主键重异常,或者在遇到主键重错误时,总是执行ROLLBACK释放获得的排他锁,如表20-21所示。
表20-21   InnoDB存储引擎中隔离级别引起的死锁例子2
session_1
session_2
session_3
mysql> select @@tx_isolation;
+—————-+
| @@tx_isolation |
+—————-+
| READ-COMMITTED |
+—————-+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.01 sec)
mysql> select @@tx_isolation;
+—————-+
| @@tx_isolation |
+—————-+
| READ-COMMITTED |
+—————-+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.01 sec)
mysql> select @@tx_isolation;
+—————-+
| @@tx_isolation |
+—————-+
| READ-COMMITTED |
+—————-+
1 row in set (0.00 sec)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.01 sec)
Session_1获得for update的共享锁:
mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update;
Empty set (0.00 sec)
由于记录不存在,session_2也可以获得for update的共享锁:
mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update;
Empty set (0.00 sec)
Session_1可以成功插入记录:
mysql> insert into actor (actor_id,first_name,last_name) values(201,’Lisa’,’Tom’);
Query OK, 1 row affected (0.00 sec)
Session_2插入申请等待获得锁:
mysql> insert into actor (actor_id,first_name,last_name) values(201,’Lisa’,’Tom’);
等待
Session_1成功提交:
mysql> commit;
Query OK, 0 rows affected (0.04 sec)
Session_2获得锁,发现插入记录主键重,这个时候抛出了异常,但是并没有释放共享锁:
mysql> insert into actor (actor_id,first_name,last_name) values(201,’Lisa’,’Tom’);
ERROR 1062 (23000): Duplicate entry ‘201’ for key ‘PRIMARY’
Session_3申请获得共享锁,因为session_2已经锁定该记录,所以session_3需要等待:
mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update;
等待
这个时候,如果session_2直接对记录进行更新操作,则会抛出死锁的异常:
mysql> update actor set last_name=’Lan’ where actor_id = 201;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Session_2释放锁后,session_3获得锁:
mysql> select first_name, last_name from actor where actor_id = 201 for update;
+————+———–+
| first_name | last_name |
+————+———–+
| Lisa       | Tom       |
+————+———–+
1 row in set (31.12 sec)
尽管通过上面介绍的设计和SQL优化等措施,可以大大减少死锁,但死锁很难完全避免。因此,在程序设计中总是捕获并处理死锁异常是一个很好的编程习惯。
如果出现死锁,可以用SHOW INNODB STATUS命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的SQL语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。下面是一段SHOW INNODB STATUS输出的样例:
mysql> show innodb status \G
…….
————————
LATEST DETECTED DEADLOCK
————————
070710 14:05:16
*** (1) TRANSACTION:
TRANSACTION 0 117470078, ACTIVE 117 sec, process no 1468, OS thread id 1197328736 inserting
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1216
MySQL thread id 7521657, query id 673468054 localhost root update
insert into country (country_id,country) values(110,’Test’)
………
*** (2) TRANSACTION:
TRANSACTION 0 117470079, ACTIVE 39 sec, process no 1468, OS thread id 1164048736 starting index read, thread declared inside InnoDB 500
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1216, undo log entries 1
MySQL thread id 7521664, query id 673468058 localhost root statistics
select first_name,last_name from actor where actor_id = 1 for update
*** (2) HOLDS THE LOCK(S):
………
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
………
*** WE ROLL BACK TRANSACTION (1)
……
小结
本章重点介绍了MySQL中MyISAM表级锁和InnoDB行级锁的实现特点,并讨论了两种存储引擎经常遇到的锁问题和解决办法。
对于MyISAM的表锁,主要讨论了以下几点:
(1)共享读锁(S)之间是兼容的,但共享读锁(S)与排他写锁(X)之间,以及排他写锁(X)之间是互斥的,也就是说读和写是串行的。
(2)在一定条件下,MyISAM允许查询和插入并发执行,我们可以利用这一点来解决应用中对同一表查询和插入的锁争用问题。
(3)MyISAM默认的锁调度机制是写优先,这并不一定适合所有应用,用户可以通过设置LOW_PRIORITY_UPDATES参数,或在INSERT、UPDATE、DELETE语句中指定LOW_PRIORITY选项来调节读写锁的争用。
(4)由于表锁的锁定粒度大,读写之间又是串行的,因此,如果更新操作较多,MyISAM表可能会出现严重的锁等待,可以考虑采用InnoDB表来减少锁冲突。
对于InnoDB表,本章主要讨论了以下几项内容。
l         InnoDB的行锁是基于锁引实现的,如果不通过索引访问数据,InnoDB会使用表锁。
l         介绍了InnoDB间隙锁(Next-key)机制,以及InnoDB使用间隙锁的原因。
l         在不同的隔离级别下,InnoDB的锁机制和一致性读策略不同。
l         MySQL的恢复和复制对InnoDB锁机制和一致性读策略也有较大影响。
l         锁冲突甚至死锁很难完全避免。
在了解InnoDB锁特性后,用户可以通过设计和SQL调整等措施减少锁冲突和死锁,包括:
l         尽量使用较低的隔离级别;
l         精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会;
l         选择合理的事务大小,小事务发生锁冲突的几率也更小;
l         给记录集显示加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;
l         不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会;
l         尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响;
l         不要申请超过实际需要的锁级别;除非必须,查询时不要显示加锁;
l         对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。

微信群发消息失败

本来正常运行的微信群发接口,突然群发消息失败了

本来正常运行的微信群发接口,突然发现粉丝收不到群发的消息了,严重影响平台活跃度,从而影响公司利益。

出了问题就要解决,先查自身原因吧,手动请求微信消息群发接口,返回接口调用成功。。。什么鬼,当头一棒啊,难不成是微信平台自身的问题,是队列拥堵,还是服务器挂了?再看看别人家的信息是正常发送的,只有自己的群发没有达到用户端,说不过去了。可是你微信偏偏回复的是我接口调用成功,就是说我把消息成功发送到了你微信平台,但是你没有把消息再发往用户。。。

只能联系微信方了,找了半天找到客服电话,一打,压根没有人工服务。。。。超级吐槽:你微信家大业大的,连个7*24小时人工客服都没有配上。。。

再想其他方法,按照给出咨询方式,提交了问题,一天过去了。。。。,两天过去了。。。。,三天。。。。没有回信。。。。。。我了个去。。。

我司花了三百大洋认证了,为的就是能好好服务我司客户,将高质量信息通过微信第一时间交到用户手里,现在,我花了钱能用你的服务了,把信息给你,你却没有下一步了,只收钱不办事啊,客服不管用,咨询不管用,你功能有问题,你不急,我皇帝(顾客是皇帝是谁说的来着)可急了啊 0.0!

客服咨询走不通,咱也不能干耗着,联系本来好好的突然不能用了这一实情,考虑也许是微信当家的突然对接口做什么更改了,咱在好好确认下的接口文档吧。别说还真发现了

不知道是以前没注意,还是的确是微信新加的功能,在文档群发接口说明里,发现了新大陆——原创校验。大概意思是你群发的文章里如果有被微信认定为原创不属于你的,如果原创人不同意转载,那你的消息我就不给你发。再往下看,发现还有个新参数:send_ignore_reprint,该参数默认为假,校验信息为转载的时候,就不给你群发了。如果设置为真,则继续群发。好吧,我承认,我的信息大多都是从网上借鉴(copy)来的,没有及时查看你的文档跟新是我的错,我改总行了吧。。。

把send_ignore_reprint参数带上,并设置为真,用测试账号测试,还是失败。。。好家伙,send_ignore_reprint是摆设吗?

猜想:send_ignore_reprint参数是 跟 原创是否允许转载 这一设定有上下级关系的。就是说这个参数本身的意义是,你要设置为假,那么只要校验出有转载,我就不给你群发了;你要设置为真,我校验出转载后,还要看看用户是否允许转载,允许就能把消息给你群发了,不允许还是不给你群发

获取不到$_GET参数

遇见了个怪现象,php获取$_GET为空,输出server信息$_SERVER[‘QUERY_STRING’]=”,$_SERVER[‘REQUEST_URI’]里面也没有“?”号后面的参数,查看nginx配置,没有重写,没有问题

今天查看网站wap端页面的时候,发现列表页出不来了,报错没有request参数,很奇怪,以前没问题的怎么突然出现了这个问题

先排除了url本身的问题,从火狐里查看,参数正常发送了,可是服务器端就是收不到,var_dump($_SERVER)查看,发现参数没有到达php,于是怀疑是nginx是不是配置出了问题,考虑到近期没有动过nginx的配置,但是本着细心求证的原则,还是去认真查看了nginx的配置,经过检验,nginx的配置也没有问题,好怪异,到底是哪里的问题,会造成php获取不到get参数呢?

难道是服务器被黑了。。。阿里云的服务器,自己又做了强大的安全配置,得多NX的大神才能黑进来啊,并且php和nginx的配置和运行都没问题,应该不是,不管怎么说,决定看一下nginx的请求日志,发现,请求到达nginx的时候,参数已经被删掉了,这下傻眼了,不用说,应该是被劫持了!!!

想想还不对,在我大天朝享誉国际的都城北京这个天子脚下,谁敢犯如此大不敬之罪,在网络上明目张胆的肆意作乱*_*!

一时想不起来问题所在,不能一直搁置,互联网公司这种问题伤不起,赶紧请教大神:姜还是老的辣,在介绍完情况后,根据我的描述,老大立刻把问题定在能出现这种问题的本源上去——cdn!

恍然大悟,前段时间网站刚刚上了阿里云的cdn功能,在hosts中重新写解析,再测试,发现可以获取参数了,看来是阿里云的cdn,在用户请求url的过程中把“?”后面的参数部分给删除了,那这要怎么办,停止cdn不好,不停动态页面又有问题,两难啊。

阿里家的技术怎么说在国内技术界也是数一数二的,这种问题不可能没有解决办法的,于是赶紧登陆阿里云,认真仔细的研究阿里云的cdn功能,果然,阿里云cdn管理里面有一个过滤参数的功能,当时自己第一次使用cdn,迷迷糊糊不是很了解这个功能,就开启了这个功能,这个功能是开启后,cdn就主动把问号后面的参数部分删除了,添加特定过滤项后,再次去查看列表页,OK,完事大吉!

总结!由于自己对CDN的认识不足,开启了阿里云cdn功能里的参数过滤功能,造成请求动态页面的时候,cdn把动态参数删除了,从而动态页因为获取不到参数而报错,只怪自己学艺不精,见识浅薄。

送自己一句话——人生路上,学无止境

dedecms安装后common.inc.php 文件权限一直是777的解决方法

dede common.inc.php这个文件总是777权限的解决方案

今天无忧主机小编给无忧主机客户在免备案空间安装好dedecms之后,过了没有多久客户找过说是后台提示有安全的问题,于是进去查看了一下common.inc.php这个文件是777权限,按照官方的要求必须是644权限,所以去修改之后刷新了一下又变回了777权限。那么经过精心的去发现以及查看资料终于得到了解决方案。
于是乎无忧主机小编猜测是有文件该文件有写入权限,把权限改了444后问题依然。如何解决?通过测试在后台的点击其它页面文件权限不会改变,那就是一登陆主页产生的写入权限。
接下来无忧主机小编就给大家说说如何解决:
找到目录下的dede/templets下的index_body.htm,全部删除测试看看是不是不会有这样的问题了。后来发现有一个js文件不加载就可以了。
下接将文件的这个js 去掉 就可以了

1 <script language="javascript" src="js/drag.js"></script>

其它不会有这问题的,可将这个common.inc.php改为444,后台就不会再有提示,并且不会自动更改了,如图1所示:

220 dedecms安装后common.inc.php 文件权限一直是777的解决方法

在刷新下,发现没有提示安全问题了,如图2所示:

315 dedecms安装后common.inc.php 文件权限一直是777的解决方法

我们在进入空间,看到文件common.inc.php的权限也不变了。那么这个问题也得到了解决。
温馨提示:该技术解决方案的是由无忧主机客服为我们空间客户处理该问题时提供的处理方法,确保在无忧主机能完美实现,因服务较多,客服繁忙,其他主机我们没有过多精力进行大范围测试,不能确保所有虚拟主机都能完美处理,请您理解!

文章来源:无忧主机http://www.51php.com/dedecms/23260.html

基于 Gitbook 制作电子书

转自“实验楼

一、实验说明

1.1 GitBook 介绍

Modern book format and toolchain using Git and Markdown

GitBook 是一个使用 GitHub/Git 和 Markdown 来制作电子书的命令行工具 (Node.js 库)。

另外,有一个网站 gitbook.com 可以帮助用户更好的使用 Gitbook。同时 gitbook.com 还提供了一个桌面编辑器,帮助用户编辑电子书。Gitbook 与 gitbook.com 的关系类似 Git 和 GitHub,一个是工具,另一个是基于工具创建的网站。

1.2 知识点

本次课程涉及如下知识点:

  • Markdown 书写
  • Git 使用
  • Gitbook 基本操作
  • Github pages

1.3 工作目录

进入实验以后,我们打开桌面上的 Xfce 终端:

$ cd Code
$ mkdir gitbook
$ cd gitbook

文件夹 gitbook 作为我们的工作目录,之后所有的操作都在这个目录下执行。

1.4 效果截图

二、实验操作

2.1 Gitbook 安装

Gitbook 的安装和准备大概需要几分钟。

目前有两种方式可以而让你使用 Gitbook 来编辑、发布和管理电子书:

  • 本地编辑,然后发布到类似 GitHub pages 的第三方平台;
  • 直接使用已有的 gitbook.com 网站。

2.1.1 本地安装

本地安装 Gitbook 需要 NodeJS 支持,推荐安装 NodeJS v4.0.0 及以上版本。

目前安装 Gitbook 最方便的方式就是通过 npm 安装:

sudo npm install -g gitbook-cli
sudo npm install -g gitbook

安装完之后,你可以检验下是否安装成功。

gitbook -V

2.1.2 编辑书籍

安装好 Gitbook 之后,我们就可以创建图书了。

Gitbook 的基本用法非常简单,基本上就只有两步:

  1. 使用 gitbook init 初始化书籍目录
  2. 使用 gitbook serve 编译书籍

首先,进入一个目录,例如之前我们创建好的 gitbook,执行初始化命令:

sudo gitbook init

然后我们的 gitbook 空目录会多出两个文件:

gitbook/
├── README.md
└── SUMMARY.md

README.md 和 SUMMARY.md 是两个必须文件,README.md 是对书籍的简单介绍。SUMMARY.md 是书籍的目录结构。

REAMDE.md:

# Introduction

SUMMARY.md:

# Summary

* [Introduction](README.md)

对上面两个文件,我们做一下编辑:

REAMDE.md:

# Introduction
This is a book powered by [GitBook](https://github.com/GitbookIO/gitbook).

SUMMARY.md:

# Summary

* [Introduction](README.md)
* [第一部分](chapter1/README.md)
    * [1.1 数学](chapter1/math/math.md)
        * [1.1 高等数学](chapter1/math/advance.md)
        * [1.2 离散数学](chapter1/math/lisan.md)
    * [1.2 编程语言](chapter1/program/program.md)
        * [1.2.1 C 语言](chapter1/program/c.md)
        * [1.2.2 C++](chapter1/program/c++.md)
        * [1.2.3 Java](chapter1/program/java.md)
    * [1.3 软件工程](chapter1/software/software.md)
    * [1.4 数据结构](chapter1/data_structure/data_structure.md)
    * [1.5 数据库原理](chapter1/database/database.md)
    * [1.6 计算机网络](chapter1/network/compute_network.md)
    * [1.7 计算机系统](chapter1/system/compute_system.md)
        * [1.7.1 操作系统](chapter1/system/operating.md)
        * [1.7.2 微机接口](chapter1/system/interface.md)
        * [1.7.3 计算机组成原理](chapter1/system/comprise.md)
        * [1.7.4 计算机系统结构](chapter1/system/system_structure.md)
* [第二部分](chapter2/README.md)
    * [2.1 算法](chapter2/algorithm/algorithm.md)
    * [2.2 数据挖掘](chapter2/data_mining/data_mining.md)
    * [2.3 机器学习](chapter2/learning/machine_learning.md)
    * [2.4 搜索引擎](chapter2/search/search_engine.md)

编辑这两个文件之后,再次执行:

gitbook init

它会为我们创建 SUMMARY.md 中的目录结构。

书籍目录结构创建完成以后,就可以使用 gitbook serve 来编译和预览书籍了:

sudo gitbook serve

gitbook serve 命令实际上会首先调用 gitbook build 编译书籍,完成以后会打开一个 web 服务器,监听在本地的 4000 端口。

现在,可以用浏览器打开 http://localhost:4000 查看书籍的效果,如下图:

现在,Gitbook 为我们创建了书籍目录结构后,就可以向其中添加真正的内容了,文件的编写使用 Markdown 语法,在文件修改过程中,每一次保存文件,gitbook serve 都会自动重新编译,所以可以持续通过浏览器来查看最新的书籍效果!

2.2 gitbook.com 使用

gitbook.com 是一个围绕 Gitbook 发行书籍的社区,于 2014 年初创,gitbook.com 提供免费和付费的服务,而且免费账户就可以享受诸多服务,包括:

  • 1 本私有书籍;
  • 托管不限数量的公开书籍;
  • 售卖不限数量的书籍,并分享 80% 的书籍收入;
  • 不限数量的协作者;
  • 免费的在线书籍编辑器。

要使用 gitbook.com 来托管你的书籍,首先需要注册一个账号。

点此注册

登陆 gitbook.com 后,在用户页面,可以管理现有书籍以及创建新的书籍:

创建完成以后,会进入书籍管理页面:

点击 “Edit your book” 按钮,进入一个在线编辑器:

  1. 中间是编辑区域,使用 Markdown 语法;
  2. 左上方是章节目录,鼠标右键可以新增章,在上右键可以新增节,托放可以改变顺序;
  3. 左下方是文件列表,鼠标右键可以新增目录或文件。这里面最必要的是 README.mdSUMMARY.md。前者是书籍的简介,后者是真正决定书籍目录结构的,比如多层次目录结构,需要手动修改 SUMMARY.md(左上方会立刻呈现效果)。
  4. 右侧是预览窗口,可以点击 “眼睛图标”关闭/开启预览区域。其实可以关闭左侧区域和右测预览区域,专心写作。

2.3 发布到 GitHub

我们可以在 GitHub 上创建一个仓库,来管理书籍源码。

注意:

源代码保存到 master 分支,编译出来的静态文件 _book 上传到 gh-pages 分支,这样我们就可以通过 GitHub pages 来发布电子书了。

具体操作:

  • 登录到Github,创建一个新的仓库,名称我们就命令为 book,这样我就就得到了一个 book 的空仓库;
  • 克隆仓库到本地:git clone git@github.com:USER_NAME/book.git;
  • 创建一个新分支:git checkout -b gh-pages,注意,分支名必须为 gh-pages;
  • 将分支 push 到仓库:git push -u origin gh-pages;
  • 切换到主分支: git checkout master

经过这一步处理,我们已经创建好 gh-pages 分支了,有了这个分支,GitHub 会自动为你分配一个访问网址:http://USERNAME.github.io/book

操作到这一步,我们所在目录是在 Code/book/ 下,现在我们需要在 Code 目录下保存我们的电子书的静态文件,切换到Code 目录,克隆远程仓库 bookgh-pages 分支并保存为 book_build:

git clone -b gh-pages git@github.com:USERNAME/book.git book-build

然后我们从 Code/book/_book 目录下把编译好的电子书静态文件复制到 Code/book_build 下,执行:

sudo git add .
sudo git commit -m "add e-book static file"
sudo git push

将静态文件 push 到远程仓库 bookgh-pages 分支。

然后,等十来分钟的样子,你就可以通过访问网址:http://USERNAME.github.io/book来访问到你的在线图书了。之后,每次修改之后,都可以将生成的静态文件 copy 到 book-build 目录,再 push 到远程仓库 bookgh-pages 分支。

2.4 Github 集成

我们之前在 gitbook.com 新建了一本电子书,并且把电子书的源码存放在了 GitHub 上。现在,我们可以将 gitbook.com上的这本电子书与 GitHub 的远程仓库关联起来,我们可以通过向书籍的 GitHub 仓库提交内容来更新书籍。

登陆 gitbook.com 网站,可以看到我们之前创建的电子书,然后点击 “Book Settings” 按钮。

在 “Settings” 栏目中,点击 “GitHub”。

点击 “Select a Repository”,选择我们创建的存放电子书源码的仓库。

确定仓库之后,点击 “Sync” 按钮,同步。

成功!之后,每次在 GitHub 上更新电子书时,都会自动同步到 gitbook.com,并且自动编译成电子书。

三、总结

本次试验完成了对 Gitbook 的简单掌握,学会制作简单的电子书籍,并且发布到 gitbook.com 或者 GitHub pages 上面。

参考:

https://github.com/GitbookIO/gitbook

至哥们儿的疑惑!

既然你诚心诚意的发问了,我就来大发慈悲的告诉你,我是穿梭在网上的——雷锋!^_^

人们似乎总是怀旧的。于是才有“好汉不提当年勇”的说法。然则,并非每个人都有那般惊天动地的经历,绝大多数人,都是平凡地生活着。但他们,也在怀旧。比如,现在很多人怀念八十年代。因为那时,中国人开始了不同的生活。从买东西要票中走过来的人,发现万元户已然成为鼓励的对象,思想上的冲击,是难以言表的。而咱八〇后,虽然在整个八〇年代还只是小孩,然各种各样的记忆,仍印在心里——这些场景,这些物件,总有一款适合你——经历过的,算怀旧,生得晚的,也可以看稀奇。

56283088_1

 

你用过郁美净儿童霜,但一次见过这么多吗?我还真是第一次见。包装盒上那个小女孩,现在也三四十岁了吧?随便查了下,原来,似乎是姓元,好像是检察官。但并不知真假。大家看看,像不像?

56283088_2

 

 

56283088_3

搪瓷脸盘,那时谁家没几个?还得轻拿轻放,一不小心,就会磕破。白色搪瓷出现放射状的破损,露出里面黑色的铁胎。慢慢就锈穿了。这时,铝牙膏拿便派上了用场,剪下一块,用被单车轮胎的胶水粘到脸盘破损处,照样用。

56283088_4

 

少生优生,幸福一生。世易时移,现在又慢慢放开二胎了。新婚学校,现在谁还要?有几个不从电脑里学成行家啊?

56283088_5

 

泡泡糖,比谁吹得更大。叭,吹破了,嘴巴边上,一条条的胶粘住。还能拿来恶作剧,放同时凳子上之类……还有酸梅粉啊、绿豆冰棒啊……

56283088_6

 

教育要从娃娃抓起。一看那厚厚的眼镜,就知道是人才啊……不过咱见到计算机,至少是一九九七、九八年的事了,毕业后去看小学班主任,在她家里第一次用……

 

56283088_7

谁还记得五讲四美三热爱吗?一个时代有一个时代的口号,但五讲四美,却是不过时的长期任务,只是没有这种旧提法而已。讲文明讲礼貌……任重道远啊。

块元素知其所以然

padding和border影响宽度或者高度时,本文章基本可解决问题

box-sizing:content-box | border-box

默认值content-box

适用于:所有接受widthheight的元素

继承性:无

取值:

content-box:
padding和border不被包含在定义的width和height之内。对象的实际宽度等于设置的width值和border、padding之和,即 ( Element width = width + border + padding )
此属性表现为标准模式下的盒模型。
border-box:
padding和border被包含在定义的width和height之内。对象的实际宽度就等于设置的width值,即使定义有border和padding也不会改变对象的实际宽度,即 ( Element width = width )
此属性表现为怪异模式下的盒模型。

示例:

  • content-box:

    .test1{ box-sizing:content-box; width:200px; padding:10px; border:15px solid #eee; }content-box

  • border-box:

    .test2{ box-sizing:border-box; width:200px; padding:10px; border:15px solid #eee; }border-box

<转>apache搭建本地https

apache搭建本地https

转自简书  http://www.jianshu.com/p/aa7f009a5364

直接开始吧!https、openssl、apache相关的东西,不知道的自行百度吧。
第一步修改apache的配置文件:
  • 修改httpd.conf文件
    目录:/etc/apache2/httpd.conf
    修改参数:
    1.LoadModule ssl_module libexec/apache2/mod_ssl.so
    2.Include /etc/apache2/extra/httpd-ssl.conf
    3.Include /etc/apache2/extra/httpd-vhosts.conf
    修改方式:将上述三条前的#去掉开启
  • 修改httpd-ssl.conf文件
    目录:/etc/apache2/extra/httpd-ssl.conf
    1.SSLCertificateFile “/etc/apache2/ssl/server.crt”
    2.SSLCertificateKeyFile “/etc/apache2/ssl/server.key”
    3.SSLCertificateChainFile “/private/etc/apache2/server-ca.crt”
    修改方式:将上述三条前的#去掉开启
  • 编辑httpd-vhosts.conf文件

    屏幕快照 2016-08-30 下午4.21.14.png
相关属性和路径的解释:
httpd.conf:

LoadModule ssl_module libexec/apache2/mod_ssl.so(告诉apache支持ssl)
Include /etc/apache2/extra/httpd-ssl.conf(ssl配置文件)
Include /etc/apache2/extra/httpd-vhosts.conf(虚拟主机配置文件)

httpd-ssl.conf:

SSLCertificateFile “/etc/apache2/ssl/server.crt”(服务端证书的目录)
SSLCertificateKeyFile “/etc/apache2/ssl/server.key”(服务端私钥的目录)
SSLCertificateChainFile “/private/etc/apache2/server-ca.crt”(钥匙链的目录)

httpd-vhosts.conf:

DocumentRoot(默认访问的文件夹路径)
ServerName(域名)
SSLEngine on(打开ssl引擎)
SSLCipherSuite(支持的加密算法)

第二步修改openssl.cnf文件:

目录:/usr/local/etc/openssl/openssl.cnf
1.[ CA_default ]部分dir目录(修改成自己想修改的任何一个目录或者不修改)
2.(前提需要添加别名等扩展信息,别名为例)[ v3_ca ]部分subjectAltName打开(设置为@alt_names[名字随意])
3.(前提需要添加别名等扩展信息,别名为例)[ usr_cert ]部分subjectAltName打开(设置为@alt_names[名字随意])
4.添加[ alt_names ]
IP.1 = 192.168.2.20
5.其他信息根据实际需求更改

第三步生成证书文件:

这里所有的操作都在[ CA_default ]部分dir目录下进行。

准备工作:

1.touch index.txt
2.touch index.txt.attr
3.echo 01 > serial
4.mkdir newcerts
5.mkdir keys

生成根证书:

私钥:openssl genrsa -des3 -out keys/rootca.key 2048
根证书:openssl req -new -x509 -days 3650 -key keys/rootca.key -out keys/rootca.crt

生成二级根证书:

私钥:openssl genrsa -des3 -out keys/secondca.key 2048
去密码:openssl rsa -in keys/secondca.key -out keys/secondCA.key
请求文件:openssl req -new -days 3650 -key keys/secondca.key -out keys/secondca.csr
根证书签名:openssl ca -extensions v3_ca -in keys/secondca.csr -config /usr/local/etc/openssl/openssl.cnf -days 3650 -out keys/secondca.crt -cert keys/rootca.crt -keyfile keys/rootca.key

生成服务端证书:

私钥:openssl genrsa -des3 -out keys/server.key 2048
去密码:openssl rsa -in keys/server.key -out keys/server.key
请求文件:openssl req -new -days 3650 -key keys/server.key -out keys/server.csr
签名:openssl ca -in keys/server.csr -config /usr/local/etc/openssl/openssl.cnf -days 3650 -out keys/server.crt -cert keys/secondca.crt -keyfile keys/secondca.key

生成钥匙链:

钥匙链:cat keys/rootca.crt keys/secondca.crt > chain.crt

第四步生成证书时需要注意的问题

1.countryName、stateOrProvinceName、organizationName生成证书的时候必须保持一致。
2.如果签名的时候发生错误修改openssl.cnf文件下的default_md属性为sha256(sha256为通用的签名算法)
3.Common Name尽量保持不一致,服务端证书生成的时候Common Name为域名
4.如果遇到其他的问题可以留言

第五步将生成的文件对应到配置文件的目录下启动apache

启动apache:sudo apachectl restart
在浏览器中访问:

屏幕快照 2016-08-30 下午5.16.07.png

继续:

屏幕快照 2016-08-30 下午5.21.36.png
第六步客户端的验证(只说AFNetworking)

提示:如果没有使用afn需遵循NSURLSessionTaskDelegate代理,在代理方法里验证即可。
1.在钥匙串中导出根证书的cer文件导入工程
2.关键代码:

AFSecurityPolicy securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
securityPolicy.allowInvalidCertificates = YES;
manager.securityPolicy = securityPolicy;

3.验证结果

结束语

在这里浪费了一些时间,很多原因是因为网上有些博客写的不是很明了或者已经很老了,因此走了一些弯路。所以写出来这篇博客介绍的不算很详尽,但是关键的点都已经写了出来。希望能够帮助到你们。

一老板挖个鱼塘,让用户免费钓鱼,结果……

一老板挖个鱼塘,让用户免费钓鱼,结果……

前几天一鱼塘开张

钓鱼费用是100大洋

老板说没钓到鱼就送一只鸡

于是很多人都去了

回来时每人拎着一只鸡

后来,鱼塘看门老大爷说

老板本来就是个养鸡专业户

这鱼塘就没有鱼

这个方法叫做“去库存”!

过了几天

另一个鱼塘也开张了

钓鱼免费

但钓上的鱼要15元一斤买走

结果还是有许多人去了

奇怪的是

不管会不会钓鱼

都能一天钓几十条

所有人都觉得自己是钓鱼大师

后来,鱼塘看门老大爷说

鱼是从批发市场3块钱一斤买来的

老板的儿子潜在水下

一条一条挂在了他们的鱼钩上……

这个方法叫做“供给侧改革”

过了两天

第三个外地连锁鱼塘开张了

这个鱼塘实行撒网式捕鱼

让顾客穿上蓑衣,戴上斗笠,乘上小舟

扮成渔夫模样,体验农耕文化

鱼塘专门负责派人拍照美图

给顾客发微信朋友圈,提升顾客逼格

最后网到的鱼只要10元一斤

许多人高兴地去了

一网下去就是好几十斤鱼

鱼塘日销售量从500斤上升到10000斤

而且时间周期大大缩短

顾客体验捕鱼很高兴

批发市场也去了库存

后来,鱼塘看门大爷说

这个方法叫做“去杠杆改革“

第四个鱼塘又开张了

受前面三个鱼塘的启发

这鱼塘钓鱼免费

钓上的鱼也可免费拿走

许多人高兴的去了

居然有人钓到了美人鱼!

然后钓鱼的和美人鱼共进午餐

餐饮服务收入比之前钓鱼收入高得多!

看鱼塘的老大爷说

其实美人鱼都是花钱请来的!

这个方法叫做“深层次挖掘客户需求”

最近鱼塘开张比较多

成了热门项目

老板对经常钓鱼的顾客讲

投资鱼塘太赚钱了

半年回本,一年翻倍

为了回馈老顾客,现推出会员激励计划

一次性投入一万八,可享受终身会员待遇

同时享有1%股份,每年分红五千

如果需要钱,还能转让股权

顾客正愁没处投资呢,这么好的项目

不到一上午,一百个人交了钱


后来鱼塘看门大爷说

老板去年跟银行贷款20万,一直逾期

今天终于还上了,欠了半年的工资也发了

这个方法叫“资产证券化”

第五个鱼塘开张当天

媒体广泛报道

很多大腕级的企业家都去取经求道

鱼塘老板招架不住啦

 

最后只得交代:

原来看门大爷才是鱼塘幕后的大股东

主导了每次变革转型的成功

老大爷在接受记者访谈时饱含眼泪哽咽着说:

我以前只是个企业中层,能有今天成就

来源就是不断的学习!


这叫:“知识改变命运,思路决定出路”!

攻下隔壁女生路由器后,我都做了些什么

路由器被蹭网后,会出现的问题

不少人的八卦心窥探欲还是很强烈的,强烈到让人恐惧。所以很多人喜欢看一些八卦文章,比如:如何优雅的窥探别人?

通常这样的文章很容易火起来,因为不少人都喜欢看而且百看不厌。

最近路由安全沸沸扬扬,可以翻阅我微信公众号历史文章查看关于路由安全的文章以及央视当时的采访。鉴于天时地利人和,最后我成功的在现实生活中上演了这样一场精彩好戏,为了满足众人的欲望,我就写成故事。大家好好琢磨琢磨蹭网之后,我们还能做些什么?

声明: 这是一个虚构故事,图片均加万恶马赛克。

———————

记忆中隔壁是一个还算不错的妹子,那天 Z 来找我的时候恰巧碰到了,进屋后跑到我耳边说:“隔壁那个妹子你能不能要到微信,我觉得挺不错的呢~”

这么三俗的场景竟然发生在我的身边,我说等两天我给你消息。

入口

既然是住在隔壁的年轻人,必然不可缺少的就是路由器,于是我打算从路由器当做入口开始这次旅程,将 wifi 打开后发现了三个信号,我首先选择这个名字非常独特的路由: ** LOVE **
http://p1.zhimg.com/91/24/9124c87b6fc9ee16bfa0b17fabd76ef1_m.jpg
根据名字 ** LOVE ** 可大概看出两个人名,应该是男朋友,想到这心里为 Z 君凉了一半。

找到疑似对方入口的地方就好说了,翻出 minidwep(一款 linux 下破解 wifi 密码的工具)导入部分密码字典,开始进行爆破。因对方使用 WPA2 加密方式,所以只能使用暴力破解方式进入,WEP 则目前可以直接破解密码,所以破解 WPA2 加密方式的路由基本上成功率取决于黑客手中字典的大小。

喝了杯咖啡回来,发现密码已经出来了:198707**,于是愉快的连接了进去。

困难

成功连接到对方路由后,下面我需要做的就是连接路由的 WEB 管理界面(进入 WEB 路由管理界面后便可以将路由 DNS 篡改、查看 DHCP 客户端连接设备以及各种功能)。

查看网段后开始访问路由器 WEB 管理界面,发现女神竟然机智的修改了默认登录帐号密码。

TP-LINK W89841N,通过路由设备漏洞进入失败后,想必只能使用暴力美学了。

通过抓取登录路由器的请求,然后遍历帐号密码发送请求查看返回数据包大小则判断是否登录成功,抓到的 GET 请求如下:

其中:Authorization: Basic YWRtaW46YWRtaW4= 为登录的帐号密码

使用 Base64 解密开查看内容:admin:admin

于是我编写了一个 python 脚本将字典中的密码与“admin:”进行组合然后进行 base64 加密,进行破解。十一点的钟声响起,发现密码已经成功爆破出来,成功登录:

查看设备连接列表,发现只有孤零零的自己,看来女神早已歇息,等待时机。

时机

第二天晚饭过后,登录路由管理界面,这时已经有好几个设备了,时机到了:

客户端名

android-b459ce5294bd721f

android-44688379be6b9139

**********iPhone

******-iPad

******-PC

我统计了一下,设备为两个安卓设备、一部 Iphone、一个 ipad、一台个人 PC。

从 iphone\ipad\pc 命名来看,我开始的猜测没错,** 确实是路由主人的名字,直觉告诉我非常大的可能这个路由的主人就是 Z 所心仪的女神。

首先测试两台安卓设备,发现其中一台开放端口很多,隐隐约约中感觉是一台小米盒子或者百度影棒这种产品,这样事情就变得有趣了,因为控制电视可就有机会了。

使用 ARP 嗅探安卓开放端口较多的设备,果然是一个影视盒子:

最后基本摸清:电视使用影视盒子,iphone,ipad 以及一台个人电脑。

寻找

找到了那么多有趣的东西,但我仍然没有忘记 Z 让我帮忙的事情,于是便开始对 iphone 进行了嗅探。

嗅探不一会便找到了有趣的东西,女神的照片在她查看自己相册的时候已经被嗅探到,于是我将照片发给了 Z,他已经激动的语无伦次了。

之后我仍然在等待机会,寻找到对方的微信以便我完成 Z 的愿望,希望出现了。

查看流量日志的时候我发现她在刷新浪微博,于是根据 URL 很方便的找到了微博:

看到生日让我想起了 wifi 连接密码,原来是她的出生日期,心想把微信找到就可以让 Z 安心了。

通过女神新浪微博个性化域名地址和获取到信息加以组合,开始猜测微信帐号,很快便搞定了:

将 Z 的心愿完成后,回过头发现还有很多有趣的事情没做,怎能轻易结束。

电视

随着时代的进步科技的发展,互联网逐渐到了物联网层度,从电视使用各种智能盒子便可以看出。影视盒子通常为了方便调试而开启远程调试端口,盒子究竟安全吗?

去年腾讯安全送的小米盒子让我有幸好好研究一番,扫描端口后发现各种各样的端口大开,其中最有趣的就是 5555 端口(adb 远程调试),使用 adb connect ip 直接可连接设备进行远程调试。

虽然是 Z 的女神,但是我想也可以调侃一番,于是我随手写了一个安卓 APK 程序。

adb 远程连接到盒子,然后 adb install 远程安装 apk,最后使用 am start -n *** 进行远程启动。

我本地使用 Genymotion 建立 android 模拟器进行测试:

当输入 am stat – n *** 敲击回车的那一刹那,脑海中曾想象过千万种女神的表情。

但我始终没有忍下心来给电视播放爱情动作片。

账户

微博、人人、淘宝等等凡是登录过后的帐号全部劫持,通过劫持后的帐号又能看到许多表面看不到的东西。

于是理所当然的账户全部被劫持掉了,当然我并没有去翻阅什么东西,窥探欲早已麻痹。

联系

我想是时候做一个结束了,当然故事过程中还有很多有趣而又精彩的东西实在无法用言语来表达。

于是我没有恶意的拿她的微博发了一条消息:hey,test

通过 MITM 中间人我又向网页中注入了 javascript,大概是这样的:alert(/ 早点休息,QQ:***/);

当然这个 QQ 是我为了取得对方最后联系而注册的:

—————————

追溯源头,其实无非就是很常见的蹭网,连接 wifi 之后设备处于同一个局域网中,于是才能做出那么多有趣的事情,上面这个故事中我未曾有过恶意以及 DNS 劫持,那么我通过路由究竟控制或者得到了哪些信息:

微博
微信
人人网
QQ 号码
手机号(淘宝获取)
照片
电视
More

常说不要连接陌生公开 WIFI,有点儿安全意识。不是没办法黑你,只是你没有被黑的价值。

但是人们总是毫不在意,常说我本来就没啥价值。这样放弃治疗的人令你头疼。

防御

作为小白用户,下面几点做到的越多,你就越安全:

1、路由器连接密码要复杂一点,比如 testak47521test 要比 ak47521 好很多

2、赶紧把路由器管理后台的帐号和密码改掉。90% 的懒人还在 admin admin

3、不要告诉不可信人员你的 Wi-Fi 密码。

4、移动设备不要越狱不要 ROOT,ROOT/ 越狱后的设备等于公交车随便上

5、常登陆路由器管理后台,看看有没有连接不认识的设备连入了 Wi-Fi,有的话断开并封掉 Mac 地址。封完以后马上修改 Wi-Fi 密码和路由器后台帐号密码。

6、More

上面这些方法都搜索的到,防御 ARP 劫持嗅探很简单,电脑上装个杀软基本就差不多,被攻击劫持时候会弹出警告,但是人们却丝毫不当回事儿,出现弹框就把杀软给关掉了,继续上网冲浪。

至于手机上的杀软,还真没啥用,劫持嗅探样样不拦截。

最后 Z 请我吃了一顿大餐 —— 热干面