Redis实现分布式锁
单机模式下,Synchronized和ReetrantLock都可以很好的处理锁问题,即使有多用户并发请求也可以运行正常。
但是,在分布式下,这些锁都会失效。(分布式下以前的所有锁都不能用,分布式系统JVM都是互不相关的,加不上同一把锁)
一般采用redis来实现分布式锁。
/**
*
* setnx->set if not exist:原子操作。判断带保存。
*
*1)、代码第一阶段;
* public void hello(){
*
* //获取和设置值必须是原子的
* String lock = getFromRedis("lock");//get("lock")
* if(lock == null){
* setRedisKey("lock","1");
* //执行业务
* delRedisKey("lock")
* return ;
* }else{
* hello();//自旋
* }
* }
* //问题:加锁的原子性
*
* 2、代码第二阶段
* public void hello(){
* //1、获取到锁
* Integer lock = setnx("lock',"111"); //0代表没有保存数据,说明已经有人占了。1代表占可坑成功
* if(lock!=0){
* //执行业务逻辑
* //释放锁、删除锁
* del("lock")
* }else{
* //等待重试
* hello();
* }
* }
* //问题:如果由于各种问题(未捕获的异常、断电等)导致锁没释放。其他人永远获取不到锁。
* //解决:加个过期时间。
*
* 3、代码第三阶段
* public void hello(){
* //超时和加锁必须原子
* Integer lock = setnx("lock',"111");
* if(lock!=null){
* expire("lock",10s);//设置过期时间
* //执行业务逻辑
* // 释放锁
* del("lock')
* }else{
* hello();
* }
*
* }
* 问题:刚拿到锁,机器炸了,没来得及设置超时。
* 解决:加锁和加超时也必须是原子的。
*
*
* 4、代码第四阶段:
* public void hello(){
* String result = setnxex("lock","111",10s);
* if(result=="ok"){
* //加锁成功
* //执行业务逻辑
* del("lock")
* }else{
* hello();
* }
* }
* 问题:如果业务逻辑超时,导致锁自动删除,业务执行完又删除一遍。至少多个人都获取到了锁。
*
* 5、代码第五阶段。
* public void hello(){
* String token = UUID;
* String result = setnxex("lock",token,10s);
* if(result == "ok"){
* //执行业务
*
* //删锁,保证删除自己的锁
* if(get("lock")==token){
* del("lock")
* }
* }else{
* hello();
* }
* }
* 问题?:我们获取锁的时候,锁的值正在给我们返回。锁过期。redis删除了锁。
* 但是我们拿到了值,而且对比成功(此时此刻正好有人又获取)。我们还删除了锁。至少两个线程又进入同一个代码。
* 原因:?删锁不是原子。
* lua脚本。
*
* 解决:
* String script =
* "if redis.call('get', KEYS[1]) == ARGV[1] then
* return redis.call('del', KEYS[1])
* else
* return 0
* end";
*
* jedis.eval(script, Collections.singletonList(key), Collections.singletonList(token));
*
* lua脚本进行删除。
*
*
* 1)、分布式锁的核心(保证原子性)
* 1)、加锁。占坑一定要是原子的。(判断如果没有,就给redis中保存值)
* 2)、锁要自动超时。
* 3)、解锁也要原子。
*
*
* 最终的分布式锁的代码:大家都去redis中占同一个坑。
*
*
*
* @Lock
* public void hello(){
* String token = uuid;
* String lock = redis.setnxex("lock",token,10s);
* if(lock=="ok"){
* //执行业务逻辑
* //脚本删除锁
* }else{
* hello();//自旋。
* }
* }
*
* AOP;上面的@Lock注解,
*
* RedisTemplate和Jedis客户端2选一
*
*/
Redisson
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet
, Set
, Multimap
, SortedSet
, Map
, List
, Queue
, BlockingQueue
, Deque
, BlockingDeque
, Semaphore
, Lock
, AtomicLong
, CountDownLatch
, Publish / Subscribe
, Bloom filter
, Remote service
, Spring cache
, Executor service
, Live Object service
, Scheduler service
)
基于Redis的Redisson分布式可重入锁RLock
Java对象实现了java.util.concurrent.locks.Lock
接口。
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();
//大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
读写锁
基于Redis的Redisson分布式可重入读写锁RReadWriteLock
Java对象实现了java.util.concurrent.locks.ReadWriteLock
接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();//一般会设置过期时间
// 或
rwlock.writeLock().lock();
缓存
1.问题:查询效率高,数据变化频率不是太快的。我们进缓存。如何让缓存和数据库同步?
答:
写场景:更新数据库成功后,同步更新缓存;
读场景:先从缓存中读,没有再从数据库读,放入缓存。
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
1):一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。
2):可以用个读写锁。 性能慢,可以用粒度细点的锁。
(以上仅为雷丰阳个人观点,以下为博客观点)
如何解决缓存与数据库不一致?
读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存和数据库间的数据一致性问题。不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举个例子:
1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
1.延时双删策略
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:
1)先删除缓存
2)再写数据库
3)休眠500毫秒(根据具体的业务时间来定)
4)再次删除缓存。
那么,这个500毫秒怎么确定的,具体该休眠多久呢?
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束(读到旧数据的请求),写请求可以删除读请求造成的缓存脏数据。
设置缓存过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
该方案的弊端
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致(双删失败的情况),而且又增加了写请求的耗时
2.异步更新缓存(基于订阅binlog的同步机制)
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis
- 读Redis:热数据基本都在Redis
- 写MySQL:增删改都是操作MySQL
- 更新Redis数据:MySQ的数据操作binlog,来更新到Redis
(1)数据操作主要分为两大块:
- 一个是全量(将全部数据一次写入到redis)
- 一个是增量(实时更新)
这里说的是增量,指的是mysql的update、insert、delate变更数据。
(2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
Redis和ElasticSearch
redis优缺点
1、redis最大特点是key-value存储,简单且性能高 一种key-value数据库中功能最全,最简单易用的款。
2、redis会把所有数据加载到内存中。
2、redis还支持数据持久化,list,set等多种数据结构,master-slave 复制备份。
redis缺点:
1、由于去掉了表字段,所有查询都用来key, 所以无法支持常规的多列查询,区段查询。
2、由于Redis需要把数据存在内存中,这也大大限制了Redis可存储的数据量,这也决定了Redis难以用在数据规模很大的应用场景中
ES(ElasticSearch)
ES:ElasticSearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。严格来说,他不是一个数据库,是个搜索引擎,即搜索服务器。es提供了一种分布式多用户能力的全文搜索引擎。
Elasticsearch是分布式的,这意味着索引可以被分成分片,每个分片可以有0个或多个副本。每个节点托管一个或多个分片,并充当协调器将操作委托给正确的分片。再平衡和路由是自动完成的。“相关数据通常存储在同一个索引中,该索引由一个或多个主分片和零个或多个复制分片组成。一旦创建了索引,就不能更改主分片的数量。
优点:能够达到实时搜索,稳定,可靠,快速,安装使用方便
缺点:也有很多,自己查
分布式事务
CAP定理:Consistency(一致性), Availability(可用性)和Partition tolerance(分区容错)这三个指标不可能同时做到。
分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。(要么是CP, 要么是AP)
Consistency 和 Availability 的矛盾
一致性和可用性,为什么不可能同时成立?答案很简单,因为可能通信失败(即出现分区容错)。
如果保证 P2 的一致性,那么 P1 必须在写操作时,锁定 P2 的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,P2 不能读写,没有可用性不。
如果保证 P2 的可用性,那么势必不能锁定 P2,所以一致性不成立。
BASE理论
BA:Basically Available 基本可用,分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
S:Soft State 软状态,允许系统存在中间状态,而该中间状态不会影响系统整体可用性。
E:Consistency 最终一致性,系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。
BASE 理论本质上是对 CAP 理论的延伸,是对 CAP 中 AP 方案的一个补充。
TCC(Try Confirm Cancel)
Try阶段:尝试执行,预留业务资源。(eg: 库存表本来只有一个库存数字段,TCC方案可以多给他一个冻结库存数字段。本来商品库存为10,在用户下单完成支付这个流程中,假设买了一个商品,原来的逻辑就得改一下。正常正常你买1个商品,那库存就是10-1=9 咯,所以将库存数改为9。而在TCC下,你就不将他改为9,而是在冻结库存数上+1,即数据库状态变为 库存数10,冻结库存数1、那么在其他的用户购买时,你返回给前端的库存数应该就是 库存数10 减去 冻结库存数1 等于现在只有9个库存能够被使用。)
Confirm阶段:确认。将库存字段真正减掉,也就是将10库存真正减1。
Cancel阶段:回滚。
如何实现接口幂等性
创建订单时,第一次调用服务超时,再次调用是否产生两笔订单?
订单支付时,服务端扣钱成功,但是接口反馈超时,此时再次调用支付,是否会多扣一笔呢?
服务方需要使用幂等的方式保证一次和多次的请求结果一致!
实验方式:
1.数据库去重表
在往数据库中插入数据的时候,利用数据库唯一索引特性,保证数据唯一。比如订单的流水号,也可以是多个字段的组合。
2.TOKEN机制
针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用Token的机制实现防止重复提交。
主要的流程步骤如下:
- 客户端先发送获取token的请求,服务端会生成一个全局唯一的ID保存在redis中,同时把这个ID返回给客户端。
- 客户端调用业务请求的时候必须携带这个token,一般放在请求头上。
- 服务端会校验这个Token,如果校验成功,则执行业务。
- 如果校验失败,则表示重复操作,直接返回指定的结果给客户端。
通过以上的流程分析,唯一的重点就是这个全局唯一ID如何生成,在分布式服务中往往都会有一个生成全局ID的服务来保证ID的唯一性,但是工程量和实现难度比较大,UUID的数据量相对有些大,此处陈某选择的是雪花算法生成全局唯一ID,不了解雪花算法的读者下一篇文章会着重介绍。
评论区