分布式锁的三种实现方式
作者:互联网
点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人。
文章不定期同步公众号,还有各种一线大厂面试原题、我的学习系列笔记。
eureka和nacos的区别
nacos | eureka | |
---|---|---|
应用 | nacos是阿里巴巴的开源中间件,可以直接启动jar即可用 | eureka需要连着springboot项目一起启动才可用 |
负载均衡 | nacos默认提供权重设置功能,调整承载流量压力 | 无 |
心跳机制 | nacos支持由客户端或服务端发起的健康检查 | Eureka是由客户端发起心跳 |
负载均衡策略 | 用Ribion | 用Ribion |
dubbo和k8s的集成 | 支持 | 不支持 |
选型建议 | 希望引入alibaba生态圈;希望在线对服务上下线&在线流量管理 | 希望引入spring clound生态圈 |
一致性协议 | 支持AP+CP任一种实现 | AP |
动态配置 | 支持(方便管理所有环境的服务配置) | 不支持 |
CAP理论
- C = 一致性,consistency,任何时候的节点数据都是一样的
- A = 可用性,avalibility,任何时候的节点请求不管成功失败都必须有回应
- p = 容错性,partition tolerence,任何时候的数据丢失都不会影响系统正常运行
任何分布式系统都无法同时满足CAP,只能满足其中两个,大部分IT公司只要求AP保证服务可用,并且在最终实现“C=最终一致性”即可,如何实现C最终一致性:分布式事务、分布式锁
幂等性
幂等性:多次重复请求/多次重复操作某一资源,产生的结果是一样的;对于数据库而言,幂等性就是多次重复地对数据库进行某一操作,得到的结果的一样的;对于接口而言,在设计的时候需要考虑幂等性,就是多次重复请求某一个接口,从接口处得到的结果是一样的
sql请求的幂等性
操作 | 是否幂等 | 示例 |
---|---|---|
查询 | 是 | select * from user where name='afei' |
新增 | 是 | insert into user(userid,name) values(1,'afei');若userid是主键,那这个sql就是幂等性的,因为只有第一次数据可以被插入,对数据库产生的结果是一样的;若userid不是主键,那这个sql就不是幂等性的,因为可以重复插入,对数据库产生的结果是不一样的 |
分布式锁
由上可知分布式锁主要用于解决CAP中的‘C’数据一致性问题:分布式环境中,可能存在多个进程竞争同一个资源,这就需要实现多进程间的“互斥锁”,在java中自带有实现线程间的互斥锁(Synchronized,Reentranlock),但是分布式环境下进程间的互斥需要自己实现,需要把这个“互斥锁”存在公共的地方被多个进程访问到,这样同一时刻只有一个进程能拿到“互斥锁”,进而保证了数据的一致性=‘C’,一般可用redis/zookeeper/数据库来实现分布式锁
基于数据库实现分布式锁
又分为两种方式:表锁、版本号机制
- 表锁:创建一张表,当要操作某些资源时,先锁住这些资源,即把这些'资源记录'往表里面插入,解锁时删掉这些记录即可
CREATE TABLE `order` ( //实现分布式锁的表
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` int(11) DEFAULT NULL comment `锁住的订单号资源`,
PRIMARY KEY (`id`),
unique key `unique_order_no`(`order_no`)
)ENGINE = INNODB
可知order_no为唯一性约束,当想锁住某个orderNo时->先把它插入表中->当有多个相同的order_no提交到数据库,只有一个能成功->想释放锁时,删除该条记录。可以先检查某个order_no是否在表中,不存在则插入,“检查+插入”应该放到同一个事务中:
@Transactional //“检查+插入”应该放到同一个事务中
public boolean addOrder(int orderNo) {
if(orderMapper.selectOrder(orderNo)==null){ //检查
//order表不存在该条记录则插入,表示orderNo订单号被锁定
int result = orderMapper.addOrder(orderNo);
if(result>0){
return true;
}
}
return false;
}
public void fun(int orderNo){
if(addOrder(orderNo)) {//拿到分布式锁
//业务处理
...
//处理完删除order表的orderNo记录,表示释放分布式锁
orderMapper.delete(orderNo);
}
}
- 版本号机制
CREATE TABLE `order` ( //实现分布式锁的表
`version` int(11) NOT NULL, //版本号、
`order_no` int(11) DEFAULT NULL comment `锁住的订单号资源`,
)ENGINE = INNODB
假如已存在version=1,orderNo='N1'的记录,则:
//先获取锁:
select version from order where order_no='N1';
//再占用锁:
udpate order ser version='2' where order_no='N1' and version='1';//更新成功则拿到锁
基于redis实现分布式锁
redis可以用【setnx(key,value)+设置expire】实现分布式锁,这是目前比较优的一种解决方案
public boolean fun(Jedis jedis,String key,String value,int expireTime){
Long r=jedis.setnx(key,value);//若key不存在则保存(key,value)并返回1,代表拿到分布式锁;若key已存在则设置失败并返回0,代表分布式锁已被占用
if(r==1){ //拿到分布式锁
//设置锁的过期时间:从当前时点开始经过expireTime(s)之后该key失效,key-value会被删除,代表分布式锁被释放;
jedis.expire(key,expireTime);
return true;//拿到true可以向下执行业务操作
}
return false;
}
产生的问题:
- 如果在【拿到锁,设置锁的过期时间】之间,程序奔溃,则设置锁的过期时间失败,也即释放锁失败,其他进程读取分布式锁失败,造成了死锁,解决:redis2.6.12之后提供一次性完成setnx和expireTime设置操作
//redis2.6.12之后:setnx提供了expireTime参数,可以一步设置过期时间
public boolean fun(Jedis jedis,String key,String value,int expireTime){
String r=jedis.setnx(key,value,"NX","PX",expireTime);//分布式锁已被占用
if("OK".equals(e)){ //拿到分布式锁
//设置过期时间释放分布式锁
jedis.expire(key,expireTime);
return true;
}
}
- 拿到锁之后,如果进行业务操作时间太长而expireTime设置太短,则会造成原来只有拿到锁才能执行的代码块失效,解决:设置expireTime之后,马上启动一个定时器,在expireTime快要到来之前,用lua原子性脚本删除原锁并重新设置新锁和新的到期时间(删除原key-value,再重新setnx设key-value和expireTime);
- 在redis集群下,主节点负责写,从节点负责读,由于主从复制是异步的,所以中间有一定的时间差,如果客户A从主节点获取到锁之后,还没来得及同步信息到其他的从节点,主节点就挂了,这时客户B再拿锁就会从 从节点获取到这个把锁,此时两个客户同时获取到了锁,解决:(待讨论)
- 锁的可重入问题,因为setnx是保证锁唯一的,如果拥有锁的进程想再次获取到该锁,就会失败,解决:当前进程用lua原子脚本把原锁删了,重新设置新锁+新expireTime(删除原key-value,再重新setnx设key-value和expireTime);
基于zookeeper实现分布式锁
先创建一个持久型父节点,每当客户端们想竞争访问共享资源时,都会在该父节点下新建一个临时有序的子节点->
不断地会有新的临时有序子节点被创建->
后面来的客户端在访问资源时,会检查自己创建的节点是否序号最小,若最小则获取锁,否则阻塞等待->
被阻塞等待的节点均会获取到上一个节点,并为上一节点注册watch事件监听节点是否还存在->
等到上一节点使用完共享资源,则会删除自身,进而触发watch事件被下一节点监听到,下一节点重复上面步骤:检查自己序号是否最小......
OK,如果文章哪里有错误或不足,欢迎各位留言。
创作不易,各位的「三连」是二少创作的最大动力!我们下期见!
分布式锁的三种实现方式
标签:方式,value,order,expireTime,三种,key,节点,分布式 来源: https://www.cnblogs.com/mofes/p/15009158.html