MySQL/InnoDB的加锁,一直是一个面试中常问的话题。例如,数据库如果有高并发请求,如何保证数据完整性?产生死锁问题如何排查并解决?我在工作过程中,也会经常用到,乐观锁,排它锁,等。于是今天就对这几个概念进行学习,屡屡思路,记录一下。
注:MySQL是一个支持插件式存储引擎的数据库系统。本文下面的所有介绍,都是基于InnoDB存储引擎,其他引擎的表现,会有较大的区别。
存储引擎查看
MySQL给开发者提供了查询存储引擎的功能,我这里使用的是MySQL5.6.4,可以使用:
SHOW ENGINES
乐观锁
用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
举例
1、数据库表设计
三个字段,分别是 id,value、version
selectid,value,versionfromTABLEwhereid=#{id}
2、每次更新表中的value字段时,为了防止发生冲突,需要这样操作
updateTABLE
setvalue=2,version=version+1
whereid=#{id} andversion=#{version};
悲观锁
与乐观锁相对应的就是悲观锁了。悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟java中的synchronized很相似,所以悲观锁需要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。
说到这里,由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。 共享锁和排它锁是悲观锁的不同的实现 ,它俩都属于悲观锁的范畴。
使用,排它锁举例
要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。
我们可以使用命令设置MySQL为非autocommit模式:
set autocommit=0;
# 设置完autocommit后,我们就可以执行我们的正常业务了。具体如下:
# 1. 开始事务
begin;/begin work;/start transaction; (三者选一就可以)
# 2. 查询表信息
select status from TABLE where id=1for update;
# 3. 插入一条数据
insert intoTABLE (id,value) values (2,2);
# 4. 修改数据为
update TABLE setvalue=2where id=1;
# 5. 提交事务
commit;/commit work;
共享锁
共享锁又称 读锁 read lock ,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获得共享锁的事务只能读数据,不能修改数据
打开第一个查询窗口
begin;/beginwork;/starttransaction; (三者选一就可以)
SELECT * fromTABLEwhereid = 1 lockinsharemode;
然后在另一个查询窗口中,对id为1的数据进行更新
update TABLEsetname="www.souyunku.com"whereid =1;
此时,操作界面进入了卡顿状态,过了超时间,提示错误信息
如果在超时前,执行 commit ,此更新语句就会成功。
[SQL]update test_one setname="www.souyunku.com"whereid =1;
[Err] 1205 - Lockwaittimeout exceeded; tryrestarting transaction
加上共享锁后,也提示错误信息
update test_one setname="www.souyunku.com"whereid =1lockinsharemode;
[SQL]update test_one setname="www.souyunku.com"whereid =1lockinsharemode;
[Err] 1064 - You have an error in yourSQL syntax; check the manual thatcorresponds to your MySQLserverversionfor the right syntax touse near 'lock in sharemode'at line 1
在查询语句后面增加 LOCK IN SHARE MODE ,Mysql会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。
加上共享锁后,对于 update,insert,delete 语句会自动加排它锁。
排它锁
排他锁 exclusive lock(也叫writer lock)又称 写锁 。
排它锁是悲观锁的一种实现,在上面悲观锁也介绍过。
若事务 1 对数据对象A加上X锁,事务 1 可以读A也可以修改A,其他事务不能再对A加任何锁,直到事物 1 释放A上的锁。这保证了其他事务在事物 1 释放A上的锁之前不能再读取和修改A。排它锁会阻塞所有的排它锁和共享锁
读取为什么要加读锁呢:防止数据在被读取的时候被别的线程加上写锁,
使用方式:在需要执行的语句后面加上 for update 就可以了
行锁
行锁又分 共享锁 和 排他锁 ,由字面意思理解,就是给某一行加上锁,也就是一条记录加上锁。
注意:行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁。
共享锁:
名词解释:共享锁又叫做读锁,所有的事务只能对其进行读操作不能写操作,加上共享锁后在事务结束之前其他事务只能再加共享锁,除此之外其他任何类型的锁都不能再加了。
SELECT * fromTABLEwhereid = "1" lockinsharemode; 结果集的数据都会加共享锁
排他锁:
名词解释:若某个事物对某一行加上了排他锁,只能这个事务对其进行读写,在此事务结束之前,其他事务不能对其进行加任何锁,其他进程可以读取,不能进行写操作,需等待其释放。
selectstatusfromTABLEwhereid=1forupdate;
可以参考之前演示的共享锁,排它锁语句
由于对于表中,id字段为主键,就也相当于索引。执行加锁时,会将id这个索引为1的记录加上锁,那么这个锁就是行锁。
表锁
如何加表锁
innodb 的行锁是在有索引的情况下,没有索引的表是锁定全表的.
Innodb中的行锁与表锁
前面提到过,在Innodb引擎中既支持行锁也支持表锁,那么什么时候会锁住整张表,什么时候或只锁住一行呢?只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。
行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁。行级锁的缺点是:由于需要请求大量的锁资源,所以速度慢,内存消耗大。
死锁
死锁(Deadlock) 所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。
解除正在死锁的状态有两种方法:
第一种:
1.查询是否锁表
showOPENTABLESwhere In_use > 0;
2.查询进程(如果您有SUPER权限,您可以看到所有线程。否则,您只能看到您自己的线程)
showprocesslist
3.杀死进程id(就是上面命令的id列)
killid
第二种:
1:查看当前的事务
SELECT * FROMINFORMATION_SCHEMA.INNODB_TRX;
2:查看当前锁定的事务
SELECT * FROMINFORMATION_SCHEMA.INNODB_LOCKS;
3:查看当前等锁的事务
SELECT * FROMINFORMATION_SCHEMA.INNODB_LOCK_WAITS;
杀死进程
kill线程ID
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。产生死锁的四个必要条件:
(1)互斥条件:一个资源每次只能被一个进程使用。(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(3)不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
虽然不能完全避免死锁,但可以使死锁的数量减至最少。将死锁减至最少可以增加事务的吞吐量并减少系统开销,因为只有很少的事务回滚,而回滚会取消事务执行的所有工作。由于死锁时回滚而由应用程序重新提交。
下列方法有助于最大限度地降低死锁:
(1)按同一顺序访问对象。(2)避免事务中的用户交互。(3)保持事务简短并在一个批处理中。(4)使用低隔离级别。(5)使用绑定连接。
参考 :
https://blog.csdn.net/puhaiyang/article/details/72284702
https://www.jb51.net/article/78088.htm
1、 简介
当今MySQL使用相当广泛,随着用户的增多以及数据量的增大,高并发随之而来。然而我们有很多办法可以缓解数据库的压力。分布式数据库、负载均衡、读写分离、增加缓存服务器等等。这里我们将采用读写分离技术进展缓解数据库的压力。
其中实现读写分离的技术有很多方法,这里我们将采用mysql-proxy这个中间软件来实现。这个软件中含有一个读写分离的lua文件,这也是我们使用mysql-proxy实现读写分离必用的文件,它需要lua解析器进行解析。因此我们还需要安装一个lua解析器。
2、基本环境
三台linux虚拟主机
Linux版本CentOS6.6、MySQL 5.5
mysql-proxy-0.8.5
lua-5.1.4
ip:192.168.95.11(写)、192.168.95.12(读)、192.168.95.13(mysql-proxy)
3、配置主从复制
详细可以参考:mysql主从复制与主主复制
http://www.cnblogs.com/phpstudy2015-6/p/6485819.html#_label2
粗略介绍一下数据库的主从复制的配置:
第一步:
在192.168.95.11中创建一个192.168.95.12主机中可以登录的MySQL用户
用户:mysql12
密码:mysql12
mysql>
GRANT
REPLICATION
SLAVE
ON
*.*
TO
‘mysql12’@’
192.168.95.12
’
IDENTIFIED
BY
‘mysql12’;
mysql>
FLUSH
PRIVILEGES
;
第二步:
查看192.168.95.11MySQL服务器二进制文件名与位置
mysql>
SHOW MASTER STATUS;
第三步:
告知二进制文件名与位置
在192.168.95.12中执行:
mysql> change master to
-> master_host=
'192.168.95.11'
,
->
master_user=
'mysql12'
,
->
master_password=
'mysql12'
,
->
master_log_file=
'mysql-bin.000124'
,
->
master_log_pos=
586
;
第四步:
在192.168.95.12中
mysql>
SLAVE START;
#
开启复制
mysql>
SHOW SLAVE STATUS\G
#
查看主从复制是否配置成功
主从复制配置成功!
(注意:上面Relicate_Do_DB:aa表示主从复制只针对数据库aa【这是我之前设置的就没改了】,这里就不讲这个了,要想去了解学医这个的话可以参考文章:
http://www.cnblogs.com/phpstudy2015-6/p/6485819.html#_label7
4、MySQL读写分离配置
百度云下载:
链接:http://pan.baidu.com/s/1slTl18L
密码:9j0m
4.1、安装lua
官网下载:http://www.lua.org/download.html
Lua 是一个小巧的脚本语言。Lua由标准C编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。
一个完整的Lua解释器不过200k,在目前所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择。
1)、安装lua需要依赖很多软件包。
可以通过rpm -qa | grep name检查以下软件是否安装:
gcc*、gcc-c++*、autoconf*、automake*、zlib*、libxml*、ncurses-devel*、libmcrypt*、libtool*、flex*、pkgconfig*、libevent*、glib*
若缺少相关的软件包,可通过yum -y install方式在线安装,或直接从系统安装光盘中找到并通过rpm -ivh方式安装。(我的话一般是直接在系统光盘软件库中找到直接rpm安装的,有些找不到,则先在网上下载然后在ftp传给linux再进行安装)
2)、依赖软件安装完毕后则进行编译安装lua
MySQL-Proxy的读写分离主要是通过rw-splitting.lua脚本实现的,因此需要安装lua。
官网下载:
http://www.lua.org/download.html(下载源码包)
#
wget http://www.lua.org/ftp/lua-5.1.4.tar.gz
#
tar zxvf lua-5.1.4.tar.gz
#
cd
lua-5.1.4
#
make linux
#
make install
#
export
LUA_CFLAGS=
"-I/usr/local/include"
LUA_LIBS=
"-L/usr/local/lib -llua -ldl"
LDFLAGS=
"-lm"
(我安装的时候是直接在光盘软件库中找到,直接rpm安装)
4.2、安装mysql-proxy
1)、首先查看linux版本确认是32位还是64为系统
查看linux内核版本
# cat /etc/issue
查看linux版本
# cat /proc/version
2)、按系统位数下载(上面百度云链接64位的文件)
3)、安装
#
tar –zxvf mysql-proxy-0.8.5- linux-rhel5-x86-64bit.tar.gz
#
mkdir /usr/
local
/mysql-proxy
#
cp ./ mysql-proxy-0.8.5-linux-rhel5-x86-64bit/* /usr/
local
/mysql-proxy
#
cd
/usr/
local
/mysql-proxy
安装成功
5、MySQL读写分离测试
1)、修改rw-splitting.lua文件
修改默认连接,进行快速测试,不修改的话要达到连接数为4时才启用读写分离
#cp/usr/local/mysql-proxy/share/doc/mysql-proxy/rw-splitting.lua ./
# vi rw-splitting.lua
2)、修改完成后,启动mysql-proxy
#
cd
/usr/
local
/mysql/bin
#
./mysql-proxy --proxy-read-only-backend-addresses=192.168.95.12:3306 --proxy-backend-addresses=192.168.95.11:3306 --proxy-lua-script=/usr/
local
/mysql-proxy/rw-splitting.lua &
参数:
--proxy-read-only-backend-addresses
#
只读服务器地址(ip)
--proxy-backend-addresses
#
服务器地址(主服务器)
--proxy-lua-script
#lua
脚本路劲
&
#
表示后台执行
3)、创建用于读写分离的数据库连接用户
用户名:proxy1
密 码:321
mysql>grant
all
on
*.*
to
'proxy1'
@
'192.168.95.13'
identified
by
'321'
;
mysql>use
aa;
mysql>create
table
tab1(id
int
auto_increment,name
varchar
(
32
)
not
null
,primary
key
(id));
【因为已经开启了主从复制所以,11、12主机mysql中都创建了这个用户】
4)、测试登陆账号[email protected]进行添加数据
可以使用任意ip客户端登陆这个账号
在192.168.95.13登陆:
#
./mysql -u proxy1 -P4040 -h192.168.95.13 –p
在两个mysql中查看结果:一致
结果表明:账号使用
(ps:id是自增长,之前高主主复制的时候更改了配置文件,还没更改回来,就将就用着先吧)
5)、关闭12mysql的从复制
mysql> stop slave;
6)、证明写分离
使用[email protected]账号打开多个客户端进行插入数据
打开三个mysql客户端分别插入2条数据:
mysql>
insert
into
tab1 (name)
values
(
'stop_slave11111'
);
….
mysql>
insert
into
tab1 (name)
values
(
'stop_slave6666’);
查看:
分别登陆11mysql与12mysql查看aa.tab1中的数据
主数据库:
从数据库:
结果中显示插入的数据存在与主数据库,而从数据库没有,所以证明写能够分离。
7)、证明读分离
使用[email protected]账号登陆mysql,查看aa.tab1中的数据
mysql>use
aa;mysql>select*from
tab1;
结果中显示只有从数据库的数据,结合上面的测试,可以证明读分离。
6、建议
为了方便启动与管理mysql-proxy可以创建mysql-proxy服务管理脚本
下面这个管理脚本仅适合以上我给出的安装路径位置
【此管理脚本需要按照自己的安装路径做出相应的修改方可使用】
#!/bin/sh
#
# mysql-proxy This script starts and stops the mysql-proxy daemon
#
# chkconfig: - 78 30
# processname: mysql-proxy
# description: mysql-proxy is a proxy daemon to mysql
# Source function library.
. /etc/rc.d/init.d/
functions
#PROXY_PATH=/usr/local/bin
PROXY_PATH=/usr/
local
/mysql-proxy/bin
prog=
"mysql-proxy"
# Source networking configuration.
. /etc/sysconfig/network
# Check that networking is up.
[
${NETWORKING}
=
"no"
] &&
exit
0
# Set default mysql-proxy configuration.
#PROXY_OPTIONS="--daemon"
PROXY_OPTIONS=
"--proxy-read-only-backend-addresses=192.168.95.12:3306 --proxy-backend-addresses=192.168.95.11:3306 --proxy-lua-script=/usr/local/mysql-proxy/rw-splitting.lua"
PROXY_PID=/usr/
local
/mysql-proxy/run/mysql-proxy.pid
# Source mysql-proxy configuration.
if
[ -f /etc/sysconfig/mysql-proxy ];
then
. /etc/sysconfig/mysql-proxy
fi
PATH=
$PATH
:/usr/bin:/usr/
local
/bin:
$PROXY_PATH
# By default it's all good
RETVAL=0
# See how we were called.
case
"
$1
"
in
start)
# Start daemon.
echo
-n $
"Starting
$prog
: "
$NICELEVEL
$PROXY_PATH
/mysql-proxy
$PROXY_OPTIONS
--daemon --pid-file=
$PROXY_PID
--user=root --
log
-level=debug --
log
-file=/usr/
local
/mysql-proxy/
log
/mysql-proxy.log
RETVAL=$?
echo
if
[
$RETVAL
= 0 ];
then
touch /var/lock/subsys/mysql-proxy]
echo
"ok"
fi
;;
stop)
# Stop daemons.
echo
-n $
"Stopping
$prog
: "
killproc
$prog
RETVAL=$?
echo
if
[
$RETVAL
= 0 ];
then
rm -f /var/lock/subsys/mysql-proxy
rm -f
$PROXY_PID
fi
;;
restart)
$0
stop
sleep 3
$0
start
;;
condrestart)
[ -e /var/lock/subsys/mysql-proxy ] &&
$0
restart
;;
status)
status mysql-proxy
RETVAL=$?
;;
*)
echo
"Usage:
$0
{start|stop|restart|status|condrestart}"
RETVAL=1
;;
esac
exit
$RETVAL
#
---
我将mysql-proxy服务管理脚本放在了/usr/
local
/mysql-proxy/init.d/
文件夹里
#
---
给执行权限,建立相应目录
#
chmod +x /usr/
local
/mysql-proxy/init.d/mysql-proxy
#
mkdir /usr/
local
/mysql-proxy/run
#
mkdir /usr/
local
/mysql-proxy/
log
#
cd
/usr/
local
/mysql-proxy/init.d/
#
---
启动mysql-proxy
#
./mysql-proxy start
#
---
停止mysql-proxy
#
./mysql-proxy stop
#
---
重启mysql-proxy
#
./mysql-proxy restart
一些相关参数:
PROXY_PATH=/usr/local/mysql-proxy/bin //
定义mysql-proxy服务二进制文件路径
--proxy-read-only-backend-addresses=
192.168
.
95.12
:
3306
//
定义后端只读从服务器地址
--proxy-backend-addresses=
192.168
.
95.11
:
3306
//
定义后端主服务器地址
--proxy-lua-script=/usr/local/mysql-proxy/rw-splitting.lua
//
定义lua读写分离脚本路径
PROXY_PID=/usr/local/mysql-proxy/run/mysql-proxy.pid //
定义mysql-proxy PID文件路径
--daemon //
定义以守护进程模式启动
--keepalive //
使进程在异常关闭后能够自动恢复【上面的管理脚本没有加上此参数】
--user=root //
以root用户身份启动服务
--
log
-level=debug
//
定义
log
日志级别,由高到低分别有(error|warning|info|message|debug)
--
log
-file=/usr/local/mysql-proxy/
log
/mysql-proxy.
log
//
定义
log
日志文件路径