Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.hibernate.cache.redis.jedis.JedisClient Maven / Gradle / Ivy
/*
* Copyright 2011-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.hibernate.cache.redis.jedis;
import org.hibernate.cache.redis.serializer.RedisSerializer;
import org.hibernate.cache.redis.serializer.SerializationTool;
import org.hibernate.cache.redis.serializer.SnappyRedisSerializer;
import org.hibernate.cache.redis.serializer.StringRedisSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Transaction;
import redis.clients.util.Pool;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* RedisClient implements using Jedis library
*
* 참고 : https://github.com/xetorthio/org.hibernate.cache.redis.jedis/wiki/AdvancedUsage
*
* @author 배성혁 ( [email protected] )
* @since 13. 4. 9 오후 10:20
*/
public class JedisClient {
public static final int DEFAULT_EXPIRY_IN_SECONDS = 120;
public static final String DEFAULT_REGION_NAME = "hibernate";
private static final int MAX_TIMESTAMP_UPDATE_ATTEMPTS = 5;
private static final Logger log = LoggerFactory.getLogger(JedisClient.class);
private final Pool jedisPool;
private int expiryInSeconds;
private final StringRedisSerializer regionSerializer = new StringRedisSerializer();
private final StringRedisSerializer keySerializer = new StringRedisSerializer();
private final RedisSerializer valueSerializer = new SnappyRedisSerializer();
public JedisClient() {
this(new JedisPool("localhost"), DEFAULT_EXPIRY_IN_SECONDS);
}
public JedisClient(Pool jedisPool) {
this(jedisPool, DEFAULT_EXPIRY_IN_SECONDS);
}
/**
* initialize JedisClient instance
*
* @param jedisPool JedisPool instance
* @param expiryInSeconds expiration in seconds
*/
public JedisClient(Pool jedisPool, int expiryInSeconds) {
log.debug("JedisClient created. jedisPool=[{}], expiryInSeconds=[{}]", jedisPool, expiryInSeconds);
this.jedisPool = jedisPool;
this.expiryInSeconds = expiryInSeconds;
}
public Pool getJedisPool() {
return this.jedisPool;
}
public int getExpiryInSeconds() {
return this.expiryInSeconds;
}
public void setExpiryInSeconds(int expiryInSeconds) {
this.expiryInSeconds = expiryInSeconds;
}
/**
* ping test for server alive
*/
public String ping() {
return run(new JedisCallback() {
@Override
public String execute(Jedis jedis) {
return jedis.ping();
}
});
}
/**
* get Redis db size
*/
public Long dbSize() {
return run(new JedisCallback() {
@Override
public Long execute(Jedis jedis) {
return jedis.dbSize();
}
});
}
/**
* confirm the specified cache item in specfied region
*
* @param region region name
* @param key cache key
*/
public boolean exists(final String region, final Object key) {
final byte[] rawRegion = rawRegion(region);
final byte[] rawKey = rawKey(key);
return run(new JedisCallback() {
@Override
public Boolean execute(Jedis jedis) {
return jedis.hexists(rawRegion, rawKey);
}
});
}
/**
* Get cache
*
* @param region region name
* @param key cache key
* @return return cached entity, if not exists return null.
*/
public Object get(final String region, final Object key) {
return get(region, key, 0);
}
/**
* Get cache
*
* @param region region name
* @param key cache key
* @param expirationInSeconds expiration timeout in seconds
* @return return cached entity, if not exists return null.
*/
public Object get(final String region, final Object key, final int expirationInSeconds) {
final byte[] rawRegion = rawRegion(region);
final byte[] rawKey = rawKey(key);
// NOTE: expire 된 캐시 정보라면 삭제하고, null 값을 반환합니다.
if (expirationInSeconds > 0 && isExpired(region, key)) {
runWithPipeline(new JedisPipelinedCallback() {
@Override
public void execute(Pipeline pipeline) {
final byte[] rawZkey = rawZkey(region);
pipeline.zrem(rawZkey, rawKey);
pipeline.hdel(rawRegion, rawKey);
}
});
return null;
}
byte[] rawValue = run(new JedisCallback() {
@Override
public byte[] execute(Jedis jedis) {
return jedis.hget(rawRegion, rawKey);
}
});
// after get, update expiration time
if (rawValue != null && rawValue.length > 0) {
if (expirationInSeconds > 0 && !region.contains("UpdateTimestampsCache")) {
run(new JedisCallback() {
@Override
public Object execute(Jedis jedis) {
final byte[] rawZkey = rawZkey(region);
final long score = System.currentTimeMillis() + expirationInSeconds * 1000L;
return jedis.zadd(rawZkey, score, rawKey);
}
});
}
}
return deserializeValue(rawValue);
}
private Boolean isExpired(final String region, final Object key) {
final byte[] rawZkey = rawZkey(region);
final byte[] rawKey = rawKey(key);
Double timestamp = run(new JedisCallback() {
@Override
public Double execute(Jedis jedis) {
return jedis.zscore(rawZkey, rawKey);
}
});
return timestamp != null && System.currentTimeMillis() > timestamp.longValue();
}
/**
* retrieve all cached items in specified region
*
* @param region region
* @return collection of cached items
*/
public Set keysInRegion(String region) {
try {
final byte[] rawRegion = rawRegion(region);
Set rawKeys = run(new JedisCallback>() {
@Override
public Set execute(Jedis jedis) {
return jedis.hkeys(rawRegion);
}
});
if (rawKeys != null)
return deserializeKeys(rawKeys);
} catch (Exception ignored) { }
return new HashSet();
}
/**
* get cache count in region
*
* @param region region
* @return cache item count in region
*/
public Long keySizeInRegion(final String region) {
final byte[] rawRegion = rawRegion(region);
return run(new JedisCallback() {
@Override
public Long execute(Jedis jedis) {
return jedis.hlen(rawRegion);
}
});
}
/**
* get all cached items in specified region
*
* @param region region name
* @return map of keys and all cached items in specified region
*/
public Map hgetAll(String region) {
final byte[] rawRegion = rawRegion(region);
Map rawMap = run(new JedisCallback>() {
@Override
public Map execute(Jedis jedis) {
return jedis.hgetAll(rawRegion);
}
});
Map map = new HashMap();
for (Map.Entry entry : rawMap.entrySet()) {
Object key = deserializeKey(entry.getKey());
Object value = deserializeValue(entry.getValue());
map.put(key, value);
}
return map;
}
/**
* multiple get cache items in specified region
*
* @param region region name
* @param keys cache key collection to retrieve
* @return cache items
*/
public List mget(final String region, final Collection> keys) {
final byte[] rawRegion = rawRegion(region);
final byte[][] rawKeys = rawKeys(keys);
List rawValues = run(new JedisCallback>() {
@Override
public List execute(Jedis jedis) {
return jedis.hmget(rawRegion, rawKeys);
}
});
return deserializeValues(rawValues);
}
/**
* save cache
*
* @param region region name
* @param key cache key to save
* @param value cache value to save
*/
public void set(String region, Object key, Object value) {
set(region, key, value, expiryInSeconds, TimeUnit.SECONDS);
}
/**
* save cache item
*
* @param region region name
* @param key cache key to save
* @param value cache value to save
* @param timeoutInSeconds expire timeout in seconds
*/
public void set(String region, Object key, Object value, long timeoutInSeconds) {
set(region, key, value, timeoutInSeconds, TimeUnit.SECONDS);
}
/**
* save cache item
*
* @param region region name
* @param key cache key to save
* @param value cache value to save
* @param timeout expire timeout
* @param unit expire timeout unit
*/
public void set(final String region, final Object key, final Object value, long timeout, TimeUnit unit) {
final byte[] rawRegion = rawRegion(region);
final byte[] rawKey = rawKey(key);
final byte[] rawValue = rawValue(value);
final int seconds = (int) unit.toSeconds(timeout);
runWithTx(new JedisTransactionalCallback() {
@Override
public void execute(Transaction tx) {
tx.hset(rawRegion, rawKey, rawValue);
if (seconds > 0 && !region.contains("UpdateTimestampsCache")) {
final byte[] rawZkey = rawZkey(region);
final long score = System.currentTimeMillis() + seconds * 1000L;
tx.zadd(rawZkey, score, rawKey);
}
}
});
}
/**
* delete cache item which is expired in region
*
* @param region region name
*/
public void expire(final String region) {
try {
final byte[] rawZkey = rawZkey(region);
final byte[] rawRegion = rawRegion(region);
final long score = System.currentTimeMillis();
// get key which score is less than current time
final Set rawKeys = run(new JedisCallback>() {
@Override
public Set execute(Jedis jedis) {
return jedis.zrangeByScore(rawZkey, 0, score);
}
});
if (rawKeys != null && rawKeys.size() > 0) {
log.debug("delete expired cache item in region[{}] expire time=[{}]", region, score);
runWithPipeline(new JedisPipelinedCallback() {
@Override
public void execute(Pipeline pipeline) {
// delete cache item
for (final byte[] rawKey : rawKeys) {
pipeline.hdel(rawRegion, rawKey);
}
pipeline.zremrangeByScore(rawZkey, 0, score);
}
});
}
} catch (Exception ignored) {
log.warn("Error in Cache Expiration Method.", ignored);
}
}
/**
* delete cache item in specified region.
*
* @param region region name
* @param key cache key to delete
* @return count of deleted key
*/
public Long del(final String region, final Object key) {
final byte[] rawRegion = rawRegion(region);
final byte[] rawKey = rawKey(key);
final byte[] rawZkey = rawZkey(region);
runWithTx(new JedisTransactionalCallback() {
@Override
public void execute(Transaction tx) {
tx.hdel(rawRegion, rawKey);
tx.zrem(rawZkey, rawKey);
}
});
return 1L;
}
/**
* multiplu delete cache items in specified region
*
* @param keys key collection to delete
*/
public void mdel(final String region, final Collection> keys) {
final byte[] rawRegion = rawRegion(region);
final byte[] rawZkey = rawZkey(region);
final byte[][] rawKeys = rawKeys(keys);
runWithTx(new JedisTransactionalCallback() {
@Override
public void execute(Transaction tx) {
for (byte[] rawKey : rawKeys) {
tx.hdel(rawRegion, rawKey);
tx.zrem(rawZkey, rawKey);
}
}
});
}
/**
* delete region
*
* @param region region name to delete
*/
public void deleteRegion(final String region) throws JedisCacheException {
log.debug("delete region region=[{}]", region);
final byte[] rawRegion = rawRegion(region);
final byte[] rawZkey = rawZkey(region);
runWithTx(new JedisTransactionalCallback() {
@Override
public void execute(Transaction tx) {
tx.del(rawRegion);
tx.del(rawZkey);
}
});
}
/**
* We want nextTimestamp to return a long that is greater than previous calls to nextTimestamp.
*
* The simplest algorithm would be to ignore Redis and return currentTimeMillis. However, different VMs can have small drifts
* in their currentTimeMillis and this algorithm isn't safe in multi VM clusters.
*
* A better algorithm would be:
* SETNX rawKey currentTimeMillis
* INCR rawKey
*
* However, there are two problems.
* 1. Clients can have small drift in their currentTimeMillis.
* If rawKey had been set but later evicted then a client which is behind other clients in currentTimeMillis
* might be the first to call SETNX and issue set to a value that had previously been issued by another client.
* 2. Master-slave replication is not synchronous.
* Therefore a slave promoted to master might have missed some calls to INCR. As a result
* we might create timestamps that have already been issued.
*
* Instead we do the following:
* WATCH rawKey
* current = GET rawKey
* new = max(current, currentTimeMillis) + 1
* MULTI
* SET rawKey $new
* EXEC
*
* If a client has drifted behind in currentTimeMillis then the current timestamp will just be incremented. This solves 1.
* If current value is behind we set to currentTimeMillis +1. This fixes 2.
*
* If the exec fails because another client changed the value we retry the update.
* To avoid deadlocking attempting the update we cap the number of retries. After max retries we fall back to just
* incrementing the current value which is a single Redis operation and will always succeed.
* This does not fix problem 2. but gives us path to eventually recover.
*/
public long nextTimestamp(final Object key) {
final byte[] rawKey = rawKey(key);
Long updatedTimestamp = null;
int updateAttempts = 0;
while (updatedTimestamp == null && updateAttempts < MAX_TIMESTAMP_UPDATE_ATTEMPTS) {
updatedTimestamp = updateOrIncrementTimestamp(rawKey);
updateAttempts++;
}
if ( updatedTimestamp != null) {
log.debug("updated timestamp: key=[{}], attempt=[{}], timestamp=[{}]", key, updateAttempts, updatedTimestamp);
return updatedTimestamp;
} else {
// If we can't update the timestamp at least fall back to incrementing
Long incrementedTimestamp = incrementTimestamp(rawKey);
log.warn("updated timestamp failed {} times. Fall back to incrementing: key=[{}], timestamp=[{}]", updateAttempts, key, incrementedTimestamp);
return incrementedTimestamp;
}
}
/**
* Cleanup any resources thathe JedisClient might have references to.
*/
public void destroy() {
if (jedisPool != null) {
jedisPool.destroy();
}
}
private Long updateOrIncrementTimestamp(final byte[] rawKey) {
return run(new JedisCallback() {
@Override
public Long execute(Jedis jedis) {
jedis.watch(rawKey);
Long currentTimestamp = (Long)deserializeValue(jedis.get(rawKey));
if (currentTimestamp == null) {
currentTimestamp = 0L;
}
Long newTimestamp = Math.max(System.currentTimeMillis(), currentTimestamp) + 1;
Transaction tx = jedis.multi();
tx.set(rawKey, rawValue(newTimestamp));
List result = tx.exec(); // it the watch fails exec returns null
return result != null ? newTimestamp : null;
}
});
}
private Long incrementTimestamp(final byte[] rawKey) {
return run(new JedisCallback() {
@Override
public Long execute(Jedis jedis) {
return jedis.incr(rawKey);
}
});
}
/**
* flush db
*/
public String flushDb() {
log.info("Flush DB...");
return run(new JedisCallback() {
@Override
public String execute(Jedis jedis) {
return jedis.flushDB();
}
});
}
/**
* serialize cache key
*/
private byte[] rawKey(final Object key) {
return keySerializer.serialize(key.toString());
}
@SuppressWarnings("unchecked")
private byte[][] rawKeys(final Collection> keys) {
byte[][] rawKeys = new byte[keys.size()][];
int i = 0;
for (Object key : keys) {
rawKeys[i++] = rawKey(key);
}
return rawKeys;
}
/**
* Serialize expiration region name
*/
private byte[] rawZkey(final String region) {
return rawRegion("z:" + region);
}
/**
* serializer region name
*/
private byte[] rawRegion(final String region) {
return regionSerializer.serialize(region);
}
/**
* deserialize key
*/
private Object deserializeKey(final byte[] rawKey) {
return keySerializer.deserialize(rawKey);
}
/**
* serializer cache value
*/
private byte[] rawValue(final Object value) {
try {
return valueSerializer.serialize(value);
} catch (Exception e) {
log.warn("value를 직렬화하는데 실패했습니다. value=" + value, e);
return null;
}
}
/**
* deserialize raw value
*/
private Object deserializeValue(final byte[] rawValue) {
return valueSerializer.deserialize(rawValue);
}
/**
* execute the specified callback
*/
private T run(final JedisCallback callback) {
Jedis jedis = jedisPool.getResource();
try {
return callback.execute(jedis);
} finally {
jedis.close();
}
}
/**
* execute the specified callback under transaction
* HINT: https://github.com/xetorthio/org.hibernate.cache.redis.jedis/wiki/AdvancedUsage
*
* @param callback executable instance under transaction
*/
private List runWithTx(final JedisTransactionalCallback callback) {
Jedis jedis = jedisPool.getResource();
try {
Transaction tx = jedis.multi();
callback.execute(tx);
return tx.exec();
} finally {
jedis.close();
}
}
/**
* execute the specified callback under Redis Pipeline
* HINT: https://github.com/xetorthio/org.hibernate.cache.redis.jedis/wiki/AdvancedUsage
*
* @param callback executable instance unider Pipeline
*/
private void runWithPipeline(final JedisPipelinedCallback callback) {
final Jedis jedis = jedisPool.getResource();
try {
final Pipeline pipeline = jedis.pipelined();
callback.execute(pipeline);
// use #sync(), not #exec()
pipeline.sync();
} finally {
jedis.close();
}
}
/**
* deserialize the specified raw key set.
*
* @return original key set.
*/
private Set deserializeKeys(final Set rawKeys) {
Set keys = new HashSet();
for (byte[] rawKey : rawKeys) {
keys.add(deserializeKey(rawKey));
}
return keys;
}
/**
* deserialize the specified raw value collection
*
* @return collection of original value
*/
private List deserializeValues(final List rawValues) {
return SerializationTool.deserialize(rawValues, valueSerializer);
}
}