给自己复盘的tjxt笔记day11第一部分

领取优惠券的优化

分布式锁

集群下的锁失效问题

请求进入了多个JVM,就会出现多个锁对象(用户id对象),自然就有多个锁监视器。此时就会出现每个JVM内部都有一个线程获取锁成功的情况,没有达到互斥的效果,并发安全问题就可能再次发生了

分布式锁

分布式锁必须要满足的特征:

  • 多JVM实例都可以访问

  • 互斥

能满足上述特征的组件有很多,因此实现分布式锁的方式也非常多,例如:

  • 基于MySQL

  • 基于Redis

  • 基于Zookeeper

  • 基于ETCD

但目前使用最广泛的还应该是基于Redis的分布式锁

简单分布式锁

 利用Redis实现的简单分布式锁流程如下

业务代码修改

从原来的乐观锁改为分布锁

分布式锁的问题 

锁误删问题

解决思路:

我们会将持有锁的线程存入lock中。因此,我们应该在删除锁之前判断当前锁的中保存的是否是当前线程标示,如果不是则证明不是自己的锁,则不删除;如果锁标示是当前线程,则可以删除

超时释放问题 

就在线程2获取锁成功后,线程1从阻塞中醒来,继续释放锁。由于在阻塞之前已经完成了锁标示判断,现在就无需判断而是直接删除锁,结果就把线程2的锁删除了

总结一下,误删的原因归根结底是因为什么?

  • 超时释放

  • 判断锁标示、删除锁两个动作不是原子操作

其它问题

除了上述问题以外,分布式锁还会碰到一些其它问题:

  • 锁的重入问题同一个线程多次获取锁的场景,目前不支持,可能会导致死锁

  • 锁失败的重试问题:获取锁失败后要不要重试?目前是直接失败,不支持重试

  • Redis主从的一致性问题:由于主从同步存在延迟,当线程在主节点获取锁后,从节点可能未同步锁信息。如果此时主宕机,会出现锁失效情况。此时会有其它线程也获取锁成功。从而出现并发安全问题。

解决方案

当然,上述问题并非无法解决,只不过会比较麻烦。例如:

  • 原子性问题:可以利用Redis的LUA脚本来编写锁操作,确保原子性

  • 超时问题:利用WatchDog(看门狗)机制,获取锁成功时开启一个定时任务,在锁到期前自动续期,避免超时释放。而当服务宕机后,WatchDog跟着停止运行,不会导致死锁。

  • 锁重入问题:可以模拟Synchronized原理,放弃setnx,而是利用Redis的Hash结构来记录锁的持有者以及重入次数,获取锁时重入次数+1,释放锁是重入次数-1,次数为0则锁删除

  • 主从一致性问题:可以利用Redis官网推荐的RedLock机制来解决

我们通常会使用一些开源框架来实现分布式锁,而不是自己来编码实现。目前对这些解决方案实现的比较完善的一个第三方组件:Redisson

Redisson

在微服务中应用的步骤:

  • 引入tj-common、Redisson依赖

  • 注入RedissonClient,使用分布式锁

业务代码修改

优化-通用分布式锁组件

Redisson的分布式锁使用并不复杂,基本步骤包括:

  • 1)创建锁对象

  • 2)尝试获取锁

  • 3)处理业务

  • 4)释放锁

但是,除了第3步以外,其它都是非业务代码,对业务的侵入较多

可以基于AOP的思想,将业务部分作为切入点,将业务前后的锁操作作为环绕增强

但是,我们该如何标记这些切入点呢?

不是每一个service方法都需要加锁,因此我们不能直接基于类来确定切入点;另外,需要加锁的方法可能也较多,我们不能基于方法名作为切入点,这样太麻烦。因此,最好的办法是把加锁的方法给标记出来,利用标记来确定切入点。如何标记呢?

最常见的办法就是基于注解来标记了。同时,加锁时还有一些参数,比如:锁的key名称、锁的waitTime、releaseTime等等,都可以基于注解来传参。

定义注解

注解本身起到标记作用,同时还要带上锁参数:

  • 锁名称

  • 锁等待时间

  • 锁超时时间

  • 时间单位

定义切面

private final RedissonClient redissonClient;

    @Around("@annotation(myLock)")
    public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable {
        // 1.创建锁对象
        RLock lock = redissonClient.getLock(myLock.name());
        // 2.尝试获取锁
        boolean isLock = lock.tryLock(myLock.waitTime(), myLock.leaseTime(), myLock.unit());
        // 3.判断是否成功
        if(!isLock) {
            // 3.1.失败,快速结束
            throw new BizIllegalException("请求太频繁");
        }
        try {
            // 3.2.成功,执行业务
            return pjp.proceed();
        } finally {
            // 4.释放锁
            lock.unlock();
        }
    }
    

 

Spring中的AOP切面有很多,会按照Order排序,按照Order值从小到大依次执行。Spring事务AOP的order值是Integer.MAX_VALUE,优先级最低。

我们的分布式锁一定要先于事务执行,因此,我们的切面一定要实现Ordered接口,指定order值小于Integer.MAX_VALUE即可。

    @Override
    public int getOrder() {
        return 0;
    }

使用锁

优化-对锁的实现进行优化 

现在还存在几个问题:

  • Redisson中锁的种类有很多,目前的代码中把锁的类型写死了

  • Redisson中获取锁的逻辑有多种,比如获取锁失败的重试策略,目前都没有设置

  • 锁的名称目前是写死的,并不能根据方法参数动态变化

所以呢,我们接下来还要对锁的实现进行优化,注意解决上述问题。

工厂模式切换锁类型

问题:Redisson中锁的种类有很多,目前的代码中把锁的类型写死了

如何让用户选择锁类型呢?

锁的类型虽然有多种,但类型是有限的几种,完全可以通过枚举定义出来。然后把这个枚举作为MyLock注解的参数,交给用户去选择自己要用的类型。

锁类型枚举
public enum MyLockType {
    RE_ENTRANT_LOCK, // 可重入锁
    FAIR_LOCK, // 公平锁
    READ_LOCK, // 读锁
    WRITE_LOCK, // 写锁
    ;
}

然后在自定义注解中添加锁类型这个参数

锁对象工厂
@Component
public class MyLockFactory {

    private final Map<MyLockType, Function<String, RLock>> lockHandlers;

    public MyLockFactory(RedissonClient redissonClient) {
        this.lockHandlers = new EnumMap<>(MyLockType.class);
        this.lockHandlers.put(RE_ENTRANT_LOCK, redissonClient::getLock);
        this.lockHandlers.put(FAIR_LOCK, redissonClient::getFairLock);
        this.lockHandlers.put(READ_LOCK, name -> redissonClient.getReadWriteLock(name).readLock());
        this.lockHandlers.put(WRITE_LOCK, name -> redissonClient.getReadWriteLock(name).writeLock());
    }

    public RLock getLock(MyLockType lockType, String name){
        return lockHandlers.get(lockType).apply(name);
    }
}

改造切面代码

我们将锁对象工厂注入MyLockAspect,然后就可以利用工厂来获取锁对象

在业务中,就能通过注解来指定自己要用的锁类型了

(其实很好理解,注解的参数相当于买家,切面实现相当于商铺中介,商铺发订单给工厂)

锁失败策略

问题:Redisson中获取锁的逻辑有多种,比如获取锁失败的重试策略,目前都没有设置

重试策略 + 失败策略组合,总共以下几种情况:

一种设计模式:策略模式。同时,我们还需要定义一个失败策略的枚举。在MyLock注解中定义这个枚举类型的参数,供用户选择。

在策略比较简单的情况下,我们完全可以用枚举代替策略工厂,简化策略模式,所以实现的思路和前面的锁类型一样

我们定义一个失败策略枚举;

在MyLock注解中添加枚举参数;

修改切面代码,基于用户选择的策略来处理;

我们就可以在使用锁的时候自由选择锁类型、锁策略了;

(写的时候这个代码顺序正好执行的时候是反着的感觉)

基于SPEL的动态锁名

问题:锁的名称目前是写死的,并不能根据方法参数动态变化

Spring中提供了一种表达式语法,称为SPEL表达式,可以执行java代码,获取任意参数。

首先,在使用锁注解时,锁名称可以利用SPEL表达式;

 而如果是通过UserContext.getUser()获取,则可以利用下面的语法:

这里T(类名).方法名()就是调用静态方法。 

  • 9
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值