轻量级分布式锁ShedLock加锁机制

springboot整合ShedLock

  1. 引入依赖

    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-spring</artifactId>
        <version>3.0.1</version>
    </dependency>
    
    <dependency>
        <groupId>net.javacrumbs.shedlock</groupId>
        <artifactId>shedlock-provider-jdbc-template</artifactId>
        <version>3.0.1</version>
    </dependency>
    
  2. 建表(mysql)
    Shedlock默认表名shedlock

    CREATE TABLE shedlock(
    	name VARCHAR(64) NOT NULL, 
    	lock_until TIMESTAMP(3) NOT NULL,
    	locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 
    	locked_by VARCHAR(255) NOT NULL, 
    	PRIMARY KEY (name)
    );
    
  3. 添加配置类

    // 标识该类为配置类
    @Configuration
    // //开启定时器
    // @EnableScheduling
    // 开启任务锁,使用方法代理拦截模式,指定一个默认的锁的时间30秒
    @EnableSchedulerLock(interceptMode = EnableSchedulerLock.InterceptMode.PROXY_METHOD, defaultLockAtMostFor = "PT30S")
    public class ShedlockJdbcConfig {
    
        /**
         * 配置锁的提供者
         */
        @Bean
        public LockProvider lockProvider(DataSource dataSource) {
            return new JdbcTemplateLockProvider(
                    JdbcTemplateLockProvider.Configuration.builder()
                            .withJdbcTemplate(new JdbcTemplate(dataSource))
                            .withTimeZone(TimeZone.getTimeZone("GMT+8"))
                            .build()
            );
        }
    }
    
  4. 添加注解

    @Service
    @Slf4j
    public class ImportFileService {
    
    //    @Scheduled(cron = "0/5 * *  * * ? ")
        @SchedulerLock(name = "importFile", lockAtMostFor = 120*1000, lockAtLeastFor = 60*1000)
        public void importFile() {
            log.info("importing...");
            try {
                TimeUnit.SECONDS.sleep(60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("import finished");
        }
    }
    

加锁机制

DefaultLockingTaskExecutor

  1. 任务执行前加锁
  2. 执行任务
  3. 任务完成后解锁
public class DefaultLockingTaskExecutor implements LockingTaskExecutor {
    private static final Logger logger = LoggerFactory.getLogger(DefaultLockingTaskExecutor.class);
    @NotNull
    private final LockProvider lockProvider;

    public DefaultLockingTaskExecutor(@NotNull LockProvider lockProvider) {
        this.lockProvider = requireNonNull(lockProvider);
    }

    @Override
    public void executeWithLock(@NotNull Runnable task, @NotNull LockConfiguration lockConfig) {
        try {
            executeWithLock((Task) task::run, lockConfig);
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable throwable) {
            // Should not happen
            throw new IllegalStateException(throwable);
        }
    }

    @Override
    public void executeWithLock(@NotNull Task task, @NotNull LockConfiguration lockConfig) throws Throwable {
        Optional<SimpleLock> lock = lockProvider.lock(lockConfig);
        if (lock.isPresent()) {
            try {
                logger.debug("Locked '{}', lock will be held at most until {}", lockConfig.getName(), lockConfig.getLockAtMostUntil());
                task.call();
            } finally {
                lock.get().unlock();
                if (logger.isDebugEnabled()) {
                    Instant lockAtLeastUntil = lockConfig.getLockAtLeastUntil();
                    Instant now = Instant.now();
                    if (lockAtLeastUntil.isAfter(now)) {
                        logger.debug("Task finished, lock '{}' will be released at {}", lockConfig.getName(), lockAtLeastUntil);
                    } else {
                        logger.debug("Task finished, lock '{}' released", lockConfig.getName());
                    }
                }
            }
        } else {
            logger.debug("Not executing '{}'. It's locked.", lockConfig.getName());
        }
    }
}

StorageBasedLockProvider

重点关注 doLock() 方法

  1. 获取锁的键名
  2. 判断缓存中(Set集合)是否包含键名
  3. 缓存中没有的话,就去数据库插入一条记录,并将键名加入缓存中
  4. 缓存中存在,就更新记录(关注更新的where条件)
public class StorageBasedLockProvider implements LockProvider {
    @NotNull
    private final StorageAccessor storageAccessor;
    private final LockRecordRegistry lockRecordRegistry = new LockRecordRegistry();

    protected StorageBasedLockProvider(@NotNull StorageAccessor storageAccessor) {
        this.storageAccessor = storageAccessor;
    }

    /**
     * Clears cache of existing lock records.
     */
    public void clearCache() {
        lockRecordRegistry.clear();
    }

    @Override
    @NotNull
    public Optional<SimpleLock> lock(@NotNull LockConfiguration lockConfiguration) {
        boolean lockObtained = doLock(lockConfiguration);
        if (lockObtained) {
            return Optional.of(new StorageLock(lockConfiguration, storageAccessor));
        } else {
            return Optional.empty();
        }
    }

    /**
     * Sets lockUntil according to LockConfiguration if current lockUntil &lt;= now
     */
    protected boolean doLock(LockConfiguration lockConfiguration) {
        String name = lockConfiguration.getName();

        if (!lockRecordRegistry.lockRecordRecentlyCreated(name)) {
            // create record in case it does not exist yet
            if (storageAccessor.insertRecord(lockConfiguration)) {
                lockRecordRegistry.addLockRecord(name);
                // we were able to create the record, we have the lock
                return true;
            }
            // we were not able to create the record, it already exists, let's put it to the cache so we do not try again
            lockRecordRegistry.addLockRecord(name);
        }

        // let's try to update the record, if successful, we have the lock
        return storageAccessor.updateRecord(lockConfiguration);
    }

    private static class StorageLock extends AbstractSimpleLock {
        private final StorageAccessor storageAccessor;

        StorageLock(LockConfiguration lockConfiguration, StorageAccessor storageAccessor) {
            super(lockConfiguration);
            this.storageAccessor = storageAccessor;
        }

        @Override
        public void doUnlock() {
            storageAccessor.unlock(lockConfiguration);
        }

        @Override
        public Optional<SimpleLock> doExtend(LockConfiguration newConfig) {
            if (storageAccessor.extend(newConfig)) {
                return Optional.of(new StorageLock(newConfig, storageAccessor));
            } else {
                return Optional.empty();
            }
        }
    }

}

LockRecordRegistry

class LockRecordRegistry {
    private final Set<String> lockRecords = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));

    public void addLockRecord(String lockName) {
        lockRecords.add(lockName);
    }

    public boolean lockRecordRecentlyCreated(String lockName) {
        return lockRecords.contains(lockName);
    }

    int getSize() {
        return lockRecords.size();
    }

    public void clear() {
        lockRecords.clear();
    }
}

JdbcTemplateStorageAccessor

class JdbcTemplateStorageAccessor extends AbstractStorageAccessor {
    private final String tableName;
    private final JdbcTemplate jdbcTemplate;
    private final TransactionTemplate transactionTemplate;
    private final TimeZone timeZone;

    JdbcTemplateStorageAccessor(@NotNull Configuration configuration) {
        this.jdbcTemplate = requireNonNull(configuration.getJdbcTemplate(), "jdbcTemplate can not be null");
        this.tableName = requireNonNull(configuration.getTableName(), "tableName can not be null");
        PlatformTransactionManager transactionManager = configuration.getTransactionManager() != null ?
            configuration.getTransactionManager() :
            new DataSourceTransactionManager(jdbcTemplate.getDataSource());

        this.transactionTemplate = new TransactionTemplate(transactionManager);
        this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        this.timeZone = configuration.getTimeZone();
    }

    @Override
    public boolean insertRecord(@NotNull LockConfiguration lockConfiguration) {
        String sql = "INSERT INTO " + tableName + "(name, lock_until, locked_at, locked_by) VALUES(?, ?, ?, ?)";
        return transactionTemplate.execute(status -> {
            try {
                int insertedRows = jdbcTemplate.update(sql, preparedStatement -> {
                    preparedStatement.setString(1, lockConfiguration.getName());
                    setTimestamp(preparedStatement, 2, lockConfiguration.getLockAtMostUntil());
                    setTimestamp(preparedStatement, 3, Instant.now());
                    preparedStatement.setString(4, getHostname());
                });
                return insertedRows > 0;
            } catch (DuplicateKeyException e) {
                return false;
            } catch (DataIntegrityViolationException e) {
                logger.warn("Unexpected exception", e);
                return false;
            }
        });
    }

    @Override
    public boolean updateRecord(@NotNull LockConfiguration lockConfiguration) {
        String sql = "UPDATE " + tableName
            + " SET lock_until = ?, locked_at = ?, locked_by = ? WHERE name = ? AND lock_until <= ?";
        return transactionTemplate.execute(status -> {
            int updatedRows = jdbcTemplate.update(sql, statement -> {
                Instant now = Instant.now();
                setTimestamp(statement, 1, lockConfiguration.getLockAtMostUntil());
                setTimestamp(statement, 2, now);
                statement.setString(3, getHostname());
                statement.setString(4, lockConfiguration.getName());
                setTimestamp(statement, 5, now);
            });
            return updatedRows > 0;
        });
    }

    @Override
    public boolean extend(@NotNull LockConfiguration lockConfiguration) {
        String sql = "UPDATE " + tableName
            + " SET lock_until = ? WHERE name = ? AND locked_by = ? AND lock_until > ? ";

        logger.debug("Extending lock={} until={}", lockConfiguration.getName(), lockConfiguration.getLockAtMostUntil());
        return transactionTemplate.execute(status -> {
            int updatedRows = jdbcTemplate.update(sql, statement -> {
                setTimestamp(statement, 1, lockConfiguration.getLockAtMostUntil());
                statement.setString(2, lockConfiguration.getName());
                statement.setString(3, getHostname());
                setTimestamp(statement, 4, Instant.now());
            });
            return updatedRows > 0;
        });
    }

    private void setTimestamp(PreparedStatement preparedStatement, int parameterIndex, Instant time) throws SQLException {
        if (timeZone == null) {
            preparedStatement.setTimestamp(parameterIndex, Timestamp.from(time));
        } else {
            preparedStatement.setTimestamp(parameterIndex, Timestamp.from(time), Calendar.getInstance(timeZone));
        }
    }

    @Override
    public void unlock(@NotNull LockConfiguration lockConfiguration) {
        String sql = "UPDATE " + tableName + " SET lock_until = ? WHERE name = ?";
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {

                jdbcTemplate.update(sql, statement -> {
                    setTimestamp(statement, 1, lockConfiguration.getUnlockTime());
                    statement.setString(2, lockConfiguration.getName());
                });
            }
        });
    }

}

参考

【1】Spring Boot集成ShedLock分布式定时任务实例
【2】SpringBoot的controller为什么不能并行执行?同一个浏览器连续多次访问同一个url竟然是串行的?- 第329篇

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值