分布式锁实现
1,数据库实现
原理
数据库的行级X锁。
优点
不需要引入第三方应用。
缺点
死锁
对数据库性能影响,可能较长时间占用数据库连接资源
如果业务是分库分表的,可能支持不了
示例代码
2,缓存实现
原理
通过SETNX是否成功。
当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0
优点
使用广泛
缺点
当线程获得锁,测试应用挂了,那么这个锁只能等到超时自动删除。所以一定要充分考虑业务可接受的超时时间。
实现
核心代码
加锁 `String ret = jedis.set(lockName, val, "NX", "EX", this.getSessionSeconds());`
解锁 Long ret = jedis.del(lockName);
3,zookeeper实现
原理
zk的临时顺序节点。加锁操作是客户端去/lock目录下创建临时顺序节点。客户端检查自己的节点序列号是目录下最小的,则获取锁成功。否则件事比自己小的最大节点。等待获取锁。
优点
稳定高效。不存在锁无法释放的问题。即使宕机,zk会自己删除临时节点。
缺点
要接入zk,实现较复杂的客户端逻辑。--但是,使用curator后也很简单了。
实现
1,使用原生zookeeper API。步骤:创建根目录,获取锁,创建临时顺序节点,排序,检查自己是不是当前最小的znode,是则获取锁,不是则监听比自己小的节点。释放锁,删除节点。
2,使用apache的curator包,步骤:引入包curator-recipes。加锁,interProcessMutex.acquire。解锁,interProcessMutex.release();
简单的令人发指。
延伸
1,主键与索引
- 索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
- 主键一定是唯一性索引,唯一性索引并不一定就是主键。
- 数据库管理系统对于主键自动生成唯一索引,所以主键也是一个特殊的索引。
- 一个表中可以有多个唯一性索引,但只能有一个主键。
- 主键列不允许空值,而唯一性索引列允许空值。
聚簇索引。InnoDB的数据组织形式。完整的记录都存储在主键索引中,通过主键索引可以找到记录所有的列。
普通索引。存的是主键索引。即通过普通索引,找到主键,再通过主键索引可以找到记录所有的列。
innodb对于主键使用了聚簇索引,这是一种数据存储方式,表数据是和主键一起存储,主键索引的叶结点存储行数据。对于普通索引,其叶子节点存储的是主键值。
2,sql执行计划
3,mysql/InnoDB 加锁机制分析
MySQL是一个支持插件式存储引擎的数据库系统。
大体上可以将Mysql分为两部分:Mysql Server + 存储引擎(如MyIsam、InnoDB)
以下内容基于InnoDB存储引擎。
InnoDB基于多版本并发控制MVCC,与MVCC相对的是基于锁的并发控制。
MVCC的最大好处是读不加锁,读写不冲突。这样在读多写少的应用中,极大的提高了并发性能。
MVCC中读又分为快照读和当前读。快照度顾名思义是读的快照版本,有可能是历史版本,不需要加锁。当前读是读取的当前版本,当前读返回的记录需要加锁,以保证其他事务不会并发修改。
那么什时候是当前读,什么时候是快照读?
快照读:简单的select语句。
select * from table where ..
不加锁
当前读:特殊的select语句。
select * from table where .. for update
X锁(排它锁)
select * from table where .. lock in share mode
S锁(共享锁)
增删改操作。
insert into table values (…)
X锁
update table set .. where ..
X锁
delete from table where ..
X锁
举个例子,对于update操作,
一句sql给到mysql,mysql server解析,根据where条件,读取第一条满足的记录,然后innoDB将第一条记录返回,并加锁。
mysql server 执行update,然后InnoDB引擎执行update操作,并返回成功。
这样一条记录执行完成。
接着,进行第二条记录的执行。
也就是说一句sql,会被拆成很多步操作。一条记录一条记录的加锁,执行。
数据库死锁问题涉及 索引 执行顺序 事务隔离级别三者。
两阶段锁
RDBMS对于加锁的一个原则是,两阶段锁,即2PL。含义是,加锁和解锁分为两个完全不相干的阶段,加锁时只加锁,解锁时只解锁。
比如,一个事务,先insert,再update,在delete。
那么加锁阶段做,inser的加锁,update的加锁,delete的加锁。
解锁阶段做,inser的解锁,update的解锁,delete的解锁。
完全分开,不混杂。
隔离级别
RDBMS四种隔离级别。
RU,read uncommit。可以读取其他事务未提交的操作。实际工程中不会使用的,忽略。
RC,read commit。针对当前读,加行锁。存在幻读情况。
RR,read repeat。针对当前读,加行锁,加间隙锁(GAP锁)。所以不存在幻读情况。
Serializable。从MVCC退回到基于锁的并发控制。读加共享锁,写加排它锁,且读写冲突。并发性能急剧下降,不建议使用。
幻读:举个例子就明白。在一个事务中,有两次一样的select操作。但是第一次select之后,有其他事务做了insert操作,导致第二次查询出的记录多了一条。这就是幻读。所以RR隔离加了间隙锁,其他事务的insert操作,不能成功,也就不存在幻读情况了。
加锁分析
给出一条sql语句,不给出前提条件就分析是很业余的表现。
必须要考虑哪些前提呢?
1,隔离级别
2,是不是主键
3,如果不是主键,是不是二级索引
4,如果是二级索引,它又是不是唯一索引
5,sql的执行计划是什么,
以一条sql为例。
update from table set .. where id = 100
1,id是主键,RC。 聚簇索引表id对应的这条记录加X锁。
2,id不是主键,但是二级唯一索引。RC。 id索引表id对应的这条索引记录加X锁。聚簇索引表对应的这条记录加X锁。
3,id不是主键,但是二级索引。RC。id索引表id对应的这条索引记录加X锁。聚簇索引表对应的这条记录加X锁。 区别就是非唯一索引,可能有多条加锁。
幻读就是在这种情况下发生的。
4,id上无索引。 此时只能走聚簇索引,做全表扫描。所有记录全加X锁。实际上mysql会做一层优化,mysql server会检查将不满足的记录释放掉锁。但是这个优化违背了2PL原则。
5,id是主键,RR。同RC一样。
6,id不是主键,但是二级唯一索引。RR。同RC一样。
7,id不是主键,但是二级索引。RR。同RC的区别就是再加间隙锁。
8,id上无索引。RR。和RC的区别是,再加上全表记录间的间隙锁。当然mysql也有优化。
9,Serializable隔离级别。测试所有的读都加共享锁。
死锁的场景
1,一种最常见的场景是,两个事务两条sql分别持有对方需要的行锁。
这种情况大部分是由于加锁顺序导致,排除方法是分析事务的加锁顺序,调整加锁顺序一致。
2,另一种是两个事务一条sql也会死锁。
这是因为索引的问题。我们已经知道数据库执行是一条一条执行的。
索引是单独存在的存储结构。
所以一条sql也是先给索引表加锁,再给对应的聚簇索引记录加锁。这就会产生和第一种情况类似的场景。
因为索引是有顺序的,比如name和age上都有索引。
update table set .. where name in (..);
update table set .. where age>10;
这样两条sql同时执行,name对应的第一条聚簇索引被锁住,age>10对应的聚簇索引第一条也被锁住。
然后再去锁各自对应的第二条时,发现都被对方锁住了。产生死锁。
所以,死锁归根结底就是加锁顺序的问题。这不只是数据库是这样的,死锁都是这个原因,资源的共享和占用时,需要同时持有多个资源,加锁顺序不同一定会导致死锁。