关于分布式中的一些锁的了解

2019-11-15  本文已影响0人  Hmcf

在高并发场景下,电商行业通常面临库存是否充足、是否超卖的问题。如下图

jt.png

这个问题有很多种技术解决方案,如:
悲观锁,分布式锁,乐观锁,队列串行化,Redis原子操作,等等



接下来就对上述的几种技术方案的原理性及基础应用做说明。

悲观锁

用法:SELECT … FOR UPDATE;

select * from tbl_user where id=1 for update;

获取锁的前提:结果集中的数据没有使用排他锁或共享锁时,才能获取锁,否则将会阻塞。
并且需要注意的是,for update 生效需要确保操作是在事务块儿中。


当执行 select ... for update时,将会把数据锁住,因此,我们需要注意一下锁的级别。MySQL InnoDB 默认为行级锁。当查询语句指定了主键时,MySQL会执行「行级锁」,否则MySQL会执行「表锁」。

常见情况如下:

缺点

悲观锁采用的是「先获取锁再访问」的策略,来保障数据的安全。但是加锁策略,依赖数据库实现,
会增加数据库的负担,且会增加死锁的发生几率。此外,对于不会发生变化的只读数据,
加锁只会增加额外不必要的负担。

适用场景

因为悲观锁会影响系统吞吐的性能,所以适合应用在为居多的场景下。

乐观锁

用法

我们建一张测试表 hmcf_test_op_lock(使用version字段),并添加两条记录,然后开始执行更新操作。
建表语句:create table hmcf_test_op_lock(id int, name text, num int, version int);

id name num version
1 葡萄干 2 0
2 香蕉 1 0



这里以id=1的数据作为测试案列

客户端A执行

update hmcf_test_op_lock set num=num-1, version=version+1 where id=1 and version=0
> Affected rows: 1
> 时间: 0.001s

此时影响的行数为一行,数据库中的数据更新了(select * from hmcf_test_op_lock where id=1)

id name num version
1 葡萄干 1 1

此时客户端B再执行

update hmcf_test_op_lock set num=num-1, version=version+1 where id=1 and version=0
> Affected rows: 0
> 时间: 0s

可以看到,此时影响的行数为0,因为version已经发生了变化。

缺点

乐观锁因为是通过我们人为实现的,它仅仅适用于我们自己业务中,
如果有外来事务插入,那么就可能发生错误。

适用场景

因为乐观锁就是为了避免悲观锁的弊端出现的,所以适合应用在为居多的场景下。

分布式锁

分布式锁所应具备的条件

分布式锁的三种常见的实现方式

分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”
所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,
都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”
只要这个最终时间是在用户可以接受的范围内即可。

在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。

一、基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

(1)创建一个表:

DROP TABLE IF EXISTS method_lock;
CREATE TABLE method_lock (
  id int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  method_name varchar(64) NOT NULL COMMENT '锁定的方法名',
  desc varchar(255) NOT NULL COMMENT '备注信息',
  update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  UNIQUE KEY uidx_method_name (method_name) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

(2)想要执行某个方法,就使用这个方法名向表中插入数据:

INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '测试的methodName');

因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。

(3)成功插入则获取锁,执行完成后删除对应的行数据释放锁:

delete from method_lock where method_name ='methodName';

注意:这只是使用基于数据库的一种方法,使用数据库实现分布式锁还有很多其他的玩法!

使用基于数据库的这种实现方式很简单,但是对于分布式锁应该具备的条件来说,它有一些问题需要解决及优化:

1、因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;

2、不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;

3、没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;

4、不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。

5、在实施的过程中会遇到各种不同的问题,为了解决这些问题,实现方式将会越来越复杂;依赖数据库需要一定的资源开销,性能问题需要考虑。



二、 基于Redis 原子操作

[图片上传失败...(image-cdbdca-1573806720083)]

1、选用Redis实现分布式锁原因:

2、使用命令介绍:

(1)SETNX

SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;
若key存在,则什么都不做,返回0。(可以考虑将过期时间作为值设置上,避免客户端expire时报错,导致死锁问题)

(2)expire

expire key timeout:为key设置一个超时时间,单位为second,
超过这个时间锁会自动释放,避免死锁。

(3)delete

delete key:删除key

在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。

队列串行化

关于队列串行化,在分布式应用中,通常应用将需要控制的操作发送的任务队列里面,然后通过消费者去队列里面去读取对应的任务,进行处理。
常用的任务队列有:rabbitmq 、 rocket_mq、kafka 等。
其中各消息队列的使用方法,会在后续的内容中补上。

上一篇 下一篇

猜你喜欢

热点阅读