锁概念 : 当高并发访问同一个资源时,可能会导致数据不一致,需要一种机制将用户访问数据的顺序进行规范化,以保证数据库数据的一致性。锁就是其中的一种机制。
举例 :以买火车票为例,火车票可面向广大消费者,每位用户都可以查询余票数量、购买火车票 ... 但最终购票成功的仅有一位用户,处于购票高峰期时会出现很多用户同时抢夺同一张票的现状,为了避免出现一张火车票被多个用户购买成功的情况,当第一位用户进入购票流程时,就将数据库锁定,让别的用户无法修改,只有当第一位用户购票成功或取消购票之后,才会解除数据库的锁定,此时别的用户就可继续进行操作。这样就保证了一张火车票只能被一个用户购买。
悲观锁: 一般代指数据库锁机制,类似于我们在多线程资源竞争时添加的互斥锁,较容易出现死锁现象。它对于数据被外界修改持保守态度,认为数据随时会修改,所以整个数据处理中需要将数据加锁。悲观锁一般都是依靠关系数据库提供的锁机制实现。
悲观锁按使用性质划分:
数据库的操作可归纳为两种:读和写。当多个事务同时读取一个对象时,不会产生有冲突。但同时读和写,或者同时写会产生冲突。因此为提高数据库的并发性能,定义三种锁
共享锁(Share locks简记为S锁):也称读锁,事务A给对象T加S锁,其他事务也只能对T加S,多个事务可以同时读,但不能有写操作,直到A释放S锁。
地球语言 : 仅对数据进行读操作,因此多个事务可以同时为一个对象添加共享锁。(火车票人人都可随时查询)。
产生共享锁: select * from ad_plan lock in share mode;
排它锁(Exclusivelocks简记为X锁):也称写锁,事务A给对象T加X锁以后,其他事务不能对T加任何锁,只有事务A可以读写对象T,直到A释放X锁。
地球语言: 对数据仅需写/读写操作,只有一个事务可以为当前对象添加排他锁,其余事务不可再为其上锁。(一个用户已经进入购票流程,其余客户不能再购票)
产生排他锁: select * from ad_plan for update;
更新锁(简记为U锁):用来预定要对此对象施加X锁,它允许其他事务读,但不允许再施加U锁或X锁;当被读取的对象将要被更新时,则升级为X锁,主要是用来防止死锁的。因为使用共享锁时,修改数据的操作分为两步,首先获得一个共享锁,读取数据,然后将共享锁升级为排它锁,然后再执行修改操作。这样如果同时有两个或多个事务同时对一个对象申请了共享锁,在修改数据的时候,这些事务都要将共享锁升级为排它锁。这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。如果一个数据在修改前直接申请更新锁,在数据修改的时候再升级为排它锁,就可以避免死锁。
select * from information_schema.innodb_locks; 可以查看锁。
悲观锁按作用范围划分:
行锁:锁的作用范围是行级别,数据库能够确定那些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。举个栗子,现有一张学生表student,有主键id和学生名字name,假设现在需要使用 update ... where id=xxx 语句修改数据库数据,因为主键字段id在创建时已经默认建立了索引,所以数据库能够明确知道你需要修改的是哪一条记录,此时仅会使用行锁。但当使用 update ... where name=xxx 语句修改数据库数据时,数据库实现并不知道会影响哪些行,此时可能会使用表锁。
表锁:表锁的作用范围是整张表。
数据库死锁:通常如果需要“修改”一条数据,数据库管理系统会先在上面加锁,以保证在同一时间只有一个事务能进行修改操作。锁定(Locking)发生在一个事务获取到某一资源的“锁”时,其他的事务就不能更改这个资源了,这种机制的存在是为了保证数据一致性。多数情况下,可以认为如果一个资源被锁定,它总会在以后某个时间被释放。而死锁发生在当多个进程访问同一数据库时,其中每个进程拥有的锁都是其他进程所需的,由此造成每个进程都无法继续下去。简单的说,进程A等待进程B释放他的资源,B又等待A释放他的资源,这样就互相等待就形成死锁。
死锁产生条件:
- 按同一顺序访问对象:如果所有并发事务按同一顺序访问对象,则发生死锁的可能性会降低。例如,如果两个并发事务获得 Supplier 表上的锁,然后获得 Part 表上的锁,则在其中一个事务完成之前,另一个事务被阻塞在 Supplier 表上。第一个事务提交或回滚后,第二个事务继续进行。不发生死锁。将存储过程用于所有的数据修改可以标准化访问对象的顺序。
- 避免事务中的用户交互:避免编写包含用户交互的事务,因为运行没有用户交互的批处理的速度要远远快于用户手动响应查询的速度,例如答复应用程序请求参数的提示。例如,如果事务正在等待用户输入,而用户去吃午餐了或者甚至回家过周末了,则用户将此事务挂起使之不能完成。这样将降低系统的吞吐量,因为事务持有的任何锁只有在事务提交或回滚时才会释放。即使不出现死锁的情况,访问同一资源的其它事务也会被阻塞,等待该事务完成。
- 保持事务简短并在一个批处理中:在同一数据库中并发执行多个需要长时间运行的事务时通常发生死锁。事务运行时间越长,其持有排它锁或更新锁的时间也就越长,从而堵塞了其它活动并可能导致死锁。保持事务在一个批处理中,可以最小化事务的网络通信往返量,减少完成事务可能的延迟并释放锁。
- 使用低隔离级别:确定事务是否能在更低的隔离级别上运行。执行提交读允许事务读取另一个事务已读取(未修改)的数据,而不必等待第一个事务完成。使用较低的隔离级别(例如提交读)而不使用较高的隔离级别(例如可串行读)可以缩短持有共享锁的时间,从而降低了锁定争夺。
- 使用绑定连接:使用绑定连接使同一应用程序所打开的两个或多个连接可以相互合作。次级连接所获得的任何锁可以象由主连接获得的锁那样持有,反之亦然,因此不会相互阻塞。
数据库具体实现:
- 使用事务时,尽量缩短事务的逻辑处理过程,及早提交或回滚事务;
- 设置死锁超时参数为合理范围,如:3分钟-10分种;超过时间,自动放弃本次操作,避免进程悬挂;
- 所有的SP都要有错误处理(通过@error)
- 一般不要修改SQL SERVER事务的默认级别。不推荐强行加锁
- 优化程序,检查并避免死锁现象出现;
- 合理安排表访问顺序
- 在事务中尽量避免用户干预,尽量使一个事务处理的任务少些。
- 采用脏读技术。脏读由于不对被访问的表加锁,而避免了锁冲突。在客户机/服务器应用环境中,有些事务往往不允许读脏数据,但在特定的条件下,我们可以用脏读。
- 数据访问时域离散法。数据访问时域离散法是指在客户机/服务器结构中,采取各种控制手段控制对数据库或数据库中的对象访问时间段。主要通过以下方式实现: 合理安排后台事务的执行时间,采用工作流对后台事务进行统一管理。工作流在管理任务时,一方面限制同一类任务的线程数(往往限制为1个),防止资源过多占用; 另一方面合理安排不同任务执行时序、时间,尽量避免多个后台任务同时执行,另外,避免在前台交易高峰时间运行后台任务
- 数据存储空间离散法。数据存储空间离散法是指采取各种手段,将逻辑上在一个表中的数据分散到若干离散的空间上去,以便改善对表的访问性能。主要通过以下方法实现: 第一,将大表按行或列分解为若干小表; 第二,按不同的用户群分解。
- 使用尽可能低的隔离性级别。隔离性级别是指为保证数据库数据的完整性和一致性而使多用户事务隔离的程度,SQL92定义了4种隔离性级别:未提交读、提交读、可重复读和可串行。如果选择过高的隔离性级别,如可串行,虽然系统可以因实现更好隔离性而更大程度上保证数据的完整性和一致性,但各事务间冲突而死锁的机会大大增加,大大影响了系统性能。
- 使用Bound Connections。Bound connections 允许两个或多个事务连接共享事务和锁,而且任何一个事务连接要申请锁如同另外一个事务要申请锁一样,因此可以允许这些事务共享数据而不会有加锁的冲突。
- 考虑使用乐观锁定或使事务首先获得一个独占锁定。
乐观锁:一般是指用户自己实现的一种锁机制,并不是真实存在的锁。它对于数据被外界修改持乐观态度,认为数据不会修改,所以数据处理时数据库不再为其加锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。可以在数据表中添加一个冗余字段,比如时间戳,在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。比如更新数据时,拿着之前相同的查询条件再一次查询数据库,若仍能够得到数据证明此条记录无人修改,即可继续操作,否则表示当前有用户正在抢夺资源,就放弃更新操作。
乐观锁实现方式:
a. 版本号(记为version):在表中新增一个version字段,作为版本标识的记号,当数据每次更新时就将此字段加1,每次读取数据时一并将version字段读出,更新数据之前比较version字段值。举个栗子,若此次读取的 新version值 比 旧version值 大,说明有其他事务在此之前修改过这条记录,并为版本号字段增加了数量,此时就无法得到这条记录,需要重新开始一遍。此字段存在的意义是作为一个标志位,准备修改数据时将version读出,真正修改数据前再查询一次version,比较上一次得到的version值和现在version是否一致,相同继续操作,不同重新开始。可使用类似 update … where … and version=”old version” 语句进行比较。根据返回结果是否为0执行下一步的操作。
b. 时间戳(timestamp):和版本号基本一样,只是通过时间戳来判断而已,注意时间戳要使用数据库服务器的时间戳,而不能是业务系统的时间。
c. 待更新字段:和版本号方式相似,只是不增加额外字段,直接使用表中现有做版本控制信息的标志位,因为有时我们可能无法改变旧系统的数据库表结构。假设现在需要保存一个订单记录,有库存stock字段: 首先需要查询数据库,得到这个商品的库存数量,再判断库存数量是否大于用户购买数量,经历一系列判断逻辑都能够通过的话,保存这个订单数据之前,需要拿着当初查询数据库时的库存字段再查询一次这个商品,若通过原始库存值能够得到商品对象,那么就进行订单的修改操作,否则就是别的用户正在抢夺资源,应放弃操作重新再来。
d. 所有字段:和待更新字段类似,只是使用所有字段做版本控制信息,只有所有字段都没有变化才会执行更新。
锁的级别:页级、表级、行级。
MySQL的锁机制比较简单,最显著的特点是不同的存储引擎支持不同的锁机制。
MyISAM & MEMORY: 表级锁(table-level locking), BDB: 页面锁(page-level locking)&表级锁, InnoDB: 行级锁(row-level locking)&表级锁,默认采用行级锁。
3种锁特性:
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。对整个表加锁,影响标准的所有记录。通常用在DDL语句中,如DELETE TABLE,ALTER TABLE等。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。对一行记录加锁,只影响一条记录。通常用在DML语句中,如INSERT, UPDATE, DELETE等。
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
数据库引擎通常必须获取多级别上的锁才能完整地保护资源。
锁的应用场景:
乐观锁适用于高并发、读多写少的场景,发生冲突时能够减少开销.
如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁
如果冲突频率非常高,建议采用悲观锁保证成功率,如果冲突频率大,乐观锁会需要多次重试才能成功,代价比较大
如果重试代价大,建议采用悲观锁
锁的优缺点:
乐观锁不会发生死锁情况,不会过多占用系统资源,无法阻止除数据库之外的操作
悲观锁写入数据时能够确保数据的准确性
来源:https://www.cnblogs.com/xiaozengzeng/p/10847252.html