锁&事务
一、概述:
锁:是计算机协调多个进程或线程并发访问某一资源的机制,数据库中最重要的资源。数据库既要保证并发性,又要保证数据的一致性,所以锁机制也更复杂。在计算机科学中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策略。
事务四特性:原子性、一致性、隔离性、持久性。如果没有事务的隔离级别,那么并发事务操作数据库时可能会产生更新丢失即两个并发事务更新同一条数据时最后更新的会覆盖前面的更新,脏读即在一个事务中读取了别的事务还未提交的更新,不可重复读即在一个事务中前后两次读取到的数据不一致,主要针对update操作,且查询条件是=,幻读即一个事务按照相同的查询条件,前后两次读到数据条数不一样,主要针对insert操作,且查询条件是一个范围。
分布式事务:事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。例如在大型电商系统中,下单接口通常会扣减库存、减去优惠、生成订单 id, 而订单服务与库存、优惠、订单 id 都是不同的服务,下单接口的成功与否,不仅取决于本地的 db 操作,而且依赖第三方系统的结果,这时候分布式事务就保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。分布式事务2pc,3pc及TCC、可靠消息最终一致性如阿里的rocketmq ,具体可参考https://xiaomi-info.github.io/2020/01/02/distributed-transaction/
幂等操作:在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,支付流程中第三方支付系统告知系统中某个订单支付成功,接收该支付回调接口在网络正常的情况下无论操作多少次都应该返回成功。
二、MySQL锁
1、粒度
- 表级锁:各存储引擎最大粒度的锁。因为粒度大,可以避免死锁的问题,但是资源争用的概率高,是并发粒度降低。
- 行级锁:各存储引擎最小粒度的锁。资源争用的概率低,并发粒度大,如存储引擎:InnoDB(通过锁引条件检索数据)
- 页级锁:介于表级锁和行级锁之间, 如存储引擎:DBD
2、锁定程度
- 共享锁:如果mysql读取某一行时给此条数据加了共享锁,那么其他事务也可以读取此条数据或者给此条数据加共享锁。但是其他事务不能为此条事务加排他锁。简单的可以理解共享锁为读锁,加锁方式:select … from tablename lock in share mode。共享所可能会产生死锁。
- 排他锁:如果mysql事务为某一行数据加了排他锁,那么其他事务不能再为这条数据加排他锁或者共享锁,简单可以理解排他锁为写锁,加锁方式:select … from tablename for update。Mysql会自动的给insert update delete操作加排他锁(隐式加锁)
3、锁定方式
- 显示锁定:手动为某一行或者某几行或者某张表加锁
- 隐式锁定:不需要手动参与,mysql存储引擎会在执行的时候自动为相应的数据进行加锁
4、提交
- 自动提交:autocommit=1, 每条sql语句都是一个事务,如果session设置为自动提交,则事务的开始、提交或者回滚由mysql来保证。每次更新操作之后mysql自动提交事务。一个事务提交之后就不会对其他事务造成干扰.
-
非自动提交:autocommit=0,或用begin开启事务,或者用start transaction开启事务
5、死锁检测
- 当产生死锁的时候,mysql会判断两个事务的大小,决定哪个事务该回滚,Innodb发现死锁之后,会计算出两个事务各自插入、更新或者删除的数据量来判断两个事务的大小。也就是说哪个事务所改变的记录条数越多,在死锁中就越不会被回滚掉。
三、分布式锁
分布式锁:在分布式系统的多进程中,用分布式锁控制多个进程对资源的访问。使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信。加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。
线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如Synchronized、Lock等。
进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
乐观锁:乐观锁机制采取了更加宽松的加锁机制,提交更新的时候才检查是否冲突
悲观锁:具有强烈的独占和排他特性。
分布式锁特点:
- 互斥性: 同一时刻只能有一个进程持有锁
- 可重入性: 同一节点上的同一个进程如果获取了锁,能够再次获取锁
- 锁超时:支持锁超时,防止死锁
- 高效和高可用: 加锁和解锁需要高效,同时也需要保证高可用,防止分布式锁失效
- 具备阻塞和非阻塞性:和ReentrantLock一样支持lock和trylock以及tryLock(long timeOut)
- 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。
常见的分布式锁:
- 基于数据库的分布式锁:
- 基于数据库乐观锁的分布式锁采用的是多版本并发控制机制,资源竞争不是很强的情况下,可以使用数据库乐观锁版本号。
- 基于数据库表的分布式锁-基于唯一键:创建一张表,当我们要锁住某个资源的时候,就往表里面增加一条数据,并对资源名字段做唯一性约束。
- 基于数据库悲观锁的分布式锁-基于数据库排他锁,优点是不需要维护额外的第三方中间件如redis、ZK,缺点是实现起来比较繁琐,需要自己考虑锁冲入、锁超时、加事务等等,一般对比缓存来说性能较低,对于高并发场景并不是很适合。
- 基于Redis的分布式锁:
- 加锁:SETNX key value,当键不存在时,对键进行设置操作并返回成功,否则返回失败。KEY 是锁的唯一标识,一般按业务来决定命名。
- 解锁:DEL key,通过删除键值对释放锁,以便其他线程可以通过 SETNX 命令来获取锁。
- 锁超时:EXPIRE key timeout, 设置 key 的超时时间,以保证即使锁没有被显式释放,锁也可以在一定时间后自动释放,避免资源被永远锁住。
- 基于Zookeeper的分布式锁(参考:https://www.imooc.com/article/284956?block_id=tuijian_wz)
- Zookeeper(业界简称zk):是一种提供配置管理、分布式协同以及命名的中心化服务,这些提供的功能都是分布式系统中非常底层且必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。因此zookeeper提供了这些功能,开发者在zookeeper之上构建自己的各种分布式系统。
- ZK节点特性:(1)有序节点:假如当前有一个父节点为/test_lock,我们可以在这个父节点下面创建子节点;zookeeper提供了一个可选的有序特性;(2)临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点。(3)事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:1)节点创建;2)节点删除;3)节点数据修改;4)子节点变更。
- 优缺点:优点是ZK 可以不需要关心锁超时时间,实现起来有现成的第三方包,比较方便,并且支持读写锁,ZK 获取锁会按照加锁的顺序,所以其是公平锁。对于高可用利用 ZK 集群进行保证。缺点是ZK 需要额外维护,增加维护成本,性能和 MySQL 相差不大,依然比较差。
锁类型 |
优点 |
缺点 |
适用场景 |
基于数据库 |
理解起来简单,不需要维护额外的第三方中间件(比如 Redis,ZK)。 |
虽然容易理解但是实现起来较为繁琐,需要自己考虑锁超时,加事务等等。性能局限于数据库,一般对比缓存来说性能较低。对于高并发的场景并不是很适合。 |
MySQL 分布式锁一般适用于资源不存在数据库,如果数据库存在比如订单,可以直接对这条数据加行锁,不需要我们上面多的繁琐的步骤。 |
基于Redis |
Redis的话实现比较简单,同时性能很好,引入集群可以提高可用性。同时定期失效的机制可以解决因网络抖动锁删除失败的问题。 |
需要维护 Redis集群,如果要实现 RedLock需要维护更多的集群。 |
性能最好,适合高并发场景 |
基于Zookeeper |
ZK 可以不需要关心锁超时时间,实现起来有现成的第三方包,比较方便,并且支持读写锁,ZK 获取锁会按照加锁的顺序,所以其是公平锁。对于高可用利用 ZK 集群进行保证。 |
ZK 需要额外维护,增加维护成本,性能和 MySQL 相差不大,依然比较差。 |
主要是用来解决分布式应用中经常遇到的一些数据管理问题:数据发布与订阅、分布式应用配置项、统一命名服务等等
|