Blame | Last modification | View Log | RSS feed
package com.spice.profitmandi.dao.config;import org.springframework.data.redis.cache.RedisCache;import org.springframework.data.redis.cache.RedisCacheConfiguration;import org.springframework.data.redis.cache.RedisCacheManager;import org.springframework.data.redis.cache.RedisCacheWriter;import org.springframework.data.redis.connection.RedisConnection;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.connection.ReturnType;import java.nio.charset.StandardCharsets;import java.time.Duration;import java.util.Map;import java.util.Set;// An offer write that commits after a reader's transaction began leaves that reader with the old row;// without this gate the reader caches it once the writer has evicted and it stays stale until the TTLpublic class OfferCacheGate {private static final byte[] LAST_CHANGE_KEY = "offer.cache.lastChange".getBytes(StandardCharsets.UTF_8);// fofo, web and cron share one host clock; the margin absorbs small driftprivate static final long CLOCK_MARGIN_MS = 2000;// outside a transaction every statement reads fresh data, so only the loader's own run time mattersprivate static final long NO_TX_LOOKBACK_MS = 10000;private static final byte[] MARK_SCRIPT = ("local cur = tonumber(redis.call('GET', KEYS[1]) or '0') "+ "if tonumber(ARGV[1]) > cur then redis.call('SET', KEYS[1], ARGV[1]) end "+ "return 1").getBytes(StandardCharsets.UTF_8);private static final byte[] PUT_IF_FRESH_SCRIPT = ("local last = tonumber(redis.call('GET', KEYS[1]) or '0') "+ "if tonumber(ARGV[1]) <= last + tonumber(ARGV[2]) then return 0 end "+ "if tonumber(ARGV[4]) > 0 then redis.call('SET', KEYS[2], ARGV[3], 'PX', ARGV[4]) "+ "else redis.call('SET', KEYS[2], ARGV[3]) end "+ "return 1").getBytes(StandardCharsets.UTF_8);private final RedisConnectionFactory connectionFactory;public OfferCacheGate(RedisConnectionFactory connectionFactory) {this.connectionFactory = connectionFactory;}// Called after an offer write's transaction completes, before its evictionspublic void markChanged() {eval(MARK_SCRIPT, 1, LAST_CHANGE_KEY, bytes(System.currentTimeMillis()));}private void putIfFresh(byte[] key, byte[] value, Duration ttl) {Long txStart = TxStartTrackingTransactionManager.currentTxStartMillis();long readerStart = txStart != null ? txStart : System.currentTimeMillis() - NO_TX_LOOKBACK_MS;eval(PUT_IF_FRESH_SCRIPT, 2, LAST_CHANGE_KEY, key, bytes(readerStart), bytes(CLOCK_MARGIN_MS), value,bytes(ttl.toMillis()));}public RedisCacheManager manager(RedisCacheConfiguration defaults, Map<String, RedisCacheConfiguration> initial,Set<String> gatedCaches) {return new Manager(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory), defaults, initial,gatedCaches);}private void eval(byte[] script, int numKeys, byte[]... keysAndArgs) {RedisConnection connection = connectionFactory.getConnection();try {connection.scriptingCommands().eval(script, ReturnType.INTEGER, numKeys, keysAndArgs);} finally {connection.close();}}private static byte[] bytes(long value) {return Long.toString(value).getBytes(StandardCharsets.UTF_8);}private class Manager extends RedisCacheManager {private final Set<String> gatedCaches;Manager(RedisCacheWriter writer, RedisCacheConfiguration defaults, Map<String, RedisCacheConfiguration> initial,Set<String> gatedCaches) {super(writer, defaults, initial);this.gatedCaches = gatedCaches;}@Overrideprotected RedisCache createRedisCache(String name, RedisCacheConfiguration config) {RedisCache cache = super.createRedisCache(name, config);return gatedCaches.contains(name) ? new GatedCache(cache) : cache;}}private class GatedCache extends RedisCache {GatedCache(RedisCache cache) {super(cache.getName(), cache.getNativeCache(), cache.getCacheConfiguration());}@Overridepublic void put(Object key, Object value) {Object cacheValue = preProcessCacheValue(value);if (cacheValue == null) {super.put(key, value);return;}putIfFresh(serializeCacheKey(createCacheKey(key)), serializeCacheValue(cacheValue),getCacheConfiguration().getTtl());}}}