侧边栏壁纸
博主头像
lmg博主等级

  • 累计撰写 55 篇文章
  • 累计创建 6 个标签
  • 累计收到 2 条评论
标签搜索

分布式锁与缓存

lmg
lmg
2020-05-07 / 0 评论 / 0 点赞 / 564 阅读 / 6,258 字
温馨提示:
本文最后更新于 2022-04-16,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

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个或多个副本。每个节点托管一个或多个分片,并充当协调器将操作委托给正确的分片。再平衡和路由是自动完成的。“相关数据通常存储在同一个索引中,该索引由一个或多个主分片和零个或多个复制分片组成。一旦创建了索引,就不能更改主分片的数量。

优点:能够达到实时搜索,稳定,可靠,快速,安装使用方便
缺点:也有很多,自己查

redis和ElasticSearch的区别

分布式事务

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,不了解雪花算法的读者下一篇文章会着重介绍。

0

评论区