1分布式锁实现方案
首先,为了确保分布式锁可用,保证锁实现的同时满足几个特性
1 互斥性。在任意时刻,只有一个客户端可以持有锁
2 不会发生死锁。即使一个客户端在持有锁的过程中崩溃,也能让其他的客户端能继续加锁
3 具有容错性,只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4 加锁和解锁必须是同一个客户端,客户端不能去解除其他客户端的锁
(1)基于redis实现
使用redis加锁,主要利用redis本身是单线程的,所以可以保证线程的安全,利用redis的setnx()方法的特点(如果key不存在,那么可以进行set操作,如果key存在则不做任何操作),加上过期时间的设置(保证这个锁在一定时间后会自动释放,哪怕当前线程死掉也不会造成死锁)的思想来实现分布式锁,下面看具体的实现
加锁的过程
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time)
,这个set()方法一共有五个形参:
- 第一个为key,我们使用key来当锁,因为key是唯一的。
- 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用
UUID.randomUUID().toString()
方法生成。 - 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
- 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
- 第五个为time,与第四个参数相呼应,代表key的过期时间。
总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。
心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。
解锁的过程
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()
方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:
简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
参考自 https://www.cnblogs.com/williamjie/p/9395659.html
(2)基于zookeeper实现
这个用的不多,所以也就不打算写出来了,如果感兴趣的可以看一下 给大家一个地址
https://www.jianshu.com/p/91976b27a188
(3)基于数据库的乐观锁
提到乐观锁,肯定就会想到悲观锁。那么先来将一下两者的差别吧
悲观锁 总是假设最坏的场景,每次去拿数据都会认为会被其他的修改,所以每次拿数据的时候都会上锁,这是别人拿数据就会阻塞,直到前一个释放了锁。共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
乐观锁 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
基于乐观锁如何实现分布式锁呢?
在数据库表中添加一个version字段用于记录这条数据的版本号,在数据更新的时候就会同时对这个版本号字段进行操作比如进行+1操作,此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据(也就是线程不安全造成的脏数据)。
乐观锁保证的是数据的最终一致性。
2分布式事务解决方案
(1)消息队列解决方案
关于消息队列有很多种RabbitMQ,ActiveMQ、Kafka
这里主要介绍如何用RabbitMQ解决分布式事务,关于RabbitMQ的具体介绍这里就不多说了 可以参考之前写的其他文章
https://blog.csdn.net/JIY743761374/article/details/98587548
如何使用RabbitMQ解决分布式事务呢?
RabbitMQ解决分布式事务采用了最终一致性的原则。
需要保证以下三要素
1、确认生产者一定要将数据投递到MQ服务器中(采用MQ消息确认机制)
2、MQ消费者消息能够正确消费消息,采用手动ACK模式,使用不补偿机制(注意重试幂等性问题)
3、如何保证第一个事务先执行,采用补偿机制(补单机制),在创建一个补单消费者进行监听,如果订单没有创建成功,进行补单。
1 确认生产者一定要将数据投递到MQ服务器中 (使用confirm机制 确认应答机制)
如果生产者数据投递到了MQ服务器成功
(1)消费者去消费消息失败,生产者不需要回滚事务,消费者采用手动的ack应答方式进行补偿机制,补偿过程中注意幂等问题。
如果生产者数据投递到了MQ服务器失败
生产者使用重试机制重发消息
如何保证第一个事务先执行,生产者投递消息到MQ服务器成功,消费者消费消息成功了,但是订单事务回滚了(生产者投递消息给消费者消费成功 然后 生产者回滚了)
MQ解决分布式原理通过最终一致性解决总体框架图: 交换机采用路由键模式 补单队列和派但队列都绑定同一个路由键
设置一个补偿交换机(派单交换机)绑定补偿队列(补单队列)然后进行补单操作。
可以在redis里面存储补单信息,然后由MQ来获取补单信息进行补单操作。
(2)TCC解决方案
这个也不详细说 存一个链接
https://blog.csdn.net/qq_39409110/article/details/88081661
(3)本地消息表解决方案
https://www.cnblogs.com/FlyAway2013/p/10124283.html
3分布式系统校验解决方案
(1)Redis实现分布式Session的单点登陆系统
首先创建一个单点登陆的SSO系统,用来作为统一的登陆页面和功能的提供,然后再其他所有的服务模块设置拦截器,通过对对应功能访问的拦截,拦截对应的url,从session中取出对应的token信息,然后通过token去redis中查看有没有对应的信息,如果没有跳转到单点系统登陆页面;如果有,则将redis中存放的用户信息存到session中去。
单点登陆系统 : 用户登录,查询数据库,查询用户信息是否正确,正确则获取用户信息,生成对应的token,作为key存入redis,用户信息作为value存入redis。
(2)JWT方式
https://www.cnblogs.com/wenqiangit/p/9592132.html
4 互联网高可用架构
(1)负载均衡技术分析
(2)通过keepalived实现中间间的高可用
5 分布式订单流水号生成策略分析
(1)基于数据库
我们知道,mysql数据库使用auto_increment.自增长字段,在集群环境下,不同的库设置不同的初始值,如每次自增加100
Mysql下修改起点和步长的方式
set @@auto_increment_offset=1; --设置起点
set @@auto_increment_increment=100; 设置步长为100;
show variables like ‘auto_inc%’;-- 查看参数
数据库1:设置初始值1 步长100 自增字段 1 101 201 301 401 .........1201
数据库2:设置初始值2 步长 100 自增字段 2 102 202 302 402 ........ 1202
数据库3:设置初始值3 步长 100 自增字段 3 103 203 303 403 ...........1203
等等...
策略2的优缺点:
优点:1.无需编码 2. 性能过得去 3.索引友好
缺点:1.大表不能做水评分表,否则插入删除一出现问题。比如数据库1. 数据库的数据过多,将其拆分,拆分后的表与新表的插入删除策略定制容易出现问题
2.以来前期规划,拓展麻烦. 如初始值100位,如数据量过于的大,100张表已经不够第101张表初始值没法设置
3.以来mysql内部维护“自增锁”,高并发下插入数据影响性能。
4.在业务中操作父、子表(关联表)插入时,要“先父后子”。比如user,order要先insert user表获取id,才能新增order表存储关联的外键。
(2)基于雪花算法
这种方案大致来说是一种以划分命名空间(UUID也算,由于比较常见,所以单独分析)来生成ID的一种算法,这种方案把64-bit分别划分成多段,分开来标示机器、时间等,比如在snowflake中的64-bit分别表示如下图(图片来自网络)所示:
41-bit的时间可以表示(1L<<41)/(1000L*3600*24*365)=69年的时间,10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。
这种方式的优缺点是:
优点:
毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
可以根据自身业务特性分配bit位,非常灵活。
缺点:
强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
https://blog.csdn.net/fly910905/article/details/82054196
(3)基于redis实现
思路:利用增长技术API,业务系统在自增长的基础上,配合其他信息组长成为一个唯一的id。
Redis的incr(key)用于将key进行自增,并返回增长的数值。
redis有如下特性:单线程原子操作、自增计数API、数据有效期机制EX
示例 1.业务编码+地区+自增数值(9 020 00000000000001)
示例 2. 12位=年2位+当年第几天3+小时2+自增5
可使用集群增加吞吐量,集群最后自增数字不同 比如节点1 自增3 节点2 自增5 节点3 自增7。
优点:
1.扩展性强,可以方便的结合业务进行处理
2.利用redis操作原子性的特性,保证在并发的时候不会重复。
缺点:
1.引入redis就意味着引入第三方依赖,要单独维护redis集群,保证redis服务的高可用
2.增加一次网略开销。
(4)各种方案对比
策略 | 优点 | 缺点 |
数据库自增 | 无代码调整、递增 | DB单点故障、扩展性瓶颈 |
雪花算法 | 性能优、不占贷款、趋势递增 | 依赖服务器时间 |
基于redis自研 | 无单点故障、性能优于DB、递增、扩展灵活 | 占用宽带、Redis集群维护 |