一个redis锁的实现

接口定义要点

  • 防止忘记释放资源:防呆
  • 给业务一个锁超时的感知:在某些情况下,使用者需要给定超时的资源回滚处理

接口定义如下:

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值