一 引言--为何mysql提供了锁html
最近看到了mysql有行锁和表锁两个概念,越想越疑惑。为何mysql要提供锁机制,并且这种机制不是一个摆设,还有不少人在用。在现代数据库里几乎有事务机制,acid的机制应该能解决并发调度的问题了,为何还要主动加锁呢?mysql
后来看到一篇文章,“防止更新丢失,并不能单靠数据库事务控制器来解决,须要应用程序对要更新的数据加必要的锁来解决”。瞬间,世界观都崩塌了。很是不敢相信,因而本身写了代码检验一下。sql
数据库表是这样的。用count字段来作100次累加。数据库
为了保证明验的科学性,先确认了数据库是InnoDB的,这样才有事务机制;也确认了隔离性级别编程
定义一个任务,读count值--程序count++--写数据库安全
public class LostUpdate implements Runnable{ private CountDownLatch countDown; public LostUpdate(CountDownLatch countDown){ this.countDown = countDown; } @Override public void run() { Connection conn=null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8", "root", "123"); } catch (Exception e) { e.printStackTrace(); return; } try { conn.setAutoCommit(false); //不加锁的状况 PreparedStatement ps =conn.prepareStatement("select * from LostUpdate where id =1"); //加锁的状况 //PreparedStatement ps =conn.prepareStatement("select * from LostUpdate where id =1 for update"); ResultSet rs=ps.executeQuery(); int count = 0; while(rs.next()){ count= rs.getInt("count"); } count++; ps =conn.prepareStatement("update LostUpdate set count=? where id =1"); ps.setInt(1, count); ps.executeUpdate(); conn.commit(); } catch (Exception e) { try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } //表示一次任务完成 countDown.countDown(); } }
主线程下建立子线程,模拟多线程环境数据结构
public class TestLock { public static void main(String[] args) throws InterruptedException { //建立线程池,里面有10个线程,共执行100次+1操做 final int THREAD_COUNT=10; final int RUN_TIME=100; ExecutorService threadPool=Executors.newFixedThreadPool(THREAD_COUNT); //用CountDownLatch保证主线程等待全部任务完成 CountDownLatch count=new CountDownLatch(RUN_TIME); for(int i=0;i<RUN_TIME;i++) threadPool.execute(new LostUpdate(count)); threadPool.shutdown(); count.await(); //提示全部任务执行完 System.out.println("finish"); } }
运行结果是:多线程
大概解释一下程序,就是建立了一个线程池,里面10个线程,执行100次任务。每一个任务就是 读count值--程序count++--写数据库,经典的银行存款(丢失修改)问题。事实胜于雄辩,结论就是上面的橙色字,解决丢失修改不能靠事务,要加必要的锁,因此数据库提供的锁不是个摆设。并发
二 数据库事务机制mvc
为了找到问题的根源,为了拯救我崩溃的世界观,我又去回顾了数据库事务的知识。借鉴 这篇
数据库的acid属性
说好的一致性呢,童话里都是骗人的!!
事务并发调度的问题
三级封锁协议
可见,三级锁操做一个比一个厉害(知足高级锁则必定知足低级锁)。但有个很是致命的地方,一级锁协议就要在第一次读加x锁,直到事务结束。几乎就要在整个事务加写锁了,效率很是低。三级封锁协议只是一个理论上的东西,实际数据库经常使用另外一套方法来解决事务并发问题。
隔离性级别
mysql用意向锁(另外一种机制)来解决事务并发问题,为了区别封锁协议,弄了一个新概念隔离性级别:包括Read Uncommitted、Read Committed、Repeatable Read、Serializable,见这篇。mysql 通常默认Repeatable Read。
终于发现本身为何会误会事务能解决丢失修改了。至于为何隔离性级别不解决丢失修改,我猜是有更好的解决方案吧。
总结一下,repeatable read能解决脏读和不可重复读,但不嗯呢该解决丢失修改。
三 mysql的行锁和表锁
说了那么久,终于入正题了,先来讲说什么是行锁和表锁。
1 MyISAM的锁
稍微提一下MyISAM,只说和InnoDB不一样的。
a. MyISAM只有表锁,锁又分为读锁和写锁。
b. 没有事务,不用考虑并发问题,世界和平~
c. 因为锁的粒度太大,因此当该表写并发量较高时,要等待的查询就会不少了。优化见 这里。
2 InnoDB的行锁和表锁
没有特定的语法。mysql的行锁是经过索引体现的,参考。
若是where条件中只用到索引项,则加的是行锁;不然加的是表锁。好比说主键索引,惟一索引和聚簇索引等。若是sql的where是全表扫描的,想加行锁也心有余而力不足。
行锁和表锁对咱们编程有什么影响,要在where中尽可能只用索引项,不然就会触发表锁。另外一个多是,咱们发疯了地想优化查询,但where子句中就是有非索引项,因而咱们本身写链接?
行锁和表锁各适合怎么样的应用,待求证?。
3 读锁和写锁
InnoDB用意向锁?实现隔离性级别,原理未名,贴张图:
回想锁协议,对什么操做加什么锁是一个问题,加锁加到何时有是一个问题。锁协议里经常会看到“加锁直到事务结束”的烦心字样。而在InnoDB中,select,insert,update,delete等语句执行时都会自动加解锁。select的锁通常执行完就释放了,修改操做的X锁会持有到事务结束,效率高不少。至于详细的加锁原理,见这里,搜“InnoDB存储引擎中不一样SQL在不一样隔离级别下锁比较”
mysql也给用户提供了加锁的机会,只要在sql后加LOCK IN SHARE MODE 或FOR UPDATE
共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE
值得注意的是,本身加的锁没有释放锁的语句,因此锁会持有到事务结束。
mysql 还提供了LOCK TABLES,UNLOCK TABLES,用于加表锁,怎么用还不太清楚?
4 考察加锁的状况
加了读锁仍是写锁,加了行锁仍是表锁,说何时释放,能够从原理上分析。但刚开始时我不太懂原理,因而又写了个程序。
public class ForUpdate1 implements Runnable{ private CountDownLatch countDown; public ForUpdate1(CountDownLatch countDown){ this.countDown = countDown; } @Override public void run() { Connection conn=null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8", "root", "123"); } catch (Exception e) { e.printStackTrace(); return; } try { conn.setAutoCommit(false); /*PreparedStatement ps =conn.prepareStatement("select * from LostUpdate where id =1 for update"); ps.executeQuery();*/ PreparedStatement ps =conn.prepareStatement("update LostUpdate set count =1 where id =1"); ps.executeUpdate(); Thread.sleep(10000); conn.commit(); System.out.println("test 1 finish"); countDown.countDown(); } catch (Exception e) { try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } } }
public class ForUpdate2 implements Runnable{ private CountDownLatch countDown; public ForUpdate2(CountDownLatch countDown){ this.countDown = countDown; } @Override public void run() { Connection conn=null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8", "root", "123"); } catch (Exception e) { e.printStackTrace(); return; } try { Thread.sleep(2000); conn.setAutoCommit(false); PreparedStatement ps =conn.prepareStatement("select * from LostUpdate where id =1 for update"); ps.executeQuery(); /*PreparedStatement ps =conn.prepareStatement("update LostUpdate set count =1 where id =1"); ps.executeUpdate();*/ conn.commit(); System.out.println("test 2 finish"); countDown.countDown(); } catch (Exception e) { try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } } }
public class TestForUpdate { public static void main(String[] args) throws InterruptedException { final int THREAD_COUNT=10; ExecutorService threadPool=Executors.newFixedThreadPool(THREAD_COUNT); CountDownLatch count=new CountDownLatch(2); threadPool.execute(new ForUpdate1(count)); threadPool.execute(new ForUpdate2(count)); threadPool.shutdown(); count.await(); System.out.println("finish"); } }
只有两个线程,ForUpdate1先执行sql语句以后等10s,ForUpdate2先等待2s再执行sql语句。因此若是ForUpdate1持有锁,并且ForUpdate2等待,输出就应该是test 1 finish->test 2 finish->finish;不然就是test 2 finish->test 1 finish->finish。
这个程序改一下能测试上面说的理论:
判断加锁状况,mysql应该有工具,但没找到?
能够经过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺状况:
mysql> show status like 'innodb_row_lock%';
若是发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,还能够经过设置InnoDB Monitors来进一步观察发生锁冲突的表、数据行等,并分析锁争用的缘由。不明觉厉?,看这篇
总结一下这一章,mysql提供了行锁和表锁,咱们写语句时应该尽可能启动行锁,以提升效率;另外一方面,也说了一下读锁和写锁的原理。好了武器(原理)咱们都懂了,那就看怎么优化了。
四 解决丢失修改--乐观锁和悲观锁
首先为何要加锁,加锁就是为了解决丢失修改(也不知道这么说对不对)。若是一个事务中只有一句sql,数据库是能够保证它是并发安全的。丢失修改的特征就是在一个事务中先读P数据,再写P数据,注意是同一个数据(也不知道这么说对不对)。只是本身推理了一下,没有太强的理据。所谓丢失修改,通常是A事务有两个操做,后一个操做依赖于前一个操做,以后后一个操做覆盖了B事务的写操做,能够表示为这样。
pro1多是Read(P),Write(P),Read(Q),Write(Q),其中P=2Q,数据库中的冗余致使的关联关系是很常见的。
综上,若是一个事务先读后写同一份数据,就可能发生丢失修改,要作一些处理。能够用下面的乐观锁和悲观锁解决。
悲观锁和乐观锁的概念:
悲观锁(Pessimistic Concurrency Control,PCC):假定会发生并发冲突,屏蔽一切可能违反数据完整性的操做。至于怎么加锁,加锁的范围也没讲。
乐观锁(Optimistic Concurrency Control,OCC):假设不会发生并发冲突,只在提交操做时检查是否违反数据完整性。也没具体指定怎么检查。
就是这么概念,什么都不说清楚。毕竟乐观锁和悲观锁也不只仅能用在数据库中,也能用在线程中。
悲观的缺陷是不管是页锁仍是行锁,加锁的时间可能会很长,这样可能会长时间的限制其余用户的访问,也就是说悲观锁的并发访问性很差。
乐观锁不能解决脏读,加锁的时间要比悲观锁短(只是在执行sql时加了基本的锁保证隔离性级别),乐观锁能够用较大的锁粒度得到较好的并发访问性能。可是若是第二个用户刚好在第一个用户提交更改以前读取了该对象,那么当他完成了本身的更改进行提交时,数据库就会发现该对象已经变化了,这样,第二个用户不得不从新读取该对象并做出更改。
可见,乐观锁更适合解决冲突几率极小的状况;而悲观锁则适合解决并发竞争激烈的状况,尽可能用行锁,缩小加锁粒度,以提升并发处理能力,即使加行锁的时间比加表锁的要长。
悲观锁的例子
并无人说悲观锁要怎么加锁,加锁的范围如何。这里仅仅提供一种解决丢失修改的悲观锁例子。
丢失修改咱们用第一章讲到的累积100次的例子。综合前面讲到的结论,丢失修改的特征就是在一个事务中先读P数据,再写P数据。并且一级锁协议能解决丢失修改,因此若是事务A 中写P,咱们只要在A中第一次读P前加X锁。作法在第一章程序中有:
//把 PreparedStatement ps =conn.prepareStatement("select * from LostUpdate where id =1"); //换成 PreparedStatement ps =conn.prepareStatement("select * from LostUpdate where id =1 for update");
乐观锁的例子
乐观锁也没有指定怎么检测并发冲突,下面是常见的两种作法(参考):
下面写两个例子,背景仍是一开始的累积100次的丢失修改问题,都是用version解决的。
1 当发生冲突时回滚并抛异常
任务类
public class LostUpdateOccDiscard implements Runnable{ private CountDownLatch countDown; public LostUpdateOccDiscard(CountDownLatch countDown){ this.countDown = countDown; } @Override public void run() { Connection conn=null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8", "root", "123"); } catch (Exception e) { e.printStackTrace(); return; } try { conn.setAutoCommit(false); //读的时候一并读出version PreparedStatement ps =conn.prepareStatement("select * from LostUpdate where id =1"); ResultSet rs=ps.executeQuery(); int count = 0; int version = 0; while(rs.next()){ count= rs.getInt("count"); version= rs.getInt("version"); } count++; //更新操做,用cas原子操做来更新 ps =conn.prepareStatement("update LostUpdate set count=?, version=version+1 where id =1 and version=?"); ps.setInt(1, count); ps.setInt(2, version); int result = ps.executeUpdate(); //检查有无因冲突致使执行失败 //成功,则commit,完成任务 if(result>0) { conn.commit(); } //失败,回滚,抛异常提醒调用者出现冲突。 else{ conn.rollback(); throw new Exception("更新count出现冲突"); } } catch (SQLException e) { try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } catch (Exception e) { System.out.println(e.getMessage()); } //表示一次任务完成 countDown.countDown(); } }
主线程,和前面差很少,建立10个线程,执行100个任务。
public class TestLockOcc { public static void main(String[] args) throws InterruptedException { //建立线程池,里面有10个线程,共执行100次+1操做 final int THREAD_COUNT=10; final int RUN_TIME=100; ExecutorService threadPool=Executors.newFixedThreadPool(THREAD_COUNT); //用CountDownLatch保证主线程等待全部任务完成 CountDownLatch count=new CountDownLatch(RUN_TIME); for(int i=0;i<RUN_TIME;i++) threadPool.execute(new LostUpdateOccDiscard(count)); threadPool.shutdown(); count.await(); //提示全部任务执行完 System.out.println("finish"); } }
输出结果:在console里出了一堆异常,看数据库,大概累积了10-12次
不要怀疑,程序没有问题。
a. 对着上面说的version方法的原理,程序也比较好懂。
b. 更新时要用cas(compare and set)的原子操做,一步搞定。而不是先读一次version,比较完再执行依据update。想一想也知道后者在多线程有问题。
至于为何只累积了10-12次,缘由是这个累加的并发量是10,就是有10个线程在争夺着修改权。九死一辈子啊,1个线程commit了,就意味着9个线程要rollback抛异常。
2 当发生冲突时重试,有时咱们咱们不但愿程序里那么多异常
任务类
public class LostUpdateOcc implements Runnable{ private CountDownLatch countDown; public LostUpdateOcc(CountDownLatch countDown){ this.countDown = countDown; } @Override public void run() { Connection conn=null; try { Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8", "root", "123"); } catch (Exception e) { e.printStackTrace(); return; } try { int try_times=100; int count; int version; PreparedStatement ps; ResultSet rs; //把循环条件放在里面if里 while(try_times>0){ //开始事务 try_times--; conn.setAutoCommit(false); //读操做 ps=conn.prepareStatement("select * from LostUpdate where id =1"); rs=ps.executeQuery(); //判断事务执行的条件,首先是能执行,其次是须要执行 if(rs.next()){ count= rs.getInt("count"); version= rs.getInt("version"); count++; //更新操做,用cas原子操做来更新 ps =conn.prepareStatement("update LostUpdate set count=?, version=version+1 where id =1 and version=?"); ps.setInt(1, count); ps.setInt(2, version); int result = ps.executeUpdate(); //每次执行完更新操做,检测一次冲突 //成功,则继续事务 //失败,回滚,睡100ms,避开竞争。结束此次循环,开启新事务。 if(result==0) { conn.rollback(); Thread.sleep(100); continue; } //事务一帆风顺,没遇到冲突,事务提交,跳出while conn.commit(); break; } //做为while条件不成立时的处理,好比该行数据被删除。 else{ conn.rollback(); break; } } if(try_times<=0) throw new Exception("冲突重试的此时过多,事务失败"); System.out.println(try_times); } catch (SQLException e) { try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); }catch (Exception e) { System.out.println(e.getMessage()); } //表示一次任务完成 countDown.countDown(); } }
主线程,和前面差很少,建立10个线程,执行100个任务。
public class TestLockOcc { public static void main(String[] args) throws InterruptedException { //建立线程池,里面有10个线程,共执行100次+1操做 final int THREAD_COUNT=10; final int RUN_TIME=100; ExecutorService threadPool=Executors.newFixedThreadPool(THREAD_COUNT); //用CountDownLatch保证主线程等待全部任务完成 CountDownLatch count=new CountDownLatch(RUN_TIME); for(int i=0;i<RUN_TIME;i++) threadPool.execute(new LostUpdateOcc(count)); threadPool.shutdown(); count.await(); //提示全部任务执行完 System.out.println("finish"); } }
任务类里就有比较多要注意的
a. 为了避免断的重试,用了一个while。由于while的终止条件通常要读了数据后才知道,因此while只放了try_times,把结束条件放在了里面的if。
b. 在while里的每一次循环就从新起一个事务。由于更新失败咱们要回滚的。下一次要重起一个。
c. 这里的事务执行条件,能执行且须要执行。好比id=1的记录被删掉了,那就不能执行了;须要执行,好比程序为了把商品记录status由未上架改成已上架,但发现已经被改了,那就不须要执行。可想而知,在多线程条件每次都要判断的。
d. try_times这个东西仍是设置一下。至于设多少,要看并发量。
e. 每次更新,都要检测一次冲突
f. 冲突了,要睡一阵子再重试,避开冲突。怎么设置这个值,我忽然想起计网的拥塞控制,说笑的~
顺手作了个小实验,仍是执行100次,冲突睡眠100ms,
总结一下:
乐观锁更适合并发竞争少的状况,最好隔那么3-5分钟才有一次冲突。当并发量为10时就能明显感受乐观锁更慢;
上面只是一读一写。考虑若是一个事务中有3个写,若是每次写都是九死一辈子,事务提交比小蝌蚪找妈妈还难,这时就更要考虑是否是要用乐观锁了。
可是,当分布式数据库规模大到必定程度后,又另说了。基于悲观锁的分布式锁在集群大到必定程度后(从几百台扩展到几千台时),性能开销就打得没法接受。因此目前的趋势是大规模的分布式数据库更倾向于用乐观锁来达成external consistency。
若是对乐观锁和悲观锁的选择还不清楚,看这篇
五 待更
mvcc:http://blog.csdn.net/chen77716/article/details/6742128
意向锁,间隙锁,加锁的查看
参考:
数据库锁协议等原理:http://blog.csdn.net/gklifg/article/details/38752691
InnoDB的几种锁:http://www.cnblogs.com/chenqionghe/p/4845693.html
InnoDB的几种锁:http://blog.csdn.net/xifeijian/article/details/20313977