Jpa 用于应用程序同步的数据库锁
我们有一个基于JavaEE5的JSF应用程序,它运行在两个WebLogic应用程序服务器上,共享一个Oracle数据库 对于某些用例来说,至关重要的是,只有一个节点在数据库中执行操作,这些操作通常是永久性的后台作业。因此,我们的想法是,一个节点(“主节点”)在数据库中获得某种锁,而另一个节点(“从节点”)识别锁,并且只要主节点可用,就不会对这些用例执行任何操作。只有当第一个节点变得不可用时,第二个节点才应该接管工作,从而从那里开始自己持有锁Jpa 用于应用程序同步的数据库锁,jpa,locking,high-availability,java-ee-5,Jpa,Locking,High Availability,Java Ee 5,我们有一个基于JavaEE5的JSF应用程序,它运行在两个WebLogic应用程序服务器上,共享一个Oracle数据库 对于某些用例来说,至关重要的是,只有一个节点在数据库中执行操作,这些操作通常是永久性的后台作业。因此,我们的想法是,一个节点(“主节点”)在数据库中获得某种锁,而另一个节点(“从节点”)识别锁,并且只要主节点可用,就不会对这些用例执行任何操作。只有当第一个节点变得不可用时,第二个节点才应该接管工作,从而从那里开始自己持有锁 我现在的问题是,我们如何实现这种行为(记住,JPA1.
我现在的问题是,我们如何实现这种行为(记住,JPA1.0),如果一个节点发生故障,数据库中的锁是否会自动释放?还是应该以不同的方式更好地完成整个任务?这里有一个简单的解决方案,类似于ActiveMQ只让一个主服务器执行任务,而其他正在运行的实例则等待成为主服务器
package com.despegar.bookedia.message.broker.lock;
import com.google.common.collect.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.data.jdbc.support.DatabaseType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import org.springframework.transaction.annotation.Transactional;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Map;
/**
* Represents an exclusive lock on a database to avoid multiple brokers running
* against the same logical database.
* <p>
* The Lease Database Locker lets the master broker acquire a lock that's valid for a fixed (usually short) duration after which it expires.
* To retain the lock the master broker must periodically extend the lock's lease before it expires.
* Simultaneously the slave broker also checks periodically to see if the lease has expired. If, for whatever reason, the master broker fails to update its
* lease on the lock the slave will take ownership of the lock becoming the new master in the process. The leased lock can survive a DB replica failover.
* </p>
* Each broker in the master/slave pair must have a different leaseHolderId attribute, as it is this value that is used to reserve a lease.
* <p>
* In the simplest case, the clocks between master and slave must be in sync for this solution to work properly. If the clocks cannot be in sync, the
* locker can use the system time from the database CURRENT TIME and adjust the timeouts in accordance with their local variance from the DB system time.
* If maxAllowableDiffFromDBTime is greater than zero the local periods will be adjusted by any delta that exceeds maxAllowableDiffFromDBTime.
* </p>
*/
public class LeaseDatabaseLocker implements Locker, AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(LeaseDatabaseLocker.class);
private static final int IM_THE_MASTER_RESULT = 1;
private int maxAllowableDiffFromDBTime;
private long diffFromCurrentTime = Long.MAX_VALUE;
private String leaseHolderId;
private JdbcTemplate jdbcTemplate;
private int queryTimeoutInSecs = -1;
private long lockAcquireSleepInterval;
private long lockHeldPeriod;
public LeaseDatabaseLocker(String leaseHolderId, JdbcTemplate jdbcTemplate, int queryTimeout,
long lockAcquireSleepInterval, int maxAllowableDiffFromDBTime, long lockHeldPeriod) {
this.maxAllowableDiffFromDBTime = maxAllowableDiffFromDBTime;
this.jdbcTemplate = jdbcTemplate;
this.queryTimeoutInSecs = queryTimeout;
this.lockAcquireSleepInterval = lockAcquireSleepInterval;
this.leaseHolderId = leaseHolderId;
this.lockHeldPeriod = lockHeldPeriod;
}
@Transactional
@Override
public void acquireLock() {
LOG.debug("Attempting to acquire the exclusive lock to become the Master broker '{}'", leaseHolderId);
String sql = Statements.LEASE_OBTAIN_STATEMENT;
initTimeDiff();
long now = System.currentTimeMillis() + diffFromCurrentTime;
long nextLockCheck = now + lockHeldPeriod;
PreparedStatementSetter preparedStatementSetter = statement -> {
setQueryTimeoutInSecs(statement);
statement.setString(Statements.ACQUIRE_LOCK_BROKER_NAME_COL_POSITION, leaseHolderId);
statement.setLong(Statements.ACQUIRE_LOCK_NEXT_CHECK_COL_POSITION, nextLockCheck);
statement.setLong(Statements.ACQUIRE_LOCK_TIME_NOW_POSITION, now);
};
LOG.trace("executing: '{}' to acquire lock with values {}, {}, {}", Statements.LEASE_OBTAIN_STATEMENT, leaseHolderId, nextLockCheck, now);
int result = jdbcTemplate.update(sql, preparedStatementSetter);
LOG.trace("Locking query result: updated rows count {}", result);
if (result == IM_THE_MASTER_RESULT) {
// we got the lease, verify we still have it
LOG.debug("Lock acquired for '{}'", leaseHolderId);
if (keepLockAlive()) {
LOG.info("Becoming the master on dataSource: {}", jdbcTemplate.getDataSource());
return;
}
}
reportLeaseOwnerShipAndDuration();
LOG.debug("{} failed to acquire lease. Sleeping for {} milli(s) before trying again...", leaseHolderId, lockAcquireSleepInterval);
throw new BrokerException.LockNotAcquiredException(leaseHolderId);
}
private void reportLeaseOwnerShipAndDuration() {
String sql = Statements.LEASE_OWNER_STATEMENT;
SqlRowSet rowSet = jdbcTemplate.queryForRowSet(sql);
while (rowSet.next()) {
LOG.debug("{} - Lease held by {} till {}", leaseHolderId, rowSet.getString(1),
Instant.ofEpochMilli(rowSet.getLong(2)));
}
}
private void setQueryTimeoutInSecs(Statement statement) throws SQLException {
if (queryTimeoutInSecs > 0) {
statement.setQueryTimeout(queryTimeoutInSecs);
}
}
private long initTimeDiff() {
if (Long.MAX_VALUE == diffFromCurrentTime) {
if (maxAllowableDiffFromDBTime > 0) {
diffFromCurrentTime = determineTimeDifference();
} else {
diffFromCurrentTime = 0l;
}
}
return diffFromCurrentTime;
}
protected long determineTimeDifference() {
ResultSetExtractor<Timestamp> timestampExtractor = rs -> {
rs.next();
return rs.getTimestamp(1);
};
Timestamp timestamp = jdbcTemplate.query(Statements.utcTimestamp(jdbcTemplate), timestampExtractor);
long result = 0L;
long diff = System.currentTimeMillis() - timestamp.getTime();
if (Math.abs(diff) > maxAllowableDiffFromDBTime) {
// off by more than maxAllowableDiffFromDBTime so lets adjust
result = -diff;
}
LOG.info("{} diff adjust from db: {}, db time: {}", leaseHolderId, result, timestamp);
return result;
}
@Transactional
public boolean keepLockAlive() {
boolean result;
final String sql = Statements.LEASE_UPDATE_STATEMENT;
initTimeDiff();
final long now = System.currentTimeMillis() + diffFromCurrentTime;
final long nextLockCheck = now + lockHeldPeriod;
PreparedStatementSetter statementSetter = statement -> {
setQueryTimeoutInSecs(statement);
statement.setString(Statements.KEEP_LOCK_NEW_BROKER_NAME_COL_POSITION, leaseHolderId);
statement.setLong(Statements.KEEP_LOCK_NEXT_CHECK_COL_POSITION, nextLockCheck);
statement.setString(Statements.KEEP_LOCK_BROKER_NAME_COL_POSITION, leaseHolderId);
};
LOG.trace("executing: '{}' to keep lock alive with values {}, {}", Statements.LEASE_UPDATE_STATEMENT, leaseHolderId, nextLockCheck);
result = jdbcTemplate.update(sql, statementSetter) == IM_THE_MASTER_RESULT;
if (!result) {
reportLeaseOwnerShipAndDuration();
}
return result;
}
private void releaseLease() {
String sql = Statements.LEASE_UPDATE_STATEMENT;
final int lockReleaseTime = 1;
PreparedStatementSetter statementSetter = statement -> {
statement.setString(Statements.RELEASE_LOCK_NEW_BROKER_NAME_COL_POSITION, leaseHolderId);
statement.setLong(Statements.RELEASE_LOCK_NEXT_CHECK_COL_POSITION, lockReleaseTime);
statement.setString(Statements.RELEASE_LOCK_BROKER_NAME_COL_POSITION, leaseHolderId);
};
LOG.trace("executing: '{}' to release lock with values {}, {}, {}", sql, leaseHolderId, 1, leaseHolderId);
if (jdbcTemplate.update(sql, statementSetter) == IM_THE_MASTER_RESULT) {
LOG.info("{}, released lease", leaseHolderId);
}
}
@Override
public void close() throws Exception {
releaseLease();
}
static class Statements {
public static final String LOCK_TABLE_NAME = "MSG_BROKER_LOCK";
public static final Map<DatabaseType, String> CURRENT_DATE_TIME_UTC = ImmutableMap.of(DatabaseType.MYSQL, "SELECT UTC_TIMESTAMP",
DatabaseType.H2, "SELECT CURRENT_TIMESTAMP");
public static final String LEASE_UPDATE_STATEMENT =
String.format("UPDATE %s SET BROKER_NAME=?, %s.TIME=? WHERE BROKER_NAME=? AND ID = 1", LOCK_TABLE_NAME, LOCK_TABLE_NAME);
public static final String LEASE_OWNER_STATEMENT =
String.format("SELECT BROKER_NAME, %s.TIME FROM %s WHERE ID = 1", LOCK_TABLE_NAME, LOCK_TABLE_NAME);
public static final String LEASE_OBTAIN_STATEMENT =
String.format("UPDATE %s SET BROKER_NAME=?, %s.TIME=? WHERE (%s.TIME IS NULL OR %s.TIME < ?) AND ID = 1",
LOCK_TABLE_NAME, LOCK_TABLE_NAME, LOCK_TABLE_NAME, LOCK_TABLE_NAME);
//Acquire constants
public static final int ACQUIRE_LOCK_BROKER_NAME_COL_POSITION = 1;
public static final int ACQUIRE_LOCK_NEXT_CHECK_COL_POSITION = 2;
public static final int ACQUIRE_LOCK_TIME_NOW_POSITION = 3;
//Keep lock alive constants
public static final int KEEP_LOCK_NEW_BROKER_NAME_COL_POSITION = 1;
public static final int KEEP_LOCK_NEXT_CHECK_COL_POSITION = 2;
public static final int KEEP_LOCK_BROKER_NAME_COL_POSITION = 3;
//Release lock constants
public static final int RELEASE_LOCK_NEW_BROKER_NAME_COL_POSITION = 1;
public static final int RELEASE_LOCK_NEXT_CHECK_COL_POSITION = 2;
public static final int RELEASE_LOCK_BROKER_NAME_COL_POSITION = 3;
private Statements() {}
private static String utcTimestamp(JdbcTemplate jdbcTemplate) {
DatabaseType dbType;
try {
dbType = DatabaseType.fromMetaData(jdbcTemplate.getDataSource());
} catch (MetaDataAccessException e) {
throw new DataAccessResourceFailureException("Unable to determine database type: ", e);
}
String query = CURRENT_DATE_TIME_UTC.get(dbType);
if(query == null) {
throw new RuntimeException("Unrecognized DatabaseType: " + dbType);
}
return query;
}
}
}
package com.despegar.bookedia.message.broker.lock;
导入com.google.common.collect.ImmutableMap;
导入org.slf4j.Logger;
导入org.slf4j.LoggerFactory;
导入org.springframework.dao.DataAccessResourceFailureException;
导入org.springframework.data.jdbc.support.DatabaseType;
导入org.springframework.jdbc.core.jdbc模板;
导入org.springframework.jdbc.core.PreparedStatementSetter;
导入org.springframework.jdbc.core.ResultSetExtractor;
导入org.springframework.jdbc.support.MetaDataAccessException;
导入org.springframework.jdbc.support.rowset.SqlRowSet;
导入org.springframework.transaction.annotation.Transactional;
导入java.sql.SQLException;
导入java.sql.Statement;
导入java.sql.Timestamp;
导入java.time.Instant;
导入java.util.Map;
/**
*表示数据库上的独占锁,以避免多个代理运行
*针对相同的逻辑数据库。
*
*租约数据库锁允许主代理获取一个在固定(通常较短)期限内有效的锁,该期限过后该锁将过期。
*为了保留锁,主代理必须在锁的租约到期之前定期延长该锁的租约。
*同时,从属代理还定期检查租约是否过期。无论出于何种原因,如果主代理未能更新其
*租用锁在这个过程中,从机将获得锁的所有权,成为新的主机。租用的锁可以在数据库副本故障切换中存活。
*
*主/从对中的每个代理必须具有不同的leaseHolderId属性,因为正是该值用于保留租约。
*
*在最简单的情况下,主机和从机之间的时钟必须同步,此解决方案才能正常工作。如果时钟不能同步,则
*locker可以使用数据库当前时间中的系统时间,并根据其与DB系统时间的局部差异调整超时。
*如果maxAllowableDiffFromDBTime大于零,则本地时段将由超过maxAllowableDiffFromDBTime的任何增量进行调整。
*
*/
公共类LeaseDatabaseLocker实现了可自动关闭的锁定器{
私有静态最终记录器LOG=LoggerFactory.getLogger(LeaseDatabaseLocker.class);
private static final int IM_THE_MASTER_RESULT=1;
私有int maxallowablediffromdbtime;
私有long diffFromCurrentTime=long.MAX_值;
私有字符串承租人ID;
私有JdbcTemplate JdbcTemplate;
私有int queryTimeoutineSecs=-1;
私人长时间锁;
私人长锁周期;
public LeaseDatabaseLocker(字符串leaseHolderId、JdbcTemplate、JdbcTemplate、int queryTimeout、,
长lockAcquireSleepInterval,int-MaxAllowableDiffromdBTime,长lockHeldPeriod){
this.MaxAllowableDiffFromDtime=MaxAllowableDiffFromDtime;
this.jdbcTemplate=jdbcTemplate;
this.querytimeoutines=queryTimeout;
this.lockAcquireSleepInterval=lockAcquireSleepInterval;
this.leaseHolderId=leaseHolderId;
this.lockHeldPeriod=lockHeldPeriod;
}
@交易的
@凌驾
公共锁(){
debug(“试图获取独占锁以成为主代理'{}',leaseHolderId”);
字符串sql=Statements.LEASE\u-get\u语句;
initTimeDiff();
long now=System.currentTimeMillis()+diffFromCurrentTime;
long nextLockCheck=now+lockHeldPeriod;
PreparedStatementSetter PreparedStatementSetter=语句->{
SetQueryTimeOutineSecs(语句);
statement.setString(Statements.ACQUIRE\u LOCK\u BROKER\u NAME\u COL\u POSITION,leaseHolderId);
statement.setLong(Statements.ACQUIRE\u LOCK\u NEXT\u CHECK\u COL\u POSITION,nextlock CHECK);
statement.setLong(Statements.ACQUIRE\u LOCK\u TIME\u NOW\u POSITION,NOW);
};
trace(“执行:{}以获取值为{},{},{}的锁,Statements.LEASE_-acquire_语句,leaseHolderId,nextLockCheck,now);
int result=jdbcTemplate.update(sql,preparedStatementSetter);
trace(“锁定查询结果:更新的行计数{}”,结果);
if(result==IM\u主结果){
//我们拿到租约了,核实一下我们还有吗
debug(“为{}”获取锁,leaseHolderId);
if(keeplockavive()){
LOG.info(“成为dataSource:{},jdbcTemplate.getDataSource()上的主节点”);
返回;
}
}
reportLeaseOwnerShipAndDuration();
LOG.debug(“{}未能获取租约。在重试之前,{}毫秒处于休眠状态…”,leaseHolderId,lockAcquireSleepInterval);
抛出新BrokerException.LockNotAcquiredException(leaseHolderId);
}
私有无效报告LeaseOwnershipAndDuration(){
字符串sql=Statements.LEASE\u OWNER\u语句;
SqlRowSet rowSet=jdbcT