网站建设公司怎么选,设计网站推荐外网,建设公司官网的请示,北京百度搜索排名优化文章目录 分布式锁0-1分布式锁--包含CAP理论模型概述分布式锁#xff1a;分布式锁应该具备哪些条件#xff1a;分布式锁的业务场景#xff1a; 分布式锁的实现方式有#xff1a;基于Zookeeper - 分布式锁实现思想优缺点基于Redis - 分布式锁实现思想实现思想的具体步骤分布式锁应该具备哪些条件分布式锁的业务场景 分布式锁的实现方式有基于Zookeeper - 分布式锁实现思想优缺点基于Redis - 分布式锁实现思想实现思想的具体步骤优缺点Redis分布式锁实现-例子方案一改进 方案一 再改进 方案一方案二改进方案二再改进方案二再再次改进 方案二分段锁 基于数据库 - 分布式锁实现思想A. 悲观锁排他锁B. 乐观锁 今天来和大家谈谈分布式锁的内容在这个快速发展的经济时代分布式锁也随之而发生。 分布式锁对应的也有
分布式事务 链接如下 https://blog.csdn.net/weixin_44797327/article/details/134611487?spm1001.2014.3001.5502 分布式锁
0-1分布式锁–包含CAP理论模型
参考文章-分布式锁
概述
随着互联网技术的不断发展用户量的不断增加越来越多的业务场景需要用到分布式系统。
分布式系统有一个著名的理论CAP指在一个分布式系统中最多只能同时满足下面三项中的两项
一致性Consistency在分布式系统中的所有数据备份在同一时刻是否同样的值等同于所有节点访问同一份最新的数据副本 可用性Availability保证每个请求不管成功或者失败都有响应 分区容错性Partition tolerance系统中任意信息的丢失或失败不会影响系统的继续运作
所以在设计系统时往往需要权衡在CAP中作选择要么AP要么CP、要么AC。
当然这个理论也并不一定完美不同系统对CAP的要求级别不一样选择需要考虑方方面面。
而在分布式系统中访问共享资源就需要一种互斥机制来防止彼此之间的互相干扰以保证一致性这个时候就需要使用分布式锁。
分布式锁
当在分布式模型下数据只有一份或有限的此时需要利用锁技术来控制某一时刻修改数据的进程数。这种锁即为分布式锁。
为了保证一个方法或属性在高并发的情况下, 同一时间只能被同一个线程执行在传统单体应用单机部署的情况下可以使用并发处理相关的功能进行互斥控制。但是随着业务发展的需要原单体单机部署的系统被演化成分布式集群系统后由于分布式系统多线程、多进程并且分布在不同机器上这将使原单机部署情况下的并发控制锁策略失效单纯的应用并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问这就是分布式锁要解决的问题
分布式锁应该具备哪些条件
互斥性在分布式系统环境下一个方法在同一时间只能被一个机器的一个线程执行 高可用的获取锁与释放锁 高性能的获取锁与释放锁 可重入性具备可重入特性具备锁失效机制防止死锁即就算一个客户端持有锁的期间崩溃而没有主动释放锁也需要保证后续其他客户端能够加锁成功 非阻塞具备非阻塞锁特性即没有获取到锁将直接返回获取锁失败
分布式锁的业务场景
互联网秒杀商品库存 抢优惠券
分布式锁的实现方式有
主要有几种实现方式
基于数据库实现 基于Zookeeper实现 基于Redis实现
如图
分布式锁对比 从理解的难易程度角度从低到高数据库 缓存 Zookeeper
从实现的复杂性角度从低到高Zookeeper 缓存 数据库
从性能角度从高到低缓存 Zookeeper 数据库
从可靠性角度从高到低Zookeeper 缓存 数据库
基于Zookeeper - 分布式锁
实现思想
ZooKeeper是一个为分布式应用提供一致性服务的开源组件它内部是一个分层的文件系统目录树结构规定同一个目录下只能有一个唯一文件名。
基于ZooKeeper实现分布式锁的步骤如下
创建一个目录mylock
线程A想获取锁就在mylock目录下创建临时顺序节点
获取mylock目录下所有的子节点然后获取比自己小的兄弟节点如果不存在则说明当前线程顺序号最小获得锁
线程B获取所有节点判断自己不是最小节点设置监听比自己次小的节点
线程A处理完删除自己的节点线程B监听到变更事件判断自己是不是最小的节点如果是则获得锁。
整个过程如图
业界推荐直接使用Apache的开源库Curator它是一个ZooKeeper客户端Curator提供的InterProcessMutex是分布式锁的实现acquire方法用于获取锁release方法用于释放锁。
使用方式很简单
InterProcessMutex interProcessMutex new InterProcessMutex(client,/anyLock);
interProcessMutex.acquire();
interProcessMutex.release(); 其他分布式锁的核心源码如下
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception{ boolean haveTheLock false; boolean doDelete false; try { if ( revocable.get() ! null ) { client.getData().usingWatcher(revocableWatcher).forPath(ourPath); } while ((client.getState() CuratorFrameworkState.STARTED) !haveTheLock ) { // 获取当前所有节点排序后的集合 ListString children getSortedChildren(); // 获取当前节点的名称 String sequenceNodeName ourPath.substring(basePath.length() 1); // 1 to include the slash // 判断当前节点是否是最小的节点 PredicateResults predicateResults driver.getsTheLock(client, children, sequenceNodeName, maxLeases); if ( predicateResults.getsTheLock() ) { // 获取到锁 haveTheLock true; } else { // 没获取到锁对当前节点的上一个节点注册一个监听器 String previousSequencePath basePath / predicateResults.getPathToWatch(); synchronized(this){ Stat stat client.checkExists().usingWatcher(watcher).forPath(previousSequencePath); if ( stat ! null ){ if ( millisToWait ! null ){ millisToWait - (System.currentTimeMillis() - startMillis); startMillis System.currentTimeMillis(); if ( millisToWait 0 ){ doDelete true; // timed out - delete our node break; } wait(millisToWait); }else{ wait(); } } } // else it may have been deleted (i.e. lock released). Try to acquire again } } } catch ( Exception e ) { doDelete true; throw e; } finally{ if ( doDelete ){ deleteOurPath(ourPath); } } return haveTheLock;
}
其实 Curator 实现分布式锁的底层原理和上面分析的是差不多的。如图详细描述其原理
另外可基于Zookeeper自身的特性和原生Zookeeper API自行实现分布式锁。
优缺点
优点
可靠性非常高 性能较好 CAP模型属于CP基于ZAB一致性算法实现
**缺点 **
性能并不如Redis主要原因是在写操作即获取锁释放锁都需要在Leader上执行然后同步到follower 实现复杂度高
基于Redis - 分布式锁 实现思想
主要是基于命令SETNX key value
命令官方文档https://redis.io/commands/setnx
用法可参考Redis命令参考
如图
实现思想的具体步骤
获取锁的时候使用setnx加锁并使用expire命令为锁添加一个超时时间超过该时间则自动释放锁锁的value值为一个随机生成的UUID通过此处释放锁的时候进行判断。 获取锁的时候还设置一个获取的超时时间若超过这个时间则放弃获取锁。 释放锁的时候通过UUID判断是不是该锁若是该锁则执行delete进行锁释放。
优缺点
优点
性能非常高可靠性较高CAP模型属于AP
缺点
复杂度较高无一致性算法可靠性并不如Zookeeper锁删除失败 过期时间不好控制非阻塞获取失败后需要轮询不断尝试获取锁比较消耗性能占用cpu资源
Redis分布式锁实现-例子
以减库存接口为例子访问接口的时候自动减商品的库存
方案一
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();//获取redis中的库存int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}return success;}
}表示
先从Redis中读取stock的值表示商品的库存判断商品库存是否大于0如果大于0则库存减1然后再保存到Redis里面去否则就报错
改进 方案一
这种简单的从Redis读取、判断值再减1保存到Redis的操作很容易在并发场景下出问题
商品超卖
比如
假设商品的库存有50个有3个用户同时访问该接口先是同时读取Redis中商品的库存值即都是读取到了50即同时执行到了这一行
int stock Integer.valueOf(valueOperations.get(stock));然后减1即到了这一行
int newStock stock - 1;此时3个用户的realStock都是49然后3个用户都去设置stock为49那么就会产生库存明明被3个用户抢了理论上是应该减去3的结果库存数只减去了1导致商品超卖。
这种问题的产生原因是因为读取库存、减库存、保存到Redis这几步并不是原子操作
那么可以使用加并发锁synchronized来解决
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();synchronized (this) {//获取redis中的库存int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}}return success;}
}注意在Java中关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块。
再改进 方案一
以上的代码在单体模式下并没太大问题但是在分布式或集群架构环境下存在问题比如架构如下 在分布式或集群架构下synchronized只能保证当前的主机在同一时刻只能有一个线程执行减库存操作但如图同时有多个请求过来访问的时候不同主机在同一时刻依然是可以访问减库存接口的这就导致问题1商品超卖在集群架构下依然存在。
解决方法
使用如下方案二的分布式锁进行解决 注意方案一并不是分布式锁
方案二
分布式锁的简单实现图如下 简单的实现代码如下
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();String lockKey product_001;//加锁: setnxBoolean isSuccess valueOperations.setIfAbsent(lockKey, 1);if(null isSuccess || isSuccess) {System.out.println(服务器繁忙, 请稍后重试);return error;}//------ 执行业务逻辑 ----start------int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;//执行业务操作减库存valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}//------ 执行业务逻辑 ----end------//释放锁redisTemplate.delete(lockKey);return success;}
}其实就是对每一个商品加一把锁代码里面是product_001
使用setnx对商品进行加锁如成功说明加锁成功如失败说明有其他请求抢占了该商品的锁则当前请求失败退出加锁成功之后进行扣减库存操作删除商品锁
以上的代码方式是有可能会造成死锁的比如说加锁成功之后扣减库存的逻辑可能抛异常了即并不会执行到释放锁的逻辑那么该商品锁是一直没有释放会成为死锁的其他请求完全无法扣减该商品的
使用try...catch...finally的方式可以解决抛异常的问题如下
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();String lockKey product_001;try {//加锁: setnxBoolean isSuccess valueOperations.setIfAbsent(lockKey, 1);if(null isSuccess || isSuccess) {System.out.println(服务器繁忙, 请稍后重试);return error;}//------ 执行业务逻辑 ----start------int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;//执行业务操作减库存valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}//------ 执行业务逻辑 ----end------} finally {//释放锁redisTemplate.delete(lockKey);}return success;}
}改进方案二
那么上面的方式是不是能够解决死锁的问题呢
其实不然除了抛异常之外比如程序崩溃、服务器宕机、服务器重启、请求超时被终止、发布、人为kill等都有可能导致释放锁的逻辑没有执行比如对商品加分布式锁成功之后在扣减库存的时候服务器正在执行重启会导致没有执行释放锁。
可以通过对锁设置超时时间来防止死锁的发生使用Redis的expire命令可以对key进行设置超时时间如图
代码实现如下
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();String lockKey product_001;try {//加锁: setnxBoolean isSuccess valueOperations.setIfAbsent(lockKey, 1);//expire增加超时时间redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);if(null isSuccess || isSuccess) {System.out.println(服务器繁忙, 请稍后重试);return error;}//------ 执行业务逻辑 ----start------int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;//执行业务操作减库存valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}//------ 执行业务逻辑 ----end------} finally {//释放锁redisTemplate.delete(lockKey);}return success;}
}加锁成功之后把锁的超时时间设置为10秒即10秒之后自动会释放锁避免死锁的发生。
再改进方案二
但是上面的方式同样会产生死锁问题加锁和对锁设置超时时间并不是原子操作在加锁成功之后即将执行设置超时时间的时候系统发生崩溃同样还是会导致死锁。
改进图案如下 对此有两种做法
lua脚本 set原生命令Redis 2.6.12版本及以上
一般是推荐使用set命令Redis官方在2.6.12版本对set命令增加了NX、EX、PX等参数即可以将上面的加锁和设置时间放到一条命令上执行通过set命令即可
命令官方文档https://redis.io/commands/set
用法可参考Redis命令参考
如图
SET key value NX 等同于 SETNX key value命令并且可以使用EX参数来设置过期时间
注意其实目前在Redis 2.6.12版本之后所说的setnx命令并非单单指Redis的SETNX key value命令一般是代指Redis中对set命令加上nx参数进行使用一般不会直接使用SETNX key value命令了
注意Redis2.6.12之前的版本只能通过lua脚本来保证原子性了。
如图
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();String lockKey product_001;try {//加锁: setnx 和 expire增加超时时间Boolean isSuccess valueOperations.setIfAbsent(lockKey, 1, 10, TimeUnit.SECONDS);if(null isSuccess || isSuccess) {System.out.println(服务器繁忙, 请稍后重试);return error;}//------ 执行业务逻辑 ----start------int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;//执行业务操作减库存valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}//------ 执行业务逻辑 ----end------} finally {//释放锁redisTemplate.delete(lockKey);}return success;}
}再再次改进 方案二
以上的方式其实还是存在着问题在高并发场景下会存在问题超时时间设置不合理导致的问题
大概的流程图可参考 流程
进程A加锁之后扣减库存的时间超过设置的超时时间这里设置的锁是10秒在第10秒的时候由于时间到期了所以进程A设置的锁被Redis释放了T5刚好进程B请求进来了加锁成功T6进程A操作完成扣减库存之后把进程B设置的锁给释放了刚好进程C请求进来了加锁成功 进程B操作完成之后也把进程C设置的锁给释放了以此类推…
解决方法也很简单
加锁的时候把值设置为唯一值比如说UUID这种随机数释放锁的时候获取锁的值判断value是不是当前进程设置的唯一值如果是再去删除
如图
实现的代码如下
Service
public class RedisLockDemo {Autowiredprivate StringRedisTemplate redisTemplate;public String deduceStock() {ValueOperationsString, String valueOperations redisTemplate.opsForValue();String lockKey product_001;String clientId UUID.randomUUID().toString();try {//加锁: setnx 和 expire增加超时时间Boolean isSuccess valueOperations.setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);if(null isSuccess || isSuccess) {System.out.println(服务器繁忙, 请稍后重试);return error;}//------ 执行业务逻辑 ----start------int stock Integer.valueOf(valueOperations.get(stock));if (stock 0) {int newStock stock - 1;//执行业务操作减库存valueOperations.set(stock, newStock );System.out.println(扣减库存成功, 剩余库存: newStock);} else {System.out.println(库存已经为0不能继续扣减);}//------ 执行业务逻辑 ----end------} finally {if (clientId.equals(valueOperations.get(lockKey))) {//释放锁redisTemplate.delete(lockKey);}}return success;}
}分段锁
怎么在高并发的场景去实现一个高性能的分布式锁呢
电商网站在大促的时候并发量很大
1若抢购不是同一个商品则可以增加Redis集群的cluster来实现因为不是同一个商品所以通过计算 key 的hash会落到不同的 cluster上
2若抢购的是同一个商品则计算key的hash值会落同一个cluster上所以加机器也是没有用的。
针对第二个问题可以使用库存分段锁的方式去实现。
分段锁
假如产品1有200个库存可以将这200个库存分为10个段存储每段20个每段存储到一个cluster上将key使用hash计算使这些key最后落在不同的cluster上。
每个下单请求锁了一个库存分段然后在业务逻辑里面就对数据库或者是Redis中的那个分段库存进行操作即可包括查库存 - 判断库存是否充足 - 扣减库存。
具体可以参照 ConcurrentHashMap 的源码去实现它使用的就是分段锁。
高性能分布式锁具体可参考链接每秒上千订单场景下的分布式锁高并发优化实践【石杉的架构笔记】
原理如图
基于数据库 - 分布式锁
实现思想
主要有两种方式
悲观锁 乐观锁
A. 悲观锁排他锁
利用select * form table where xxyy for update 排他锁
注意这里需要注意的是 where xxyyxx字段必须要走索引否则会锁表。有些情况下比如表不大mysql优化器会不走这个索引导致锁表问题。
**核心思想**以「悲观的心态」操作资源无法获得锁成功就一直阻塞着等待。
注意该方式有很多缺陷一般不建议使用。
实现
创建一张资源锁表
CREATE TABLE resource_lock (id int(4) NOT NULL AUTO_INCREMENT COMMENT 主键,resource_name varchar(64) NOT NULL DEFAULT COMMENT 锁定的资源名,owner varchar(64) NOT NULL DEFAULT COMMENT 锁拥有者,desc varchar(1024) NOT NULL DEFAULT 备注信息,update_time timestamp NOT NULL DEFAULT COMMENT 保存数据时间自动生成,PRIMARY KEY (id),UNIQUE KEY uidx_resource_name (resource_name ) USING BTREE
) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT锁定中的资源;注意resource_name 锁资源名称必须有唯一索引
使用事务查询更新
Transaction
public void lock(String name) {ResourceLock rlock exeSql(select * from resource_lock where resource_name name for update);if (rlock null) {exeSql(insert into resource_lock(reosurce_name,owner,count) values (name, ip,0));}
}使用 for update 锁定的资源 :
如果执行成功会立即返回执行插入数据库后续再执行一些其他业务逻辑直到事务提交执行结束
如果执行失败就会一直阻塞着。
可以在数据库客户端工具上测试出来这个效果当在一个终端执行了 for update不提交事务。在另外的终端上执行相同条件的 for update会一直卡着
虽然也能实现分布式锁的效果但是会存在性能瓶颈。
优点
简单易用好理解保障数据强一致性。
缺点
1在 RR 事务级别select 的 for update 操作是基于间隙锁gap lock 实现的是一种悲观锁的实现方式所以存在阻塞问题。
2高并发情况下大量请求进来会导致大部分请求进行排队影响数据库稳定性也会耗费服务的CPU等资源。
当获得锁的客户端等待时间过长时会提示
[40001][1205] Lock wait timeout exceeded; try restarting transaction
高并发情况下也会造成占用过多的应用线程导致业务无法正常响应。
3如果优先获得锁的线程因为某些原因一直没有释放掉锁可能会导致死锁的发生。
4锁的长时间不释放会一直占用数据库连接可能会将数据库连接池撑爆影响其他服务。
5MySql数据库会做查询优化即便使用了索引优化时发现全表扫效率更高则可能会将行锁升级为表锁此时可能就更悲剧了。
6不支持可重入特性并且超时等待时间是全局的不能随便改动。
B. 乐观锁
所谓乐观锁与悲观锁最大区别在于基于 CAS思想 表中添加一个时间戳或者是版本号的字段来实现update xx set versionnew_version where xxyy and versionOld_version通过增加递增的版本号字段实现乐观锁。
不具有互斥性不会产生锁等待而消耗资源操作过程中认为不存在并发冲突只有update version失败后才能觉察到。
抢购、秒杀就是用了这种实现以防止超卖的现象。
实现
创建一张资源锁表
CREATE TABLE resource (id int(4) NOT NULL AUTO_INCREMENT COMMENT 主键,resource_name varchar(64) NOT NULL DEFAULT COMMENT 资源名,share varchar(64) NOT NULL DEFAULT COMMENT 状态,version int(4) NOT NULL DEFAULT COMMENT 版本号,desc varchar(1024) NOT NULL DEFAULT 备注信息,update_time timestamp NOT NULL DEFAULT COMMENT 保存数据时间自动生成,PRIMARY KEY (id),UNIQUE KEY uidx_resource_name (resource_name ) USING BTREE
) ENGINEInnoDB DEFAULT CHARSETutf8 COMMENT资源;为表添加一个字段版本号或者时间戳都可以。通过版本号或者时间戳来保证多线程同时间操作共享资源的有序性和正确性。
伪代码实现
Resrouce resource exeSql(select * from resource where resource_name xxx);
boolean succ exeSql(update resource set version newVersion ... where resource_name xxx and version oldVersion);if (!succ) {// 发起重试
}实际代码中可以写个while循环不断重试版本号不一致更新失败重新获取新的版本号直到更新成功。
优缺点 优点
实现简单复杂度低 保障数据一致性
缺点
性能低并且有锁表的风险 可靠性差 非阻塞操作失败后需要轮询占用CPU资源 长时间不commit或者是长时间轮询可能会占用较多的连接资源