最近有很多小伙伴给我留言,分布式系统时代,线程并发,资源抢占,"锁" 慢慢变得很重要。那么常见的锁都有哪些?
今天就和大家简单聊聊这个话题
1、悲观锁
正如其名,它是指对数据修改时持保守态度,认为其他人也会修改数据。因此在操作数据时,会把数据锁住,直到操作完成。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。如果加锁的时间过长,其他用户长时间无法访问,影响程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是长事务而言,这样的开销往往无法承受。
如果是单机系统,我们可以采用 JAVA 自带的 synchronized 关键字,通过添加到方法或同步块上,锁住资源 如果是分布式系统,我们可以借助数据库自身的锁机制来实现
select * from 表名 where id= #{id} for update
使用悲观锁的时候,我们要注意锁的级别,MySQL innodb 在加锁时,只有明确的指定主键或(索引字段)才会使用 行锁;否则,会执行 表锁,将整个表锁住,此时性能会很差。在使用悲观锁时,我们必须关闭 MySQL 数据库的自动提交属性,因为mysql默认使用自动提交模式。悲观锁适用于写多的场景,而且并发性能要求不高
2、乐观锁
乐观锁,从字面意思也能猜到个大概,在操作数据时非常乐观,认为别人不会同时修改数据,因此乐观锁不会上锁 只是在 提交更新 时,才会正式对数据的冲突与否进行检测。如果发现冲突了,则返回错误信息,让用户决定如何去做,fail-fast 机制 。否则,执行本次操作。
分为三个阶段:数据读取、写入校验、数据写入。
如果是单机系统,我们可以基于JAVA 的 CAS来实现,CAS 是一种原子操作,借助硬件的比较并交换来实现。
如果是分布式系统,我们可以在数据库表中增加一个 版本号 字段,如:version
update 表
set ... , version = version +1
where id= #{id} and version = #{version}
操作前,先读取记录的版本号,更新时,通过SQL语句比较版本号是否一致。如果一致,则更新数据。否则会再次读取版本,重试上面的操作。
3、分布式锁
synchronized 、ReentrantLock 等,都是解决单体应用单机部署的资源互斥问题。随着业务快速发展,当单体应用演化为分布式集群后,多线程、多进程分布在不同的机器上,原来的单机并发控制锁策略失效
此时我们需要引入 分布式锁,解决跨机器的互斥机制来控制共享资源的访问。
分布式锁需要具备哪些条件:
-
与单机系统一样的资源互斥功能,这是锁的基础
-
高性能获取、释放锁
-
高可用
-
具备可重入性
-
有锁失效机制,防止死锁
-
非阻塞,不管是否获得锁,要能快速返回
实现方式多种多样,基于 数据库、Redis、以及 Zookeeper等,这里讲下主流的基于Redis的实现方式:
加锁:
SET key unique_value [EX seconds] [PX milliseconds] [NX|XX]
通过原子命令,如果执行成功返回 1,则表示加锁成功。注意:unique_value 是客户端生成的唯一标识,区分来自不同客户端的锁操作 解锁要特别注意,先判断 unique_value 是不是加锁的客户端,是的话才允许解锁删除。毕竟我们不能删除其他客户端加的锁。
解锁:解锁有两个命令操作,需要借助 Lua 脚本来保证原子性。
// 先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
借助 Redis 的高性能