接口定义要点
- 防止忘记释放资源:防呆
- 给业务一个锁超时的感知:在某些情况下,使用者需要给定超时的资源回滚处理
接口定义如下:
package hww.utils.redis;
import java.util.function.Supplier;
/**
* redisLock提供资源保护功能,防止获取锁后忘记关闭。同时RedisLockAcquiredTimeOutException,提供了锁释放失败的提示
*
* @author hanweiwei
* @see RedisLock#doWithLock(Supplier, Long)
*/
public interface RedisLock {
Long defaultExpireMills = 5000L;
String getKey();
//防止忘记关闭资源
<T> ResultHolder<T> doWithLock(Supplier<T> supplier, Long expireMills) throws RedisLockAcquiredTimeOutException;
/**
* 业务端有必要处理该异常
*/
class RedisLockAcquiredTimeOutException extends RuntimeException {
private ResultHolder resultHolder;
public RedisLockAcquiredTimeOutException(ResultHolder resultHolder) {
this.resultHolder = resultHolder;
}
public ResultHolder getResultHolder() {
return resultHolder;
}
}
class ResultHolder<T> {
//业务逻辑处理过程中产生的异常
private Throwable err;
//是否获取到redis锁
private boolean lockAcquired = false;
//返回结果
private T result;
public Throwable getErr() {
return err;
}
public void setErr(Throwable err) {
this.err = err;
}
public boolean isLockAcquired() {
return lockAcquired;
}
public void setLockAcquired(boolean lockAcquired) {
this.lockAcquired = lockAcquired;
}
public T getResult() {
return result;
}
public void setResult(T result) {
this.result = result;
}
}
}
给定一个基于spring的实现
package hww.utils.redis.spring;
import hww.utils.redis.RedisLock;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;
/**
* 基于spring的redis处理
*
* @author hanweiwei
*/
public class SpringRedisLock implements RedisLock, ApplicationContextAware {
private RedisTemplate redisTemplate;
private ApplicationContext applicationContext;
private String key;
private List<String> keyList = new ArrayList<>(1);
private static final RedisScript<String> SCRIPT_LOCK = new DefaultRedisScript<>("return redis.call('set',KEYS[1],ARGV[1],'NX','PX',ARGV[2])", String.class);
private static final RedisScript<String> SCRIPT_UNLOCK = new DefaultRedisScript<>("if redis.call('get',KEYS[1]) == ARGV[1] then return tostring(redis.call('del',KEYS[1])==1) else return '0' end", String.class);
private String id;
public SpringRedisLock(RedisTemplate redisTemplate, String key) {
this.redisTemplate = redisTemplate;
this.key = key;
keyList.add(key);
}
public SpringRedisLock(String key) {
this.key = key;
keyList.add(key);
}
@Override
public String getKey() {
return key;
}
@SuppressWarnings("unchecked")
protected boolean tryLock(long expireMillis) {
id = UUID.randomUUID().toString();
String r = (String) redisTemplate.execute(SCRIPT_LOCK, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), keyList, id, String.valueOf(expireMillis));
return "OK".equalsIgnoreCase(r);
}
@SuppressWarnings("unchecked")
protected boolean release() {
String result = (String) getRedisTemplate().execute(SCRIPT_UNLOCK, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), keyList, id);
boolean released = "1".equals(result);
id = null;
return released;
}
@Override
public <T> ResultHolder<T> doWithLock(Supplier<T> supplier, Long expireMills) throws RedisLockAcquiredTimeOutException {
ResultHolder<T> resultHolder = new ResultHolder<>();
if (tryLock(expireMills == null || expireMills <= 0 ? defaultExpireMills : expireMills)) {
resultHolder.setLockAcquired(true);
try {
T r = supplier.get();
resultHolder.setResult(r);
} catch (Exception e) {
resultHolder.setErr(e);
}
if (!release()) {
throw new RedisLockAcquiredTimeOutException(resultHolder);
}
}
return resultHolder;
}
protected RedisTemplate getRedisTemplate() {
if (redisTemplate == null && applicationContext != null) {
redisTemplate = applicationContext.getBean(RedisTemplate.class);
}
return redisTemplate;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
package hww.utils.redis.spring;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringRedisLockFactory implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware {
private ApplicationContext applicationContext;
public SpringRedisLock getSpringRedisLock(String key) {
return applicationContext.getBean(SpringRedisLock.class, key);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
if (!beanDefinitionRegistry.containsBeanDefinition(SpringRedisLock.class.getName())) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(SpringRedisLock.class);
beanDefinitionBuilder.setScope("prototype");
beanDefinitionRegistry.registerBeanDefinition(SpringRedisLock.class.getName(), beanDefinitionBuilder.getBeanDefinition());
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
}
public class SpringRedisExample {
@Autowired
private SpringRedisLockFactory springRedisLockFactory;
/**
* function sum(a,b) protected by redis lock
*
* @return a+b
*/
public Integer sum(Integer a, Integer b) {
RedisLock redisLock = springRedisLockFactory.getSpringRedisLock("key");
try {
return redisLock.doWithLock(() -> {
//业务逻辑处理
return a + b;
}, 1000L).getResult();
} catch (RedisLock.RedisLockAcquiredTimeOutException ex) {
//回滚逻辑,也可以忽略这个异常
}
return null;
}
}
在某些常见下,我们获取不到锁的情况下需要重试获取,一个基于spring retry的模板如下
package hww.utils.redis.spring;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.listener.RetryListenerSupport;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import java.util.function.Supplier;
/**
* 提供了基于spring retry的重试功能
*
* @author hanweiwei
*/
public class StringRetryAbleRedisLock extends SpringRedisLock {
private static final int retryTimes = 3;
private static final SimpleRetryPolicy SIMPLE_RETRY_POLICY = new SimpleRetryPolicy(retryTimes);
private static final FixedBackOffPolicy FIXED_BACK_OFF_POLICY;
private static final RetryListener RETRY_LISTENER = new RetryListenerSupport() {
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
if (throwable instanceof RedisLockAcquiredTimeOutException) {
throw (RedisLockAcquiredTimeOutException) throwable;
}
}
};
static {
FIXED_BACK_OFF_POLICY = new FixedBackOffPolicy();
FIXED_BACK_OFF_POLICY.setBackOffPeriod(100);
}
public StringRetryAbleRedisLock(String key) {
super(key);
}
public StringRetryAbleRedisLock(RedisTemplate redisTemplate, String key) {
super(redisTemplate, key);
}
@SuppressWarnings("unckecked")
public <T> ResultHolder<T> doWithRetry(Supplier<T> supplier, Long expireMills, RetryPolicy retryPolicy, BackOffPolicy backOffPolicy) throws RedisLockAcquiredTimeOutException {
RetryTemplate retryTemplate = new RetryTemplate();
if (retryPolicy == null) {
retryPolicy = SIMPLE_RETRY_POLICY;
}
if (backOffPolicy == null) {
backOffPolicy = FIXED_BACK_OFF_POLICY;
}
retryTemplate.setBackOffPolicy(backOffPolicy);
retryTemplate.setRetryPolicy(retryPolicy);
ResultHolder<T> resultHolder = null;
retryTemplate.setListeners(new RetryListener[]{RETRY_LISTENER});
try {
resultHolder = (ResultHolder<T>) retryTemplate.execute((RetryCallback<Object, Throwable>) retryContext -> doWithLock(supplier, expireMills));
} catch (Throwable throwable) {
resultHolder = new ResultHolder<>();
resultHolder.setLockAcquired(false);
}
return resultHolder;
}
}
具体程序参见https://github.com/skyhww/java-util/tree/main/src/main/java/hww/utils/redis