All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.googlecode.objectify.cache.redis.RedisMemcacheService Maven / Gradle / Ivy

package com.googlecode.objectify.cache.redis;

import com.googlecode.objectify.cache.IdentifiableValue;
import com.googlecode.objectify.cache.MemcacheService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * We store everything using java serialization. In theory we could be more efficient with some sort of custom
 * translator, but we really just store Entity objects, which are pretty complex, so let's just go with serialization.
 *
 * Storage format: We prefix all values with a 16-byte UUID version number, which is randomized on every put.
 */
@RequiredArgsConstructor
@Slf4j
public class RedisMemcacheService implements MemcacheService {
	/**
	 * If we give Jedis bytes, we get back bytes
	 */
	private static final byte[] OK_BYTES = "OK".getBytes(StandardCharsets.UTF_8);

	/** */
	private final JedisPool jedisPool;

	public RedisMemcacheService(final String host) {
		this(new JedisPool(host));
	}

	public RedisMemcacheService(final String host, final int port) {
		this(new JedisPool(host, port));
	}

	/** */
	private RedisIdentifiableValue fromCacheValue(final byte[] thing) {
		if (thing == null)
			return null;

		if (thing.length == 0)
			return null;

		try {
			return RedisIdentifiableValue.fromRedisString(thing);
		} catch (final Exception e) {
			log.error("Error deserializing from redis", e);
			return null;
		}
	}

	/** */
	private RedisIdentifiableValue getValue(final String key) {
		final byte[] binKey = key.getBytes(StandardCharsets.UTF_8);

		try (final Jedis jedis = jedisPool.getResource()) {
			final byte[] bytes = jedis.get(binKey);
			return fromCacheValue(bytes);
		}
	}

	/** */
	private void putValue(final String key, final RedisIdentifiableValue value) {
		final byte[] binKey = key.getBytes(StandardCharsets.UTF_8);

		try (final Jedis jedis = jedisPool.getResource()) {
			jedis.set(binKey, value.toRedisString());
		}
	}

	@Override
	public Object get(final String key) {
		final RedisIdentifiableValue iv = getValue(key);
		return iv == null ? null : iv.getValue();
	}

	@Override
	public Map getIdentifiables(final Collection keys) {
		// Can't use streams because they don't allow nulls
		//return keys.stream().collect(Collectors.toMap(Functions.identity(), this::getIdentifiable));
		final Map result = new LinkedHashMap<>();

		for (final String key : keys) {
			final IdentifiableValue iv = getIdentifiable(key);
			result.put(key, iv);
		}

		return result;
	}

	private IdentifiableValue getIdentifiable(final String key) {
		final RedisIdentifiableValue foundIv = getValue(key);

		if (foundIv != null)
			return foundIv;

		// Force a null value into the system
		final RedisIdentifiableValue nullIv = new RedisIdentifiableValue(null);

		putValue(key, nullIv);

		return nullIv;
	}

	@Override
	public Map getAll(final Collection keys) {
		final byte[][] binKeys = keys.stream().map(key -> key.getBytes(StandardCharsets.UTF_8)).toArray(byte[][]::new);

		final List fetched;
		try (final Jedis jedis = jedisPool.getResource()) {
			fetched = jedis.mget(binKeys);
		}

		final Map result = new LinkedHashMap<>();

		final Iterator keysIt = keys.iterator();
		int index = 0;
		while (keysIt.hasNext()) {
			final String key = keysIt.next();
			final byte[] value = fetched.get(index);

			final RedisIdentifiableValue iv = RedisIdentifiableValue.fromRedisString(value);

			result.put(key, iv.getValue());

			index++;
		}

		return result;
	}

	@Override
	public void put(final String key, final Object value) {
		putValue(key, new RedisIdentifiableValue(value));
	}

	@Override
	public Set putIfUntouched(final Map values) {
		final Set successes = new HashSet<>();

		values.forEach((key, cput) -> {
			final RedisIdentifiableValue iv = (RedisIdentifiableValue)cput.getIv();
			final byte[] lastVersion = iv.getVersionRedisString();

			final byte[] binKey = key.getBytes(StandardCharsets.UTF_8);
			final RedisIdentifiableValue nextIv = new RedisIdentifiableValue(cput.getNextToStore());

			try (final Jedis jedis = jedisPool.getResource()) {
				final byte[] response = executePutScript(jedis, binKey, lastVersion, nextIv.toRedisString(), cput.getExpirationSeconds());
				if (Arrays.equals(OK_BYTES, response)) {
					successes.add(key);
				}
			}
		});

		return successes;
	}

	private static final byte[] PUT_SCRIPT_WITH_EXPIRATION = "local value = redis.call('get', KEYS[1]); if (value:sub(1, 16) == KEYS[2]) then return redis.call('set', KEYS[1], KEYS[3], 'EX', KEYS[4]) end".getBytes(StandardCharsets.UTF_8);

	private static final byte[] PUT_SCRIPT_WITHOUT_EXPIRATION = "local value = redis.call('get', KEYS[1]); if (value:sub(1, 16) == KEYS[2]) then return redis.call('set', KEYS[1], KEYS[3]) end".getBytes(StandardCharsets.UTF_8);

	/**
	 * @param expiration if 0 or less, will not include an expiration
	 */
	private byte[] executePutScript(final Jedis jedis, final byte[] binKey, final byte[] lastVersion, final byte[] value, final int expiration) {
		if (expiration > 0) {
			// Redis expects the number as a string
			final byte[] exp = Integer.toString(expiration).getBytes(StandardCharsets.UTF_8);
			return (byte[])jedis.eval(PUT_SCRIPT_WITH_EXPIRATION, 4, binKey, lastVersion, value, exp);
		} else {
			return (byte[])jedis.eval(PUT_SCRIPT_WITHOUT_EXPIRATION, 3, binKey, lastVersion, value);
		}
	}

	@Override
	public void putAll(final Map values) {
		values.forEach(this::put);
	}

	@Override
	public void deleteAll(final Collection keys) {
		keys.forEach(this::delete);
	}

	private void delete(final String key) {
		final byte[] binKey = key.getBytes(StandardCharsets.UTF_8);

		try (final Jedis jedis = jedisPool.getResource()) {
			jedis.del(binKey);
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy