轻量级分布式锁ShedLock加锁机制
springboot整合ShedLock
-
引入依赖
<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>
-
建表(mysql)
Shedlock默认表名shedlockCREATE 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) );
-
添加配置类
// 标识该类为配置类 @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() ); } }
-
添加注解
@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
- 任务执行前加锁
- 执行任务
- 任务完成后解锁
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() 方法
- 获取锁的键名
- 判断缓存中(Set集合)是否包含键名
- 缓存中没有的话,就去数据库插入一条记录,并将键名加入缓存中
- 缓存中存在,就更新记录(关注更新的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 <= 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篇