mysql读写分离主从原理、事务隔离级别及使用、锁表和锁行场景、乐观锁和悲观锁、lock锁和sychronized区别及使用自己学习之后总结和参考一些博客感觉系统了解了

synchronized与Lock的区别

  • 两者区别:
  • 1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  • 2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • 3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • 4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  • 5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
  • 6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

 

一.synchronized的缺陷

  •   synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?
  •   在上面一篇文章中,我们了解到如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
  •   1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  •   2)线程执行发生异常,此时JVM会让线程自动释放锁。
  •   那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
  •   因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
  •   再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
  •   但是采用synchronized关键字来实现同步的话,就会导致一个问题:
  •   如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
  •   因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
  •   另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
  •   总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:
  •   1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
  •   2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

lock锁:

//尝试获取锁,获取成功则返回,否则阻塞当前线程
void lock(); 

//尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常 
void lockInterruptibly() throws InterruptedException; 

//尝试获取锁,获取锁成功则返回true,否则返回false 
boolean tryLock(); 

//尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常 
boolean tryLock(long time, TimeUnit unit) 
                                   throws InterruptedException; 

//释放锁
void unlock(); 

//返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition();

由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

mysql事务隔离原理:

在MySQL的众多存储引擎中,只有InnoDB支持事务,所有这里说的事务隔离级别指的是InnoDB下的事务隔离级别。

  1. 读未提交:一个事务可以读取到另一个事务未提交的修改。这会带来脏读、幻读、不可重复读问题。(基本没用)
  2. 读已提交:一个事务只能读取另一个事务已经提交的修改。其避免了脏读,但仍然存在不可重复读和幻读问题。
  3. 可重复读:同一个事务中多次读取相同的数据返回的结果是一样的。其避免了脏读和不可重复读问题,但幻读依然存在。
  4. 串行化:事务串行执行。避免了以上所有问题。

以上是SQL-92标准中定义的四种隔离级别。在MySQL中,默认的隔离级别是REPEATABLE-READ(可重复读),并且解决了幻读问题。简单的来说,mysql的默认隔离级别解决了脏读、幻读、不可重复读问题。

不可重复读重点在于update和delete,而幻读的重点在于insert。

MySQL默认隔离级别

  1.  MySQL/InnoDB默认是可重复读的(REPEATABLE READ);
  2. Oracle默认隔离级别是读已提交(READ_COMMITTED);

修改与查询MySQL事务隔离级别的方法:

复制代码

 1 #查全局事务隔离级别
 2 SELECT @@global.tx_isolation;
 3 #查当前会话事务隔离级别
 4 SELECT @@session.tx_isolation; 
 5 #查当前事务隔离级别
 6 SELECT @@tx_isolation;
 7 #设置全局隔离级别
 8 set global transaction isolation level read committed;
 9 #设置当前会话隔离级别
10 set session transaction isolation level read committed;

MySQL表级锁的锁模式(MyISAM)

  • MySQL表级锁有两种模式:表共享锁(Table Read Lock)和表独占写锁(Table Write Lock)。
  • 对MyISAM的读操作,不会阻塞其他用户对同一表请求,但会阻塞对同一表的写请求;
  • 对MyISAM的写操作,则会阻塞其他用户对同一表的读和写操作;
  • MyISAM表的读操作和写操作之间,以及写操作之间是串行的。
  • 当一个线程获得对一个表的写锁后,只有持有锁线程可以对表进行更新操作。其他线程的读、写操作都会等待,直到锁被释放为止。

如何加表锁

  •     MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。在本书的示例中,显式加锁基本上都是为了方便而已,并非必须如此。

InnoDB行锁实现方式

  •     InnoDB行锁是通过索引上的索引项来实现的,这一点MySQL与Oracle不同,后者是通过在数据中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味者:只有通过索引条件检索数据,InnoDB才会使用行级锁,否则,InnoDB将使用表锁!
  •     在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。

InnoDB实现了以下两种类型的行锁。

  • 共享锁(s):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
  • 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。

另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。

  • 意向共享锁(IS):事务打算给数据行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
  • 意向排他锁(IX):事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。

  如果一个事务请求的锁模式与当前的锁兼容,InnoDB就请求的锁授予该事务;反之,如果两者两者不兼容,该事务就要等待锁释放。

    意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及及数据集加排他锁(X);对于普通SELECT语句,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方式获取排他锁。

六、悲观锁和乐观锁

悲观锁(Pessimistic Lock), 每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

  1. 乐观锁(Optimistic Lock), 每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。
  2. 两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。与乐观锁相对应的就是悲观锁了。悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟java中的synchronized很相似,所以悲观锁需要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。
     

 mysql内建的复制功能是构建大型,高性能应用程序的基础,将mysql的数据分布到多个系统上去,这种分布的机制,是通过将mysql的某一台主机的数据复制到其他主机(slaves)上,并重新执行一遍来实现,复制过程中一个服务器充当主服务器,而一个或多个服务器充当从服务器。主服务器将更新写入 二进制日志文件,并维护文件的索引以跟踪日志循环。这些日志可以记录发送到从服务器的更新。当一个 从服务器链接主服务器时。它通知主服务器从服务器在日志中读取的最后一次成功更新的位置,从服务器接收从那时发生的任何更新,然后封锁并等待主服务器通知新的更新

   复制解决的问题:

 1、master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志时间,binary log events),

 2、slave将master的bianry log evens拷贝到它的中继日志(relay log)

3、slave重做中继日志中的事件,将改变反映它自己的数据

复制过程:

  1.      第一部分就是master记录二进制日志,在每个事务更新之前,master在日志记录这些改变,mysql将事务窜行的写入二进制日志中,即时事务语句是交叉执行,在事件写入二进制日志完成,master通知存储引擎提交事务,下一步就是slave从服务器拷贝master的日志到它自己的中继日志。slave开始一个工作线程,i/o线程。i/o线程在master打开一个普通的链接,然后开始一个binlog dump process。binlog dump process从master的二进制日志读取事件,如果已经跟上master,它会睡眠一会儿等待master产生新的事件,i/o线程将这个事件读取到slave它自己的中继日志。sql slave thread处理该过程最后一步。sql线程从中继入职读取事件,并重放其中事件更新slave的数据,使其与master中数据一致,只要该线程与i/o线程一致,中继日志通常会位于os的缓存中,所以中继日志开销很小。而且master还有一个工作线程,和其他mysql的链接,slave在master中打开一个连接也会使得master开始一个线程。复制过程有一个很重要限制---复制在slave上是串行化,也就是说在master上并行更新操作不能在slave上并行操作。

异步复制:

  1. 逻辑:mysql默认是即是异步的,主库在执行完成客户端提交的事务后会立即将结果返回给客户端,并不关心从库是否接收并处理,这样就会一个问题,主如果crash掉了,此时主上已经提交的事务可能并没有传到从库上处理,强行把从库提升主库,可能导致新主上的数据不完整。
  2. 技术:主库将事件biglog事件写入binlog日志完成,此时主库只会通知dump线程发送这些新biglog日志,然后主库继续处理提交操作,而此时不保证这些binlog传到任何一个从库当中,如果主库crash掉,此时强行将从库提升为主库,这时候会导致新主库数据缺失。

半同步复制:

  1.    逻辑上:
  2.    是介于全同步复制与异步复制之间的一种,主库只需要等待至少一个从库节点收到并且flush binlog到relay log文件即可,主库不需要等待所有从库给主库反馈,同时,这里只是一个收到的反馈,而不是已经完成并且提交的反馈,如此,节省了很多时间。
  3. 技术:
  4.   介于异步复制和全同步复制之间,主库在执行完客户端提交的事务后不是立刻 返回给客户端,而是等待至少一个从库接收到并且到relay log中才返回给客户端。相对于异步复制,半同步复制提高了数据的安全性,同时它也造成了一定程度的延迟,这个延迟,这个延迟最少是一个tcp/ip的往返的时间,所以,半同步复制最好在低延迟的网络中使用。

全同步复制:

  1.     逻辑:只当主库执行完一个事务,所有的从库都执行了该事务才 返回给客户端。因为需要等待所有从库执行完成该事务才能返回,所以从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。
  2.     技术:当主库提交事务之后,所有的从库节点必须收到,apply并且提交这些事务,然后主库线程才能继续做后续操作。但缺点是,主库完成一个事务的时间会被拉长,性能降低。

为什么要做主从复制?

  • 1、在业务复杂的系统中,有一个情景,有一句sql语句需要锁表,导致崭时不能使用读的服务,那么就很影响运行中的业务,使用主从复制,让主库负责写,从库负责读,这样,即时主库出现了锁表的情景,通过从库也可以保证业务的正常的运作 。
  • 2、做数据的热备(当主机出现crash掉就是主库宕机)
  • 3、架构的扩展,业务量越来越大,i/o访问频率过高,单机无法满足,此时做多库的存储,降低磁盘i/o访问的频率,提高单个机器的i/o性能

binlog:binary log ,主库中保存更新事件,日志的二进制文件。

  1.   主从复制的基础是主库记录数据库的所有变更记录到binlog,binlog是数据库中保存配置中过期时间内所有修改数据库结构或      内容的一个文件。如果过期时间是10d的话,那么就是最近10d的数据库修改记录。
  2.    mysql主从复制是一个异步的复制过程,主库发送更新事件到从库,从库读取更新记录,并执行更新记录,使得从库的内容与     主库保持一致。
  3.    在主库里,只要有更新事件出现,就会被依次写入binlog里面,是之后从库连接在主库时,从库拉取过来进行复制操作的数据     源

binlog输出线程:每当有从库连接到主库的时候,主库都会创建一个线程然后发送binlog内容到从库。

  1.    对于每一个即将发给从库的sql事件,binlog输出线程会将其锁住,一旦该事件被读取完之后,该锁会被释放,即使在该事件       完 成发送到从库的时候,该锁也会被释放。
  2.    在从库里,当复制开始的时候,从库就会创建两个线程进行处理;
  • 从库i/o线程:当start slave语句在从库开始执行之后,从库创建一个i/o线程,该线程连接到主库并请求主库发送binlog里面的更新记录到从库上,从库i/o线程读取主库的binlog输出线程发送的更新并拷贝这些更新的本地文件,其中包括relay log文件,其中包括relay文件。
  • 从库的sql线程:从库创建一个sql线程,这个线程读取从库i/o线程写到relay log的更新事件并执行。
  •   可以知道对于一个主从复制的连接,都有三个线程,用有多个从库的主库为每一个线程,用多个从库的主库为每一个连接主库的从库创建一个binlog输出线程,每一个从库都有它自己的i/o线程和sql线程。
  • 从库通过创建两个独立的线程,使得在进行复制时,从库的读和写进行了分离。因此,即使负责执行的线程运行较慢,负责读取更新语句的线程并不会因此便的缓慢,比如说,如果从库一段时间没运行了,当他再次启动时候,尽管它的sql线程执行比较慢,它的i/o线程可以快速地从主库里读取所有的binlog内容,这样一来,即使从库在sql线程执行所有读取到的语句前停止运行了,i/o线程也至少完全读取了所有的内容,并将其安全地备份在从库本地的relay  log,随时准备在从库下一次启动的时候执行语句。

参考一些博客:https://blog.csdn.net/xihuanyuye/article/details/81220524

                        https://www.cnblogs.com/baizhanshi/p/6419268.html

                        https://www.cnblogs.com/hoohack/p/7149234.html

猜你喜欢

转载自blog.csdn.net/qq_38014385/article/details/86526630