分布式环境下的锁(数据库锁)

为何要有分布式锁?java

目前不少的大型网站及应用都是分布式部署的,在不少场景中,咱们为了保证数据的一致性,须要不少技术来支撑,好比分布式事务,分布式锁。所谓分布式锁,是分布式系统或者集群的时候使用的锁,来锁定共享的资源。有时候咱们须要保证在同一时间同一方法中只有一个线程在执行。单机环境中,java提供了相应的API,可是在分布式环境下这些就无能为力了,因此针对分布式锁就有如下几种方案。mysql

1.基于数据库实现分布式锁redis

2.redis分布式锁(后面会有实际场景演练)sql

3.zookpeer实现分布式锁数据库

这些锁要实现或者是知足什么样的状况才须要用到?分布式

首先呢,锁就是为了保证同一时间同一方法下只有一个线程在执行;其次,这个锁的获取锁和释放锁要高可用,性能要好,最后就是是一把可重入锁(避免死锁)。性能

1、基于数据库实现分布式锁(排他锁)网站

用法: select … for update;spa

例如:select * from goods(表名) where id = 1 for update;线程

排他锁的申请前提:没有线程对该结果集中的任何行数据使用排他锁或共享锁,不然申请会阻塞。

for update仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效。在进行事务操做时,经过“for update”语句,MySQL会对查询结果集中每行数据都添加排他锁,其余线程对该记录的更新与删除操做都会阻塞。排他锁包含行锁、表锁。

场景分析

假设有一张商品表 goods,它包含 id,商品名称,库存量三个字段,表结构以下:

CREATE TABLE `goods` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT NULL,
  `stock` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_name` (`name`) USING HASH
) ENGINE=InnoDB

插入以下数据:

INSERT INTO `goods` VALUES ('1', 'prod11', '1000');
INSERT INTO `goods` VALUES ('2', 'prod12', '1000');
INSERT INTO `goods` VALUES ('3', 'prod13', '1000');
INSERT INTO `goods` VALUES ('4', 'prod14', '1000');
INSERT INTO `goods` VALUES ('5', 'prod15', '1000');
INSERT INTO `goods` VALUES ('6', 'prod16', '1000');
INSERT INTO `goods` VALUES ('7', 'prod17', '1000');
INSERT INTO `goods` VALUES ('8', 'prod18', '1000');
INSERT INTO `goods` VALUES ('9', 'prod19', '1000');

1、数据一致性

假设有A、B两个用户同时各购买一件 id=1 的商品,用户A获取到的库存量为 1000,用户B获取到的库存量也为 1000,用户A完成购买后修改该商品的库存量为 999,用户B完成购买后修改该商品的库存量为 999,此时库存量数据产生了不一致。

有两种解决方案:

悲观锁方案:每次获取商品时,对该商品加排他锁。也就是在用户A获取获取 id=1 的商品信息时对该行记录加锁,期间其余用户阻塞等待访问该记录。悲观锁适合写入频繁的场景。

begin;
select * from goods where id = 1 for update;
update goods set stock = stock - 1 where id = 1;
commit;

乐观锁方案:每次获取商品时,不对该商品加锁。在更新数据的时候须要比较程序中的库存量与数据库中的库存量是否相等,若是相等则进行更新,反之程序从新获取库存量,再次进行比较,直到两个库存量的数值相等才进行数据更新。乐观锁适合读取频繁的场景。

#不加锁获取 id=1 的商品对象
select * from goods where id = 1

begin;
#更新 stock 值,这里须要注意 where 条件 “stock = cur_stock”,只有程序中获取到的库存量与数据库中的库存量相等才执行更新
update goods set stock = stock - 1 where id = 1 and stock = cur_stock;
commit;

若是咱们须要设计一个商城系统,该选择以上的哪一种方案呢?

查询商品的频率比下单支付的频次高,基于以上我可能会优先考虑第二种方案(固然还有其余的方案,这里只考虑以上两种方案)。

2、行锁与表锁

咱们要注意锁的级别,mysql中InnoDB默认Row-Level Lock,因此只有在明确的指定主键的时候,mysql会执行Row Lock(锁住被选取的数据),不然会执行Table Lock(锁住整张表)。

一、只根据主键进行查询,而且查询到数据,主键字段产生行锁。

begin;
select * from goods where id = 1 for update;
commit;

二、只根据主键进行查询,没有查询到数据,不产生锁。

begin;
select * from goods where id = 1 for update;
commit;

三、根据主键、非主键含索引(name)进行查询,而且查询到数据,主键字段产生行锁,name字段产生行锁。

begin;
select * from goods where id = 1 and name='prod11' for update;
commit;

四、根据主键、非主键含索引(name)进行查询,没有查询到数据,不产生锁。

begin;
select * from goods where id = 1 and name='prod12' for update;
commit;

五、根据主键、非主键不含索引(name)进行查询,而且查询到数据,若是其余线程按主键字段进行再次查询,则主键字段产生行锁,若是其余线程按非主键不含索引字段进行查询,则非主键不含索引字段产生表锁,若是其余线程按非主键含索引字段进行查询,则非主键含索引字段产生行锁,若是索引值是枚举类型,mysql也会进行表锁。

begin;
select * from goods where id = 1 and name='prod11' for update;
commit;

六、根据主键、非主键不含索引(name)进行查询,没有查询到数据,不产生锁。

begin;
select * from goods where id = 1 and name='prod12' for update;
commit;

七、根据非主键含索引(name)进行查询,而且查询到数据,name字段产生行锁。

begin;
select * from goods where name='prod11' for update;
commit;

八、根据非主键含索引(name)进行查询,没有查询到数据,不产生锁。

begin;
select * from goods where name='prod11' for update;
commit;

九、根据非主键不含索引(name)进行查询,而且查询到数据,name字段产生表锁。

begin;
select * from goods where name='prod11' for update;
commit;

十、根据非主键不含索引(name)进行查询,没有查询到数据,name字段产生表锁。

begin;
select * from goods where name='prod11' for update;
commit;

十一、只根据主键进行查询,查询条件为不等于,而且查询到数据,主键字段产生表锁。

begin;
select * from goods where id <> 1 for update;
commit;

十二、只根据主键进行查询,查询条件为不等于,没有查询到数据,主键字段产生表锁。

begin;
select * from goods where id <> 1 for update;
commit;

1三、只根据主键进行查询,查询条件为 like,而且查询到数据,主键字段产生表锁。

begin;
select * from goods where id like '1' for update;
commit;

1四、只根据主键进行查询,查询条件为 like,没有查询到数据,主键字段产生表锁。

begin;
select * from goods where id like '1' for update;
commit;

 

总结

一、InnoDB行锁是经过给索引上的索引项加锁来实现的,只有经过索引条件检索数据,InnoDB才使用行级锁,不然,InnoDB将使用表锁。

二、因为MySQL的行锁是针对索引加的锁,不是针对记录加的锁,因此虽然是访问不一样行的记录,可是若是是使用相同的索引键,是会出现锁冲突的。应用设计的时候要注意这一点。  三、当表有多个索引的时候,不一样的事务可使用不一样的索引锁定不一样的行,另外,不管是使用主键索引、惟一索引或普通索引,InnoDB都会使用行锁来对数据加锁。  四、即使在条件中使用了索引字段,可是否使用索引来检索数据是由MySQL经过判断不一样执行计划的代价来决定的,若是MySQL认为全表扫描效率更高,好比对一些很小的表,它就不会使用索引,这种状况下InnoDB将使用表锁,而不是行锁。所以,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。  五、检索值的数据类型与索引字段不一样,虽然MySQL可以进行数据类型转换,但却不会使用索引,从而致使InnoDB使用表锁。经过用explain检查两条SQL的执行计划,咱们能够清楚地看到了这一点。

相关文章
相关标签/搜索