Add RedisTemplate methods for executing Redis scripts

DATAREDIS-199
This commit is contained in:
Jennifer Hickey
2013-08-04 16:43:00 -07:00
parent faa78cdfaa
commit 7d387ab529
19 changed files with 914 additions and 1 deletions

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.redis.connection;
import java.util.List;
/**
* Represents a data type returned from Redis, currently used to denote the
* expected return type of Redis scripting commands
@@ -37,5 +39,21 @@ public enum ReturnType {
STATUS,
/** Returned as byte[] **/
VALUE
VALUE;
public static ReturnType fromJavaType(Class<?> javaType) {
if(javaType == null) {
return ReturnType.STATUS;
}
if(javaType.isAssignableFrom(List.class)) {
return ReturnType.MULTI;
}
if(javaType.isAssignableFrom(Boolean.class)) {
return ReturnType.BOOLEAN;
}
if(javaType.isAssignableFrom(Long.class)) {
return ReturnType.INTEGER;
}
return ReturnType.VALUE;
}
}

View File

@@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.query.SortQuery;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
@@ -104,6 +105,40 @@ public interface RedisOperations<K, V> {
*/
List<Object> executePipelined(final SessionCallback<?> session, final RedisSerializer<?> resultSerializer);
/**
* Executes the given {@link RedisScript}
*
* @param script
* The script to execute
* @param keys
* Any keys that need to be passed to the script
* @param args
* Any args that need to be passed to the script
* @return The return value of the script or null if {@link RedisScript#getResultType()} is
* null, likely indicating a throw-away status reply (i.e. "OK")
*/
<T> T execute(RedisScript<T> script, List<K> keys, Object... args);
/**
* Executes the given {@link RedisScript}, using the provided {@link RedisSerializer}s to
* serialize the script arguments and result.
*
* @param script
* The script to execute
* @param argsSerializer
* The {@link RedisSerializer} to use for serializing args
* @param resultSerializer
* The {@link RedisSerializer} to use for serializing the script return value
* @param keys
* Any keys that need to be passed to the script
* @param args
* Any args that need to be passed to the script
* @return The return value of the script or null if {@link RedisScript#getResultType()} is
* null, likely indicating a throw-away status reply (i.e. "OK")
*/
<T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer,
List<K> keys, Object... args);
Boolean hasKey(K key);
void delete(K key);

View File

@@ -37,6 +37,9 @@ import org.springframework.data.redis.connection.SortParameters;
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
import org.springframework.data.redis.core.query.QueryUtils;
import org.springframework.data.redis.core.query.SortQuery;
import org.springframework.data.redis.core.script.DefaultScriptExecutor;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.script.ScriptExecutor;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationUtils;
@@ -83,6 +86,8 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
private RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = new StringRedisSerializer();
private ScriptExecutor<K> scriptExecutor;
// cache singleton objects (where possible)
private ValueOperations<K, V> valueOps;
private ListOperations<K, V> listOps;
@@ -124,6 +129,10 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
}
if(scriptExecutor == null) {
this.scriptExecutor = new DefaultScriptExecutor<K>(this);
}
initialized = true;
}
@@ -269,6 +278,15 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
});
}
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
return scriptExecutor.execute(script, keys, args);
}
public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer,
List<K> keys, Object... args) {
return scriptExecutor.execute(script, argsSerializer, resultSerializer, keys, args);
}
private Object executeSession(SessionCallback<?> session) {
return session.execute(this);
}
@@ -441,6 +459,14 @@ public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperation
this.stringSerializer = stringSerializer;
}
/**
*
* @param scriptExecutor The {@link ScriptExecutor} to use for executing Redis scripts
*/
public void setScriptExecutor(ScriptExecutor<K> scriptExecutor) {
this.scriptExecutor = scriptExecutor;
}
@SuppressWarnings("unchecked")
private byte[] rawKey(Object key) {
Assert.notNull(key, "non null key required");

View File

@@ -0,0 +1,101 @@
/*
* Copyright 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.springframework.data.redis.core.script;
import java.io.IOException;
import org.springframework.core.io.Resource;
import org.springframework.scripting.ScriptSource;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.scripting.support.StaticScriptSource;
/**
* Default implementation of {@link RedisScript}. Delegates to an underlying {@link ScriptSource} to
* retrieve script text and detect if script has been modified (and thus should have SHA1
* re-calculated). This class is best used as a Singleton to avoid re-calculation of SHA1 on every
* script execution.
*
* @author Jennifer Hickey
*
* @param <T>
* The script result type. Should be one of Long, Boolean, List, or deserialized value
* type. Can be null if the script returns a throw-away status (i.e "OK")
*/
public class DefaultRedisScript<T> implements RedisScript<T> {
private ScriptSource scriptSource;
private String sha1;
private Class<T> resultType;
private final Object shaModifiedMonitor = new Object();
/**
*
* @param scriptLocation
* The location of the script
* @param resultType
* The Script result type
*/
public DefaultRedisScript(Resource scriptLocation, Class<T> resultType) {
this(new ResourceScriptSource(scriptLocation), resultType);
}
/**
*
* @param script
* The script
* @param resultType
* The Script result type
*/
public DefaultRedisScript(String script, Class<T> resultType) {
this(new StaticScriptSource(script), resultType);
}
/**
*
* @param scriptSource
* The {@link ScriptSource} of the script
* @param resultType
* The Script result type
*/
public DefaultRedisScript(ScriptSource scriptSource, Class<T> resultType) {
this.scriptSource = scriptSource;
this.resultType = resultType;
}
public String getSha1() {
synchronized (shaModifiedMonitor) {
if (sha1 == null || scriptSource.isModified()) {
this.sha1 = DigestUtils.sha1DigestAsHex(getScriptAsString());
}
return sha1;
}
}
public Class<T> getResultType() {
return this.resultType;
}
public String getScriptAsString() {
try {
return scriptSource.getScriptAsString();
} catch (IOException e) {
throw new ScriptingException("Error reading script text", e);
}
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 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.springframework.data.redis.core.script;
import java.util.ArrayList;
import java.util.List;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* Default implementation of {@link ScriptExecutor}. Optimizes performance by attempting to execute
* script first using evalsha, then falling back to eval if Redis has not yet cached the script.
* Evalsha is not attempted if the script is executed in a pipeline or transaction.
*
* @author Jennifer Hickey
*
* @param <K>
* The type of keys that may be passed during script execution
*/
public class DefaultScriptExecutor<K> implements ScriptExecutor<K> {
private RedisTemplate<K, ?> template;
/**
*
* @param template The {@link RedisTemplate} to use
*/
public DefaultScriptExecutor(RedisTemplate<K, ?> template) {
this.template = template;
}
@SuppressWarnings("unchecked")
public <T> T execute(final RedisScript<T> script, final List<K> keys, final Object... args) {
// use the Template's value serializer for args and result
return execute(script, template.getValueSerializer(),
(RedisSerializer<T>) template.getValueSerializer(), keys, args);
}
public <T> T execute(final RedisScript<T> script, final RedisSerializer<?> argsSerializer, final RedisSerializer<T> resultSerializer,
final List<K> keys, final Object... args) {
return template.execute(new RedisCallback<T>() {
public T doInRedis(RedisConnection connection) throws DataAccessException {
final ReturnType returnType = ReturnType.fromJavaType(script.getResultType());
final byte[][] keysAndArgs = keysAndArgs(argsSerializer, keys, args);
final int keySize = keys != null ? keys.size() : 0;
if (connection.isPipelined() || connection.isQueueing()) {
// We could script load first and then do evalsha to ensure sha is present,
// but this adds a sha1 to exec/closePipeline results. Instead, just eval
connection.eval(scriptBytes(script), returnType, keySize, keysAndArgs);
return null;
}
return eval(connection, script, returnType, keySize, keysAndArgs,
resultSerializer);
}
});
}
protected <T> T eval(RedisConnection connection, RedisScript<T> script, ReturnType returnType,
int numKeys, byte[][] keysAndArgs, RedisSerializer<T> resultSerializer) {
Object result;
try {
result = connection.evalSha(script.getSha1(), returnType, numKeys, keysAndArgs);
} catch (Exception e) {
result = connection.eval(scriptBytes(script), returnType, numKeys, keysAndArgs);
}
if (script.getResultType() == null) {
return null;
}
return deserializeResult(resultSerializer, result);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected byte[][] keysAndArgs(RedisSerializer argsSerializer, List<K> keys, Object[] args) {
final int keySize = keys != null ? keys.size() : 0;
byte[][] keysAndArgs = new byte[args.length + keySize][];
int i = 0;
if(keys != null) {
for (K key : keys) {
if (keySerializer() == null && key instanceof byte[]) {
keysAndArgs[i++] = (byte[]) key;
} else {
keysAndArgs[i++] = keySerializer().serialize(key);
}
}
}
for (Object arg : args) {
if (argsSerializer == null && arg instanceof byte[]) {
keysAndArgs[i++] = (byte[]) arg;
} else {
keysAndArgs[i++] = argsSerializer.serialize(arg);
}
}
return keysAndArgs;
}
protected byte[] scriptBytes(RedisScript<?> script) {
return template.getStringSerializer().serialize(script.getScriptAsString());
}
@SuppressWarnings({ "rawtypes", "unchecked" })
protected <T> T deserializeResult(RedisSerializer<T> resultSerializer, Object result) {
if (result instanceof byte[]) {
if (resultSerializer == null) {
return (T) result;
}
return resultSerializer.deserialize((byte[]) result);
}
if (result instanceof List) {
List results = new ArrayList();
for (Object obj : (List) result) {
results.add(deserializeResult(resultSerializer, obj));
}
return (T) results;
}
return (T) result;
}
@SuppressWarnings("rawtypes")
protected RedisSerializer keySerializer() {
return template.getKeySerializer();
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 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.springframework.data.redis.core.script;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Utilties for working with {@link MessageDigest}
*
* @author Jennifer Hickey
*
*/
abstract public class DigestUtils {
private static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f' };
private static final Charset UTF8_CHARSET = Charset.forName("UTF8");
/**
* Returns the SHA1 of the provided data
*
* @param data
* The data to calculate, such as the contents of a file
* @return The human-readable SHA1
*/
public static String sha1DigestAsHex(String data) {
byte[] dataBytes = getDigest("SHA").digest(data.getBytes(UTF8_CHARSET));
return new String(encodeHex(dataBytes));
}
private static char[] encodeHex(byte[] data) {
int l = data.length;
char[] out = new char[l << 1];
for (int i = 0, j = 0; i < l; i++) {
out[j++] = HEX_CHARS[(0xF0 & data[i]) >>> 4];
out[j++] = HEX_CHARS[0x0F & data[i]];
}
return out;
}
/**
* Creates a new {@link MessageDigest} with the given algorithm. Necessary because
* {@code MessageDigest} is not thread-safe.
*/
private static MessageDigest getDigest(String algorithm) {
try {
return MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("Could not find MessageDigest with algorithm \""
+ algorithm + "\"", ex);
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 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.springframework.data.redis.core.script;
/**
* A script to be executed using the <a href="http://redis.io/commands/eval">Redis scripting
* support</a> available as of version 2.6
*
* @author Jennifer Hickey
*
* @param <T>
* The script result type. Should be one of Long, Boolean, List, or deserialized value
* type. Can be null if the script returns a throw-away status (i.e "OK")
*/
public interface RedisScript<T> {
/**
*
* @return The SHA1 of the script, used for executing Redis evalsha command
*/
String getSha1();
/**
*
* @return The script result type. Should be one of Long, Boolean, List, or deserialized value
* type. Can be null if the script returns a throw-away status (i.e "OK")
*/
Class<T> getResultType();
/**
*
* @return The script contents
*/
String getScriptAsString();
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 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.springframework.data.redis.core.script;
import java.util.List;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* Executes {@link RedisScript}s
*
* @author Jennifer Hickey
*
* @param <K>
* The type of keys that may be passed during script execution
*/
public interface ScriptExecutor<K> {
/**
* Executes the given {@link RedisScript}
*
* @param script
* The script to execute
* @param keys
* Any keys that need to be passed to the script
* @param args
* Any args that need to be passed to the script
* @return The return value of the script or null if {@link RedisScript#getResultType()} is
* null, likely indicating a throw-away status reply (i.e. "OK")
*/
<T> T execute(RedisScript<T> script, List<K> keys, Object... args);
/**
* Executes the given {@link RedisScript}, using the provided {@link RedisSerializer}s to
* serialize the script arguments and result.
*
* @param script
* The script to execute
* @param argsSerializer
* The {@link RedisSerializer} to use for serializing args
* @param resultSerializer
* The {@link RedisSerializer} to use for serializing the script return value
* @param keys
* Any keys that need to be passed to the script
* @param args
* Any args that need to be passed to the script
* @return The return value of the script or null if {@link RedisScript#getResultType()} is
* null, likely indicating a throw-away status reply (i.e. "OK")
*/
<T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer,
List<K> keys, Object... args);
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 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.springframework.data.redis.core.script;
import org.springframework.core.NestedRuntimeException;
/**
* {@link RuntimeException} thrown when issues occur with {@link RedisScript}s
*
* @author Jennifer Hickey
*
*/
@SuppressWarnings("serial")
public class ScriptingException extends NestedRuntimeException {
/**
* Constructs a new <code>ScriptingException</code> instance.
*
* @param msg
* @param cause
*/
public ScriptingException(String msg, Throwable cause) {
super(msg, cause);
}
/**
* Constructs a new <code>ScriptingException</code> instance.
*
* @param msg
*/
public ScriptingException(String msg) {
super(msg);
}
}