乐观锁和悲观锁
单机环境里的概念
-
乐观锁
认为一个用户读数据的时候,别人不会去写自己所读的数据(本质上是不加锁的)
-
乐观锁,基于数据版本或时间戳机制实现,(比如为表加一个timestamp字段),提前读取,事后对比
-
优点是避免了长事务中的数据库加锁开销
-
缺点是系统级实现,对外部系统无法控制。如下方法可改进:
- 将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开
-
-
悲观锁
认为并发更新冲突一定会发生,所以不管冲突是否真的发生,都会使用锁机制
- 悲观锁的实现,往往依靠数据库自身的锁机制
- 缺点是系统开销大
锁的粒度
-
行级锁
- 是一种排他锁,防止其他事务修改此行
- SELECT … FOR UPDATE语句允许用户一次锁定多条记录进行更新;
- MySQL在使用SELECT … FOR UPDATE语句时也会加行级锁,前提是能够应用到索引
- 使用COMMIT或ROLLBACK语句释放锁
- 在使用以下语句时,Oracle会自动为应用加行级锁:
INSERT、UPDATE、DELETE、SELECT … FOR UPDATE [OF columns] [WAIT n | NOWAIT];
-
表级锁,就是给表加锁,都是悲观锁
-
页级锁,按数据的分页加锁,比表加锁的范围小
-
块级锁,按查询条件给数据块加锁
锁模式
-
共享锁(Shared lock)
- 事务读的时候加锁
- 多个读可重复加锁
- 有共享锁不可以加排它锁
- 可升级为更新锁
-
更新锁(Update lock)
- 更新锁意味着在做一个更新时,一个共享锁在扫描完成符合条件的数据后可能会转化成排他锁
- 更新锁一定是由共享锁升级来的
- 更新锁本质上就是排它锁
- 加了更新锁不能再加其它的锁
- 更新锁的存在是为了解决共享锁和排它锁造成的死锁问题
-
排他锁(Exclusive Locks)
- 其它事务既不能读,又不能改它锁定的资源
- 加了排它锁不能再加任何锁
-
意向锁(Intent Locks)
- 用来解决行级锁和表级锁的加锁冲突
- 当一个表中的某一行被加上排他锁后,整张表会加上意向锁,该表就不能再被加表锁
-
计划锁(Schema Locks)
- 向DDL这种修改表结构的语句会被加上计划锁
- 加了计划锁,表不允许再加其它的锁
分布式锁
-
Zookeeper实现
基于zookeeper瞬时有序节点实现的分布式锁- 优点: 锁安全性高,zk可持久化,且能实时监听获取锁的客户端状态。一旦客户端宕机,则瞬时节点随之消失,zk因而能第一时间释放锁。这也省去了用分布式缓存实现锁的过程中需要加入超时时间判断的这一逻辑
- 缺点:性能开销比较高。因为其需要动态产生、销毁瞬时节点来实现锁功能。所以不太适合直接提供给高并发的场景使用
-
memcached实现
利用add函数的特性即可实现分布式锁,add会添加第一个到达的值,并返回true,后续的添加则都会返回false- 优点: 并发高效
- 缺点:
- memcached采用LRU置换策略,所以如果内存不够,可能导致缓存中的锁信息丢失 (LRU是Least Recently Used 近期最少使用算法)
- memcached无法持久化,一旦重启,将导致信息丢失
-
redis实现
实现方式和memcached类似,采用setnx即可实现,redis也需要设置超时时间,以避免死锁。可以利用jedis客户端实现-
setnx函数
- 根据 key 的值设置 value值,当且仅当 key 不存在
- 若给定的 key 已经存在,则 SETNX 不做任何动作
- 如果 SETNX 返回1,说明该进程获得锁;如果 SETNX 返回0,说明其他进程已经获得了锁
-
getset函数
- 根据 key 的值设置 value值,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil
-
redis分布式锁原理
- setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2
- get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3
- 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime
- 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试
- 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
-
优点: 据有zookeeper的安全度,和memcached的高效率
-