锁是计算机协调多个进程或线程并发访问某一资源的机制,在数据库中,除传统的计算资源(CPU、RAM、I/O)争用外,数据也是一种供许多用户共享的资源,如何保证数据并发访问的一致性,有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素,从这个角度来说,锁对数据库而言是尤其重要,也更加复杂。MySQL中的锁,按照锁的粒度分为:1、全局锁,就锁定数据库中的所有表。2、表级锁,每次 *** 作锁住整张表。3、行级锁,每次 *** 作锁住对应的行数据。
全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,已经更新 *** 作的事务提交语句都将阻塞。其典型的使用场景就是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。但是对数据库加全局锁是有弊端的,如在主库上备份,那么在备份期间都不能执行更新,业务会受影响,第二如果是在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志,会导致主从延迟。
解决办法是在innodb引擎中,备份时加上--single-transaction参数来完成不加锁的一致性数据备份。
添加全局锁: flush tables with read lock解锁 unlock tables。
表级锁,每次 *** 作会锁住整张表.锁定粒度大,发送锁冲突的概率最高,并发读最低,应用在myisam、innodb、BOB等存储引擎中。表级锁分为: 表锁、元数据锁(meta data lock, MDL)和意向锁。
表锁又分为: 表共享读锁 read lock、表独占写锁write lock
语法: 1、加锁 lock tables 表名 ... read/write
2、释放锁 unlock tables 或者关闭客户端连接
注意: 读锁不会阻塞其它客户端的读,但是会阻塞其它客户端的写,写锁既会阻塞其它客户端的读,又会阻塞其它客户端的写。大家可以拿一张表来测试看看。
元数据锁,在加锁过程中是系统自动控制的,无需显示使用,在访问一张表的时候会自动加上,MDL锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入 *** 作。为了避免DML和DDL冲突,保证读写的正确性。
在MySQL5.5中引入了MDL,当对一张表进行增删改查的时候,加MDL读锁(共享);当对表结构进行变更 *** 作时,加MDL写锁(排他).
查看元数据锁:
select object_type,object_schema,object_name,lock_type,lock_duration from performance_schema_metadata_locks
意向锁,为了避免DML在执行时,加的行锁与表锁的冲突,在innodb中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查。意向锁分为,意向共享锁is由语句select ... lock in share mode添加。意向排他锁ix,由insert,update,delete,select。。。for update 添加。
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_lock
行级锁,每次 *** 作锁住对应的行数据,锁定粒度最小,发生锁冲突的概率最高,并发读最高,应用在innodb存储引擎中。
innodb的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁,对于行级锁,主要分为以下三类:
1、行锁或者叫record lock记录锁,锁定单个行记录的锁,防止其他事物对次行进行update和delete *** 作,在RC,RR隔离级别下都支持。
2、间隙锁Gap lock,锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事物在这个间隙进行insert *** 作,产生幻读,在RR隔离级别下都支持。
3、临键锁Next-key-lock,行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap,在RR隔离级别下支持。
innodb实现了以下两种类型的行锁
1、共享锁 S: 允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
2、排他锁 X: 允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁。
insert 语句 排他锁 自动添加的
update语句 排他锁 自动添加
delete 语句 排他锁 自动添加
select 正常查询语句 不加锁 。。。
select 。。。lock in share mode 共享锁 需要手动在select 之后加lock in share mode
select 。。。for update 排他锁 需要手动在select之后添加for update
默认情况下,innodb在repeatable read事务隔离级别运行,innodb使用next-key锁进行搜索和索引扫描,以防止幻读。
间隙锁唯一目的是防止其它事务插入间隙,间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用的间隙锁。
MySQL 中有哪些锁?数据库中锁的设计初衷处理并发问题,作为多用户共享资源,当出现并发访问的时候,数据库需要合理控制资源访问规则。锁就是实现这些访问规则中的重要数据。
锁的分类根据加锁范围,MySQL 里面的锁可以分成 全局锁 、 表级锁 、 行锁 三类。
全局锁全局锁,就是对整个数据库实例加锁,MySQL 提供了一个加全局读锁的方法,命令是:
Flush tables with read lock (FTWRL)当需要整个库只读状态的时候,可以使用这个命令,之后其他线程的:数据更新语句(增删改),数据定义语句(建表,修改表结构)和更新事务的提交语句将会被阻塞。
全局锁的使用场景全局锁的定型使用场景,做 全库逻辑备份 。也就是把整个库每个表都 Select 出来,然后存成文本。
如何整个库都只读,会有什么问题? 如果你在主库上备份,那么在备份期间都不能执行更想,业务就基本上停摆。 如果在从库上备份,那么备份期间从库不能执行主库同步过来的 binlog ,会导致从延迟。 既然要全库只读, 为什么不使用set global readonly=true的方式呢?readonly 方式也可以让全库进入只读状态,但我还是会建议你用FTWRL方式, 主要有两个原因:
一是, 在有些系统中, readonly的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改global变量的方式影响面更大, 我不建议你使用。 二是, 在异常处理机制上有差异。如果执行FTWRL命令之后由于客户端发生异常断开, 那么MySQL会自动释放这个全局锁, 整个库回到可以正常更新的状态。而将整个库设置为readonly之后, 如果客户端发生异常, 则数据库就会一直保持readonly状态, 这样会导致整个库长时间处于不可写状态, 风险较高 表级别锁MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lok, MDL)。表锁的语法是 :
lock tables ... read/write与 FTWRL 类似,可以使用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。需要注意的是,lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的 *** 作对象。
MDL 表级锁MDL 不需要显示使用,在访问一个表的时候自动加上, MDL 保证读写的正确性,也就是说在查询数据时,不允许有其他线程对这个表结构做变更。
什么 *** 作会加 MDL 锁?在MySQL 5.5版本中引入了MDL, 当对一个表做增删改查 *** 作的时候,加 MDL读锁 ;当要对表做结构变更 *** 作的时候,加 MDL写锁 。
读锁之间不互斥,因此可以有多个线程同时对一张表增删改查。 读写之间、写锁之间是互斥的,用来保证变更表结构 *** 作的安全性,如果有两个线程要同时给一个表加字段,其中一个要等另外一个执行完才能执行。 更改表结构要注意哪些?给一个表加字段, 或者修改字段, 或者加索引, 需要扫描全表的数据。在对大表 *** 作的时候, 你肯定会特别小心, 以免对线上服务造成影响。而实际上, 即使是小表, *** 作不慎也会出问题,导致整个库的线程爆满。
举个例子我们来看一下下面的 *** 作序列, 假设表t是一个小表。
image
session A先启动, 这时候会对表t加一个 MDL读锁 。由于session B需要的也是 MDL读锁 , 因此可以正常执行。 session C会被blocked, 是因为session A的MDL读锁还没有释放, 而session C需要MDL写锁, 因此只能被阻塞,读写锁互斥。 如果只有session C自己被阻塞还没什么关系, 但是之后所有要在表t上新申请MDL读锁的请求也会被session C阻塞。前面我们说了,所有对表的增删改查 *** 作都需要先申请MDL读锁, 就都被锁住, 等于这个表现在完全不可读写了。如果某个表上的查询语句频繁, 而且客户端有重试机制,也就是说超时后会再起一个新session 再请求的话, 这个 库的线程很快就会爆满 。事务中的MDL锁, 在语句执行开始时申请, 但是语句结束后并不会马上释放, 而会等到整个事务提交后再释放。
怎么解决这个 更改表结构问题比较理想的机制是, 在alter table语句里面设定等待时间, 如果在这个指定的等待时间里面能够拿到MDL写锁最好, 拿不到也不要阻塞后面的业务语句, 先放弃。
ALTER TABLE tbl_name NOWAIT add column ... ALTER TABLE tbl_name WAIT N add column ...以前参加过一个库存系统,由于其业务复杂性,搞了很多个应用来支撑。这样的话一份库存数据就有可能同时有多个应用来修改库存数据。
比如说,有定时任务域xx.cron,和SystemA域和SystemB域这几个JAVA应用,可能同时修改同一份库存数据。如果不做协调的话,就会有脏数据出现。
对于跨JAVA进程的线程协调,可以借助外部环境,例如DB或者Redis。下文介绍一下如何使用DB来实现分布式锁。
本文设计的分布式锁的交互方式如下:
在使用synchronized关键字的时候,必须指定一个锁对象。
进程内的线程可以基于obj来实现同步。obj在这里可以理解为一个锁对象。如果线程要进入synchronized代码块里,必须先持有obj对象上的锁。这种锁是JAVA里面的内置锁,创建的过程是线程安全的。那么借助DB,如何保证创建锁的过程是线程安全的呢?
可以利用DB中的UNIQUE KEY特性,一旦出现了重复的key,由于UNIQUE KEY的唯一性,会抛出异常的。在JAVA里面,是 SQLIntegrityConstraintViolationException 异常。
transaction_id是事务Id,比如说,可以用
来组装一个transaction_id,表示某仓库某销售模式下的某个条码资源。不同条码,当然就有不同的transaction_id。如果有两个应用,拿着相同的transaction_id来创建锁资源的时候,只能有一个应用创建成功。
在写 *** 作频繁的业务系统中,通常会进行分库,以降低单数据库写入的压力,并提高写 *** 作的吞吐量。如果使用了分库,那么业务数据自然也都分配到各个数据库上了。
在这种水平切分的多数据库上使用DB分布式锁,可以自定义一个DataSouce列表。并暴露一个 getConnection(String transactionId) 方法,按照transactionId找到对应的Connection。
实现代码如下:
首先编写一个initDataSourceList方法,并利用Spring的PostConstruct注解初始化一个DataSource 列表。相关的DB配置从db.properties读取。
DataSource使用阿里的DruidDataSource。
接着最重要的一个实现getConnection(String transactionId)方法。实现原理很简单,获取transactionId的hashcode,并对DataSource的长度取模即可。
连接池列表设计好后,就可以实现往distributed_lock表插入数据了。
接下来利用DB的 select for update 特性来锁住线程。当多个线程根据相同的transactionId并发同时 *** 作 select for update 的时候,只有一个线程能成功,其他线程都block住,直到 select for update 成功的线程使用commit *** 作后,block住的所有线程的其中一个线程才能开始干活。
我们在上面的DistributedLock类中创建一个lock方法。
当线程执行完任务后,必须手动的执行解锁 *** 作,之前被锁住的线程才能继续干活。在我们上面的实现中,其实就是获取到当时 select for update 成功的线程对应的Connection,并实行commit *** 作即可。
那么如何获取到呢?我们可以利用ThreadLocal。首先在DistributedLock类中定义
每次调用lock方法的时候,把Connection放置到ThreadLocal里面。我们修改lock方法。
这样子,当获取到Connection后,将其设置到ThreadLocal中,如果lock方法出现异常,则将其从ThreadLocal中移除掉。
有了这几步后,我们可以来实现解锁 *** 作了。我们在DistributedLock添加一个unlock方法。
毕竟是利用DB来实现分布式锁,对DB还是造成一定的压力。当时考虑使用DB做分布式的一个重要原因是,我们的应用是后端应用,平时流量不大的,反而关键的是要保证库存数据的正确性。对于像前端库存系统,比如添加购物车占用库存等 *** 作,最好别使用DB来实现分布式锁了。
如果想锁住多份数据该怎么实现?比如说,某个库存 *** 作,既要修改物理库存,又要修改虚拟库存,想锁住物理库存的同时,又锁住虚拟库存。其实也不是很难,参考lock方法,写一个multiLock方法,提供多个transactionId的入参,for循环处理就可以了。这个后续有时间再补上。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)