redission学习记录

前言:最近做的一个接口由于没有实现幂等性,老是会出现重复提交导致数据出错的情况。之前只能依靠接口调用方去做逻辑控制避免这种情况,这次决定使用分布式锁来解决之歌问题,之前学习的时候用的是jedis写分布式锁,但是发现确还有些许缺陷,机缘巧合下得知redission框架封装了分布式锁,不但类型全面,而且使用方便,周六特略微学习了一波,今天则记录下来。附上阿里社区的redission中文版官方文档地址:https://yq.aliyun.com/articles/551423

分布式锁类型

  1. 普通锁 lock:如果多个线程竞争锁,获取不到锁的线程就会一直死循环不断去获取。
  2. 可重入锁 tryLock : 多个线程竞争锁,获取不到锁的线程会间隔一定时间重试几次,获取不到就返回false,获取到了返回true
  3. 联锁 MultiLock:多把锁作为一组锁使用,同时锁住,同时释放
  4. 红锁 RedissonRedLock :用于redis分布式,通过算法,获取大多数节点的锁住之后才标识为锁住。
  5. 读写锁 RReadWriteLock:可以控制redis数据是否可读或可写
  6. 闭锁

redis客户端类型

  1. 集群模式
  2. 单机模式
  3. 云托管模式
  4. 单机模式
  5. 哨兵模式
  6. 主从模式

实操源码

说明: 这里举例的是我写的一个demo关于解决重复提交的例子,用比较简单的可重入锁加单机模式的形式实现的。

redissionClient配置

    public RedissonClient redissionClient(RedisProperties redisProperties) {
        Config config = new Config();

        config
                //看门狗超时时间(续期时间为三分之一)
                .setLockWatchdogTimeout(5*3*1000)
                //单机模式
                .useSingleServer()
                //redis地址
                .setAddress("redis://"+redisProperties.getHost() + ":" + redisProperties.getPort())
                //redis密码
                .setPassword(redisProperties.getPassword())
                //最小空闲数
                .setConnectionMinimumIdleSize(redisProperties.getLettuce().getPool().getMinIdle())
                //最大连接数量
                .setConnectionPoolSize(redisProperties.getLettuce().getPool().getMaxActive());

        //重试次数,重试间隔 采用默认值
        return Redisson.create(config);
    }

标识注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface NoRepeatSubmit {
    //过期时间 秒为单位
    long ttl() default 5L;
}

拦截切面

@Aspect
@Component
public class RepeatSubmitAspect {


    @Autowired
    private RedissonClient redissonClient;

    @Pointcut("@annotation(com.tensquare.redislock.anno.NoRepeatSubmit)")
    public void pointCut(){

    }
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Logger logger = LoggerFactory.getLogger(this.getClass());
        //生产的话需要从theadLocal里面获取用户信息,与request请求,request用来获取请求路径,这里用方法名与写死的userId代替
        String userId="111";
        String methodName = joinPoint.getSignature().getName();
        String key = userId+"_"+methodName;
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        NoRepeatSubmit noRepeatSubmit = signature.getMethod().getAnnotation(NoRepeatSubmit.class);
        long ttl = noRepeatSubmit.ttl();
        //获取锁对象
        RLock lock = redissonClient.getLock(key);
        if(lock.tryLock(1,ttl, TimeUnit.SECONDS)){
            Object o=null;
            try {
                o = joinPoint.proceed();
                return o;
            }catch (Exception e){
                throw  e;
            }finally {
                try {
                    lock.unlock();
                }catch (Exception e){
                    logger.error("释放锁出错",e);
                    throw new RuntimeException("释放锁出错");
                }
            }

        }else {
            logger.info("未获取到锁"+key);
            throw new RuntimeException("重复提交,拒绝请求") ;
        }
    }
}

请求controller

@RestController
public class LockTestController {


    @RequestMapping(value = "/lockTest")
    @NoRepeatSubmit(ttl = 21)
    public String lockTest(){
        long startTime = System.currentTimeMillis();
        while (System.currentTimeMillis()-startTime<40*1000){

        }
        return "请求成功";
    }
}

案例说明

该案例通过注解 @NoRepeatSubmit(ttl = 21)去标识锁定该方法(可以自己去扩展实现锁一条数据或一张业务表),过期时间设为21s,该方法运行最少需要40s,所以unlock过程中会报锁不存在的错。
通过解读源码发现,如果lock.tryLock方法用户提供了过期时间,则redission不会开启另一个线程来作为看门狗(每隔三分之一的过期时间,会自动续期),所以需要将 if(lock.tryLock(1,ttl, TimeUnit.SECONDS)) 改为 if(lock.tryLock(1,TimeUnit.SECONDS)),则该方法跑完之前就算看门狗的超时lockWatchdogTimeout只有十五秒,在释放锁的时候也不会出现锁不存在的情况;

源码分析

  1. //获取锁对象 RLock lock = redissonClient.getLock(key);
    public RLock getLock(String name) {
    	//new一个锁
        return new RedissonLock(this.connectionManager.getCommandExecutor(), name);
    }
    public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        //异步命令执行器
        this.commandExecutor = commandExecutor;
        //线程id
        this.id = commandExecutor.getConnectionManager().getId();
        //默认过期时间
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
       
        this.entryName = this.id + ":" + name;
        //消息订阅  貌似是多个线程竞争锁的时候用到的 没细看
        this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
    }
  1. lock.tryLock(1,ttl, TimeUnit.SECONDS)
  public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
       *********
        Long ttl = this.tryAcquire(leaseTime, unit, threadId);
        //如果获取到了就返回
        if (ttl == null) {
            return true;
        } else {
        //如果没有获取到就经过一定的时间 在一定的重试次数内反复获取
           *********
           }
    }
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
        return (Long)this.get(this.tryAcquireAsync(leaseTime, unit, threadId));
    }
  private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
  		//如果给了过期时间
        if (leaseTime != -1L) {
            return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
        //如果没给过期时间就使用LockWatchdogTimeout作为过期时间,并启动另一个线程去监听,当方法运行完之前不断的续期
            RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
            ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                if (e == null) {
                    if (ttlRemaining == null) {
                        this.scheduleExpirationRenewal(threadId);
                    }

                }
            });
            return ttlRemainingFuture;
        }
    }
//执行lua脚本去获取锁(lua脚本可保证原子性)
    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        this.internalLockLeaseTime = unit.toMillis(leaseTime);
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command,
         "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; 
         if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;
          return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
    }

以下是看门狗定时器续期的代码

    private void scheduleExpirationRenewal(long threadId) {
        RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
        RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
       //如果已存在对当前锁的监听续期任务就把当前线程id加入进去
        if (oldEntry != null) {
            oldEntry.addThreadId(threadId);
        } else {
        //如果不存在对当前锁的续期任务就新建一个
            entry.addThreadId(threadId);
            this.renewExpiration();
        }

    }
    private void renewExpiration() {
        RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (ee != null) {
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
                    if (ent != null) {
                        Long threadId = ent.getFirstThreadId();
                        if (threadId != null) {
                            RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
                            future.onComplete((res, e) -> {
                                if (e != null) {
                                    RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
                                } else {
                                    if (res) {
                                        RedissonLock.this.renewExpiration();
                                    }

                                }
                            });
                        }
                    }
                }
                //每隔三分之一超时时间续期一次
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            ee.setTimeout(task);
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值