分布式ID,分布式锁?看完不懂回家种田

分布式ID

1.分布式ID应用场景

关键字:全局唯一ID

​ 某一张表的数据量很大,影响读写效率,为了解决这样的问题,我们可以将一个大的数据库拆分成多个小的数据库(分库分表)(主从、读写分离),但是随着数据流的不断增多,分到每一个数据库的数据也会越来越多,此时响应的速度就达不到要求了。

​ 此时我们可以将每个数据库拆分成多个表(例如10亿数据,拆分成100个表,这样每个表的数据就只有1000万)。经过这样的拆分,每个表的唯一的行记录,总是需要一个唯一标识的(也就是主键),此时,该主键若是用数据库的自增ID,显然是不能满足要求的:表A中主键有1、2、3,表B中也会出现主键1、2、3的数据,这样一来,就无法确定行数据的唯一性。

​ 基于上述背景,一个能够生成全局唯一ID的系统是非常必要的,表A中数据的ID是1、2、3,表B中数据的ID是4、5、6。这样的全局唯一的ID就称之为分布式ID

2.分布式ID的条件

  1. 全局唯一
  2. 高性能
    • 生成ID请求的响应速度必须要快。
  3. 高可用
    • 尽量的让我们的分布式ID都能用。
  4. 易用
    • 秉承着使用难度低的原则,其他人上手即用。
  5. 趋势递增

3.分布式ID的生成方案

1.UUID方案

关键字:无序,无业务含义,字符串,本地生成

public class TestUUID{
    public static void main(String[] args){
        String uuid=UUID.randomUUID().toString();
    }
}

UUID是生成一个完全随机的无序字符串(甚至是全球唯一)完全没有具体的含义。但其实,UUID并不适用于实际的需求

  • 生成订单号时,订单号是要符合一定的规律的(地区、时间、厂家),此时若使用UUID来当作订单号,意义不大
  • 作为数据库业务主键时候,UUID过长且是一个字符串,导致存储性能差,查询时间长,无意义的字符串不具备趋势递增的特性,且UUID的无序性,可能导致数据位置的变动。导致不推荐,为什么是不推荐?因为UUID作为数据库业务主键的话,是本地生成的,无网络消耗
    • Mysql官方建议:主键越短越好

2.基于数据库自增ID

关键字:基于独立的一个数据库,访问量突增易宕机

​ 这里的数据库自增ID不是基于每张表的,而是基于一个独立的数据库,用这个数据库(记作A)来生成ID。当某个数据库的某个表新增了一条数据需要ID的时候,请求数据库A来得到一个ID,以此往复。

​ 当访问量很大的时候,有可能会出现数据库A宕机的情景,这样会导致其他数据库无法运行。

3.基于数据库集群模式

关键字:解决DB单点故障,不易扩展

​ 该模式是基于单点数据库作为自增ID时候出现的单点故障进行优化而产生的模式

​ 我们将生成ID的mysql数据库做成主从集群的模式或者双主模式。两个Mysql的实例都能单独的生成自增ID,那么如何保证两个数据库的ID不会重复呢?

  • 根据数据库的数量设置起始值和自增的步长。数据库A设置起始值为1,步长为2;数据库B设置起始值为2,步长为2。
    • 对于已经投入使用的数据库,还需要再新增一个ID数据库时,无论怎么设置起始值都会导致重复,所以,我们在最开始设计的时候,数据库之间的ID起始值间距设置大一些

4.基于数据库号段模式

关键字:减少数据库访问的次数

​ 该模式也是当下主流的分布式ID生成模式。从数据库批量的获取自增ID每次从数据库取出一个号段的范围,例如:某个服务需要生成ID,先从数据库里拿上100或1000个ID回来,具体使用从拿回来的ID里分配。该模式生成ID的时候不会强依赖于数据库,不会去频繁的访问ID数据库。

在ID数据库中的一个表存上几个字段

  • 业务类型:用户/订单………

  • maxID:最大的号段ID是多少。

  • step:步长,号段的长度(该号段一共多少个)

  • version:乐观锁的方式避免号段重复请求

    ​ 一条语句请求的maxID是1000,step也是1000,那么1-1000的数据就会返回回去。至于是否用得完,业务不关心

5.Redis模式

关键字:redis递增命令,持久化策略(数据重复,重启恢复时间长)

利用Redis的递增命令,实现key-value中value的递增,并且redis的单线程的原子操作,可以保证redis的唯一性。

需要注意redis持久化的问题:RDB、AOF

  • 在持久化策略为RDB的情况下,可能会导致数据重复。例如:请求增了100个id,在存到51个的时候redis刚好触发快照,也就是当时只持久化了50条数据,在下一次快照之前redis宕机了,就会导致剩余的50个id没有被持久化,在启动的时候又会从51开始,导致id重复。
  • 在持久化策略为AOF的情况下,可能会导致Redis重启恢复的时候消耗过长,这是由于递增命令的特殊性(需要计算)

6.雪花算法模式

关键字:减少网络请求的开销

在这里插入图片描述

​ 一大长串的数字,各个位数有各自特定的意义。

只要机器确定了之后,是不需要单独搭建一个分布式ID的数据库的。减少了网络请求的开销

分布式锁

1.分布式锁的应用场景

关键字:超卖,多个请求进入同一个业务逻辑

  • 电商业务中的下单:多个人抢固定数量的商品。

​ 在真实的项目中,为了实现高可用,一般会构建多个Server服务器,当用户发起请求的时候,用户的请求会同时进入到这两个Server当中(以下记作A、B)。如果商品的库存只有一份商品,那么一个商品就会产生两次扣减请求,此时商品的数量为-1,这就是常说的超卖

  • 在打车的业务场景中,多个司机抢同一个订单的情况下,需要对订单的状态加以控制(不会出现司机C与司机D同时抢到了同一个订单的情况)

2.预防超卖业务实现

1.架构

关键词:负载均衡,中台,共有服务

在这里插入图片描述

  • 服务A,一般是以api-xxx开头。、
    • 接受用户的请求。具体的实现是通过服务B实现
  • 服务B,一般是以service-xxx开头。
    • 实现具体的业务

当服务B是以集群的形式搭建的时候,服务A也可以同时起到一个负载均衡的效果

很多公司会把底层的服务B称作为基础能力抽象成中台(业务中台、数据中台),也叫做共用业务,谁要用,谁就来调用

2.单体服务超卖问题

关键词:单体服务,intern()方法,synchronized

在这里插入图片描述

  • String的intern()方法:返回字符串池中的一个相同内容的实例引用。如果字符串池中没有相同内容的字符串,就把当前字符串内容放到池中并返回这个字符串在池中的地址。
String s1="xxx";//从字符串常量池中创建xxx,并且返回地址
String s2=new String("xxx")//从堆中创建"xxx"并且返回地址

​ 在synchronized加锁的时候,希望锁住的对象是同一个,刚好符合String常量池的概念,只要创建String对象时去常量池找,就可以保证对象的唯一性。所以我们通常使用String的intern()方法。

  • 由于synchronized本身的特性,这样做很有可能会影响服务整体的速度,该方法也是在单体应用的情况下才会存在。

3.集群下解决超卖问题

关键字:集群,锁互不相认,分布式锁,第三方加锁

在这里插入图片描述

​ 在大多数场景中,是不会出现上述的单体应用去处理服务的情况的,因为一旦单体应用服务挂掉了,可能会导致整个服务的瘫痪,所以应用基本上都是集群部署的。

​ 在集群模式中,我们有两个服务,但是仍然会出现超卖的问题,归根结底的原因是:这两个服务中的锁互不认识。为了解决这一问题,就出现了分布式锁

​ 所谓分布式锁,其实就是第三方加锁,当服务A收到请求之后,先去第三方加锁,拿到锁之后,开始执行业务逻辑,执行完毕之后释放锁,至此服务B才可以尝试去获取锁。

3.分布式锁的方案

1.mysql方案

关键字:主键,唯一索引,磁盘IO

​ 当服务A执行业务逻辑的时候,往数据库插入一条主键为1的数据或者唯一索引为一的数据。这样在服务B想要去执行逻辑的时候,由于互斥性无法插入数据,也就达到了锁的效果。当服务A执行完毕后删除该记录,也就是释放锁,至此服务B才可以继续执行。

​ 这样加锁的弊端在于频繁的对数据库进行操作,可能会导致数据库宕机。其实mysql是基于磁盘的IO进行的

public class MysqlLock implements Lock {

    @Autowired
    private TblOrderLockDao mapper;
    
    private ThreadLocal<TblOrderLock> orderLockThreadLocal ;

    @Override
    public void lock() {
        // 1、尝试加锁
        if(tryLock()) {
            System.out.println("尝试加锁");
            return;
        }
        // 2.休眠
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 3.递归再次调用
        lock();
    }
    
    /**
     *  非阻塞式加锁,成功,就成功,失败就失败。直接返回
     */
    @Override
    public boolean tryLock() {
        try {
            TblOrderLock tblOrderLock = orderLockThreadLocal.get();
            mapper.insertSelective(tblOrderLock);
            System.out.println("加锁对象:"+orderLockThreadLocal.get());
            return true;
        }catch (Exception e) {
            return false;
        }
        
        
    }
    
    @Override
    public void unlock() {
        mapper.deleteByPrimaryKey(orderLockThreadLocal.get().getOrderId());
        System.out.println("解锁对象:"+orderLockThreadLocal.get());
        orderLockThreadLocal.remove();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        // TODO Auto-generated method stub
        
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        // TODO Auto-generated method stub
        return false;
    }


    @Override
    public Condition newCondition() {
        // TODO Auto-generated method stub
        return null;
    }

}

2.Redis单节点方案

关键字:内存IO,setnx,死锁,超时时间(原子操作),WatchDog

​ 利用Redis中的SETNX命令

加锁:setnx [key] [value]
将key的值设置为value,当且仅当key不存在。若key存在,则不会进行任何操作。

释放锁:del key
2.1 Redis分布式锁的死锁问题以及解决方案

​ 有一种特殊的情况,在服务A获取锁之后,执行业务逻辑的途中(释放锁之前),服务A挂掉了,这样的话key就会一直存在于业务当中,导致后续的服务B一直无法获取到锁。这就是Redis分布式锁的死锁情况。

​ 解决办法也很简单,就是给key设置一个过期时间,且必须和setnx命令一同设置(不可单独设置)。保证操作的原子性

​ 在setnx操作的时候设置过期时间,又可能会由于过期时间过早导致新的问题:

  • 过期时间过早服务A总执行时间为14秒,但是key的超时时间为10秒,此时由于key已经不存在了,导致服务B获取到了锁。
    • 解决方案:
      • 锁续期:当key又即将释放的时候,服务A还未执行完,就对key进行一个续期操作(WatchDog)。
  • 释放了别人的锁:如上B获取锁之后执行,服务A执行完毕后删除的锁的key是服务B创建的,以此往复。
    • 解决方案
      • setnx key value的时候value保证特有性,当删除锁的时候去判断value是否是自己特有的value,如果是再去删除。
2.2 利用分段锁提升Redis锁的性能

关键字:分段锁

​ 在执行setnx操作的时候,key值进行分段操作。例如id-1,id-2,这样就可以有两把锁了。

3. Redis集群加锁

关键字:实时性,红锁,宕机不立即重启

背景:一主二从的三哨兵模式。

​ 在集群模式下可能会出现一个问题:当服务A执行setnx命令成功之后,主Redis挂掉了,此时主节点数据还未同步给从节点,导致从节点没有服务Asetnx的key值,此时服务B就可以获取到锁去执行业务流程了。仍然会出现多个服务执行同一个方法的问题出现。该问题出现的原因:Redis节点数据的同步不是实时的。

​ 为了解决上述的问题,可以让Redis集群之间不做数据的同步,启动多台Redis,并且他们都是独立的,于是:红锁方案诞生了

4 红锁

关键字:单数Redis服务器,超过半数

在这里插入图片描述

​ 启动单数台的Redis,业务先去R1加锁,成功后去R2加锁,以此类推,直至超过了半数的Redis都加锁成功,则认为业务加锁成功,所以一定要是单数台的Redis。

​ 如果其中某一台服务器挂掉了,没关系,只要加锁成功的Redis数量超过了启动时Redis数量的一半,即可加锁超过。

4.1 红锁问题

​ 有一种特殊情况,假设Redis部署了三台R1,R2,R3。

业务请求A请求进行加锁,R1加锁成功,R2也加锁成功,业务A拿到做,执行业务逻辑。

此时R2服务器挂掉了,并且立即重启

业务请求B请求进行加锁,R1中已经存在key,在R1中加锁失败,R2中由于重启了,没有业务A存留下来的key,加锁成功,R3加锁成功,这样业务B也会拿到锁,去执行业务逻辑。最终导致业务出现错误。

这是一种很特殊的情况,解决办法就是部署Redis的时候,要求宕机后不立即进行重启即可

5. Redis终极问题

关键字:FullGC

在这里插入图片描述

​ 服务A有JVM,服务B也有JVM。

当服务A中拿到锁后正在执行业务逻辑的时候,服务A的JVM触发了FullGC,导致服务A处于停滞状态,此时服务A的WatchDog自然就不会去Redis进行续期操作

此时服务B登场了,拿到了锁执行了业务逻辑,导致业务逻辑的错误。

5.1 zookeeper解决Redis终极问题

关键字:顺序节点,临时节点

利用ZK做锁,需要用到其两个特性

  • **顺序节点:**第一次加锁的节点是1号节点,第二次来加锁的节点是2号节点,依次递增。
  • **临时节点:**若程序与ZK构建了网络连接,那么节点就会一直存在,若网络连接断开,则不会存在。

客户端A创建节点my_lock , 创建节点成功xxx-00001,然后判断当前节点是不是my_lock下的第一个节点,是的,所以加锁成功

客户端B创建节点my_lock ,因为是有序的,所以节点是xxx-00002,然后判断当前节点是不是my_lock下的第一个节点,显然不是,对当前节点的上一个节点(xxx-00001)添加一个监听器

客户端A使用完后,释放锁,删除节点xxx-00001,ZK会负责通知监听这个节点的监听器,也就是客户端B的监听器说锁释放了。

客户端B开始加锁,重复上面的操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值