分布式锁实践

需求:

多用户评论增加楼层

现状:

使用了synchronized本地锁。本地锁已经优化,细化过后的锁

    public String commitTopicReply(ArTopicReplayModel TopicReplayModel){
    
    

        // 创建插入数据库需要的实体
        TopicReplyRecord topicReplyRecord = new TopicReplyRecord();
        topicReplyRecord.setActive_id(BigInteger.valueOf(Long.parseLong(TopicReplayModel.getActiveId())));

        // 获取传入的用户手机号
        String userId = TopicReplayModel.getUserId();

        // 获取回复的文本内容
        String content = URLDecoder.decode(TopicReplayModel.getContent());
        if (content.equals("") && TopicReplayModel.getFiles_attr().equals("")) {
    
    
            return null;
        }

        // 存储上传的图片
        List<String> urlList = new ArrayList<>();
        // 用分号分隔图片或文件的url地址
        String[] branch = TopicReplayModel.getFiles_attr().split(";");

        for (String s : branch) {
    
    
            urlList.add(s);
        }
        // 保存到资源记录表并返回资源组id
   
        // 插入url地址到资源记录表中

        // 插入资源组id到回复记录表中

        // 插入到主题讨论回复接口中
        int insertCount = saveTopicReplyRecord(topicReplyRecord,TopicReplayModel);
        if (insertCount < 0) {
    
    
            return null;
        }
        return String.valueOf(insertCount);
    }


    private synchronized int saveTopicReplyRecord(TopicReplyRecord topicReplyRecord,ArTopicReplayModel TopicReplayModel){
    
    
        if (StringUtils.isEmpty(TopicReplayModel.getReplyId())) {
    
    
            String key=CommonConfigurationUtils.NUM_OF_FLOORS+topicReplyRecord.getActive_id();
            // 获取楼层数,本身increment就是原子性的
            Long fool = redisTemplate.opsForValue().increment(key, 1);
            topicReplyRecord.setReply_floor("第" + fool + "楼");
        }
        return topicReplyRecordMapper.insertTopicReplyRecord(topicReplyRecord);
    }

解决办法:

使用分布式锁,Redisson的解决方案

使用步骤

Redisson:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

1.引入依赖

<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.15.5</version>
        </dependency>

2.创建配置类

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Redisson的配置类
 *
 * @author Promsing(张有博)
 * @version 1.0.0
 * @since 2022/9/17 - 8:46
 */
@Configuration
public class MyRedisConfig {
    
    

    /**
     * 对redisson的使用通过RedissonClient对象
     */
    @Bean
    public RedissonClient redisson(){
    
    
        //创建配置
        Config config = new Config();

        //集群模式
        //config.useClusterServers().addNodeAddress("127.0.0.1:6379","127.0.0.1:6380");

        //单机模式
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        config.useSingleServer().setDatabase(1);
        //config.useSingleServer().setUsername().setPassword();//账号密码
        //config.useSingleServer().setConnectionMinimumIdleSize()//连接池最小空闲连接数
        //config.useSingleServer().setConnectionPoolSize()//连接池最大线程数
        //等等

        return Redisson.create(config);

       /* public static RedissonClient create() {
           Config config = new Config();
           config.useSingleServer().setAddress("redis://127.0.0.1:6379");
           return create(config);
        }*/
    }
}


3.使用lock.lock() lock.ulock()

//测试可重入
private void entry(){
    
    
    RLock lock = redissonClient.getLock("lock-park");
    lock.lock();
    System.out.println("可重入");
    //lock.unlock();
}

4.分析:

使用默认的lock方法,不但会加锁,还支持可重入与看门狗机制
监控锁的看门狗超时时间单位为毫秒。该参数只适用于分布式锁的加锁请求中未明确使用leaseTimeout参数的情况。
如果该看门口未使用lockWatchdogTimeout去重新调整一个分布式锁的lockWatchdogTimeout超时,那么这个锁将变为失效状态。这个参数可以用来避免由Redisson客户端节点宕机或其他原因造成死锁的情况。
验证可重入:
redisson 中的锁,底层是一个HASH结构。uuid为key ,value为重入次数

验证自动续期:
只要占锁成功,就会启动一个定时任务:每隔 10 秒重新给锁设置过期的时间,过期时间为 30 秒。

部分源码解析

加锁时:Lua脚本

    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    
    
        return this.evalWriteAsync(this.getRawName(), 
                                   LongCodec.INSTANCE, command, 
                                   "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', 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.getRawName()),
                                   new Object[]{
    
    unit.toMillis(leaseTime),
                                                this.getLockName(threadId)
                                                }
                                  );
    }

异步任务定时器

    private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    
    
        RFuture ttlRemainingFuture;
        if (leaseTime != -1L) {
    
    
            //加锁
            ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        } else {
    
    
            ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        }

        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
    
    
            if (e == null) {
    
    
                if (ttlRemaining) {
    
    
                    if (leaseTime != -1L) {
    
    
                        this.internalLockLeaseTime = unit.toMillis(leaseTime);
                    } else {
    
    
                        //起一个定时计时器
                        this.scheduleExpirationRenewal(threadId);
                    }
                }

            }
        });
        return ttlRemainingFuture;
    }

protected void scheduleExpirationRenewal(long threadId) {
    
    
        RedissonBaseLock.ExpirationEntry entry = new RedissonBaseLock.ExpirationEntry();
        RedissonBaseLock.ExpirationEntry oldEntry = (RedissonBaseLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
        if (oldEntry != null) {
    
    
            oldEntry.addThreadId(threadId);
        } else {
    
    
            entry.addThreadId(threadId);
            this.renewExpiration();
        }

    }


private void renewExpiration() {
    
    
    RedissonBaseLock.ExpirationEntry ee = (RedissonBaseLock.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 {
    
    
                RedissonBaseLock.ExpirationEntry ent = (RedissonBaseLock.ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
                if (ent != null) {
    
    
                    Long threadId = ent.getFirstThreadId();
                    if (threadId != null) {
    
    
                        RFuture<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
                        future.onComplete((res, e) -> {
    
    
                            if (e != null) {
    
    
                                RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);
                                RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
                            } else {
    
    
                                if (res) {
    
    
                                    RedissonBaseLock.this.renewExpiration();
                                }

                            }
                        });
                    }
                }
            }
        }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
        ee.setTimeout(task);
    }
    }

解锁时:Lua脚本

  protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    
    
        return this.evalWriteAsync(this.getRawName(),
                                   LongCodec.INSTANCE,
                                   RedisCommands.EVAL_BOOLEAN, 
                                   "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;",
                                   Arrays.asList(this.getRawName(), this.getChannelName()),
                                   new Object[]{
    
    LockPubSub.UNLOCK_MESSAGE, 
                                                this.internalLockLeaseTime,
                                                this.getLockName(threadId)});
    }

完整测试代码

    /**
     * 简单加锁过程
     *
     * @return
     */
    @GetMapping("floor")
    public String floor() {
    
    

        Long increment = null;
        //只要占锁成功,就会启动一个定时任务:每隔 10 秒重新给锁设置过期的时间,过期时间为 30 秒。
       /* LockSupport.park();
        LockSupport.unpark(null);*/
        //获取锁
        RLock lock = redissonClient.getLock("lock-park");
        Object floor = null;
        try {
    
    

            //2.加锁,默认30s
            //lock.lock(2, TimeUnit.MINUTES);
            lock.lock();
            System.out.println("加锁成功,线程ID" + Thread.currentThread().getId());
            System.out.println("加锁成功,线程Name" + Thread.currentThread().getName());

            Random random = new Random();
            final int i = random.nextInt(list.size());

            //自增:incrby
            //自减:decrby
            //此方法会先检查key是否存在,存在+1,不存在先初始化,再+1
            floor = list.get(0);
            increment = redisTemplate.opsForValue().increment(floor, 1);

           /* for (int j = 0; j < 10; j++) {
                entry();
            }*/
            Thread.sleep(500);//休眠30s
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock();
            System.out.println("解锁成功,线程Name" + Thread.currentThread().getName());

        }
        System.out.println("楼层" + floor + "已经加到" + increment.toString());
        return increment.toString();
    }

    //测试可重入
    private void entry() {
    
    
        RLock lock = redissonClient.getLock("lock-park");
        lock.lock();
        //System.out.println("可重入");
        lock.unlock();
    }
    //测试看门狗机制

image.png

猜你喜欢

转载自blog.csdn.net/promsing/article/details/129211781