Redis分布式锁

在分布式环境中,实现分布式锁是比较复杂的,不能像传统的 synchronizedReentrantLock 这种使用进程锁的方式,因为如果你的项目部署了多个副本,使用这种方式上锁,都是锁定的当前进程的锁对象,如果请求进入不同的副本中,对不同的副本锁是无效的。

Redis分布式锁的问题

不设置过期时间

我们知道,可以使用 Redis 的 setNx 命令实现 Redis的分布式锁,使用 Redis 的 lua 脚本实现解锁, Spring data Redis中的 RedisConnection#setNX(byte[] key, byte[] value) 接收两个参数,分别为 key,和 value,此方法不能设置key的过期时间。如果key不设置过期时间,就可能造成死锁,如应用程序没有正常的解锁,就会导致后续的需要获取此key的线程一直拿不到锁,造成死锁。所以,应该给这个key设置合理的过期时间。

过期时间设置太短

同样,如果一个key的过期时间设置太短,被锁定的临界区代码还没有执行完,key就自动过期了,会导致其它线程也能获取到这个key的锁,此时,两个线程同时进入被锁定的临界区代码块中,锁失效。

配合 @Transactional 事物注解使用不当的影响

如下一段代码,在高并发的情况会出现问题,解决方法:

  • lock.lock(); 加锁的逻辑放在调用此方法调用者,谁要调用此方法,自己先加锁,在加锁的临界区代码中调用此方法。

    如果有100个地方要调用此方法,那就要写100遍lock.lock()类似的代码,你会非常不爽…

  • 其实,我们只要保证 lock.lock() 加锁方法在事物AOP 拦截之前执行,且lock.unlock()解锁方法在事物 AOP 拦截提交之后执行即可

@Override
@Transactional
public void transactionalAndRedisLockTest(String keyId) {
    Lock lock = new RedisLock("transactionalAndRedisLockTest:" + keyId);
    lock.lock();
    try {
    	// 执行业务逻辑,先查询数据库记录,判断库存是否满足,如果满足减去库存,再执行update操作
    } finally {
    // 执行完业务逻辑,解锁后,此时事物还没有提交(因为有Transactional,需要整个方法执行完后,才会提交事物),数据库的记录并没有发生变化,
    // 此时,另一个线程是可以获取到锁的,那它可以进入到try语句中,如果这个线程查询是前一个线程还没有提交的数据,此时,锁的意义失效
        lock.unlock();
    }
}

终极Redis分布式锁的代码实现

定时任务

此定时任务是定时刷新key过期时间,防止 key设置的过期时间太短,业务方法还没有执行完,导致其它线程进入加锁的临界区代码中。

package com.hk.core.redis.locks;

import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 定义此Schedule,是防止Redis 锁key过期时间设置太短,导致key自动过期后,加锁的代码块还没有执行完成,导致其它线程也获取锁
 *
 * @author Kevin
 */
class RedisLockSchedule {

   private static final ConcurrentHashMap<String, ScheduledFuture<?>> FUTURE_MAP;

    private static final ScheduledThreadPoolExecutor POOL_EXECUTOR;

    static {
        var creator = new CustomizableThreadCreator();
        creator.setDaemon(false);
        creator.setThreadGroupName("Redis-Lock-");
        POOL_EXECUTOR = new ScheduledThreadPoolExecutor(2, creator::createThread);
        FUTURE_MAP = new ConcurrentHashMap<>(128);
    }

    static void register(String key, Runnable runnable, long period, TimeUnit timeUnit) {
        ScheduledFuture<?> fixedRate = POOL_EXECUTOR.scheduleAtFixedRate(runnable, 0, period, timeUnit);
        ScheduledFuture<?> oldFuture = FUTURE_MAP.put(key, fixedRate);
        if (Objects.nonNull(oldFuture)) {
            oldFuture.cancel(true);
        }
    }

    static void cancel(String key) {
        ScheduledFuture<?> future = FUTURE_MAP.remove(key);
        if (Objects.nonNull(future)) {
            future.cancel(true);
        }
    }
}

Redis锁实现核心类
------------------------------------------------------------------------
package com.hk.core.redis.locks;

import com.hk.commons.util.ArrayUtils;
import com.hk.commons.util.Lazy;
import com.hk.commons.util.ObjectUtils;
import com.hk.commons.util.SpringContextHolder;
import com.hk.core.redis.scripting.RedisScriptSource;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalTime;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 基于 Redis setNx 与 lua 脚本实现分布式锁,支持可重入。
 *
 * @author Kevin
 */
public class RedisLock implements Lock {

    /**
     * 线程睡眠时间,单位:毫秒
     */
    private static final long SLEEP_TIME = 10;

    /**
     * <pre>
     * 默认过期时间: 20 秒
     * </pre>
     */
    private static final long EXPIRE_SECONDS = 20;
    /**
     * lua 脚本内容,lua 脚本能保证原子性执行
     */
    private static final DefaultRedisScript<Long> LUA_SCRIPT;

    /**
     * 非事物的 redisTemplate
     */
    private final static Lazy<StringRedisTemplate> STRING_REDIS_TEMPLATE_LAZY =
            Lazy.of(() -> SpringContextHolder.getBean("stringRedisTemplateDisabledTransactionSupport",
                    StringRedisTemplate.class));

    static {
        LUA_SCRIPT = new DefaultRedisScript<>();
        /*
        * 这里 LOCK是一段lua脚本,内容为:
        *    if redis.call("get",KEYS[1]) == ARGV[1] then 
        *        return redis.call("del",KEYS[1])
        *    else
        *        return 0
        *     end
        */
        LUA_SCRIPT.setScriptSource(RedisScriptSource.LOCK);
        LUA_SCRIPT.setResultType(Long.class);
    }

    /**
     * redis Key
     */
    private final String key;
    /**
     * key 过期时间,防止死锁
     */
    private final long expire;


    /**
     * 默认过期时间 2 秒
     *
     * @param key key
     */
    public RedisLock(String key) {
        this(key, EXPIRE_SECONDS);
    }

    /**
     * 默认过期时间 2 秒
     *
     * @param key key
     */
    public RedisLock(String key, long expire) {
        this.key = key;
        this.expire = expire <= 0 ? EXPIRE_SECONDS : expire;
    }

    /**
     * 获取锁
     */
    @Override
    public void lock() {
        while (!tryLock()) {
            if (SLEEP_TIME >= 0) {
                try {
                    TimeUnit.MILLISECONDS.sleep(SLEEP_TIME);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    /**
     * 可被打断的获取 lock
     */
    @Override
    public void lockInterruptibly() throws InterruptedException {
        if (Thread.interrupted()) {//如果当前线程已中段
            throw new InterruptedException();
        }
        while (!tryLock()) {
            TimeUnit.MILLISECONDS.sleep(SLEEP_TIME);
        }
    }

    /**
     * 尝试获取锁,立即返回。如果返回  true ,加锁成功
     *
     * @return true or false
     */
    @Override
    public boolean tryLock() {
        /*
         * 只有当Key 不存在时,设置key的值,
         *  注意,redisTemplate.setIfAbsent方法,在 pipeline / transaction 的方法调用此方法时, 会返回 null,
         *  所以这里使用  stringRedisTemplateDisabledTransactionSupport 的 redisTemplate
         */
        var result = ObjectUtils.defaultIfNull(STRING_REDIS_TEMPLATE_LAZY.get().opsForValue().setIfAbsent(key,
                Long.toString(Thread.currentThread().getId()), expire, TimeUnit.SECONDS), Boolean.FALSE);
        if (result && expire > 1) { //过期时间大于1秒才设置开启线程定时刷新过期时间
            registerSchedule();
        }
        return result;
    }

    /**
     * 初始化线程并启动
     */
    private void registerSchedule() {
        cancelSchedule();
        long period;
        TimeUnit timeUnit;
        if (expire > 2) {
            period = expire - 2;
            timeUnit = TimeUnit.SECONDS;
        } else {
            period = 500;
            timeUnit = TimeUnit.MILLISECONDS;
        }
        RedisLockSchedule.register(key, () -> {
            //刷新过期时间,如果没有成功,可能是key不存在了,直接结束此任务
            if (!ObjectUtils.defaultIfNull(STRING_REDIS_TEMPLATE_LAZY.get().expire(key, expire, TimeUnit.SECONDS),
                    Boolean.FALSE)) {
                cancelSchedule();
            }
        }, period, timeUnit);
    }

    private void cancelSchedule() {
        RedisLockSchedule.cancel(key);
    }

    /**
     * 在指定的时间段获取锁,超出指定的时间立即返回
     */
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (Thread.interrupted()) {
            throw new InterruptedException();
        }
        long max = System.nanoTime() + unit.toNanos(time);
        while (System.nanoTime() < max) {
            if (tryLock()) {
                return true;
            }
        }
        return false;
    }

    /**
     * 解锁
     */
    @Override
    public void unlock() {
        STRING_REDIS_TEMPLATE_LAZY.get().execute(LUA_SCRIPT, ArrayUtils.asArrayList(key),
                Long.toString(Thread.currentThread().getId()));
        cancelSchedule();
    }

    @Override
    public Condition newCondition() {
        throw new UnsupportedOperationException("Redis Lock newCondition");
    }
}
Redis锁使用注解方式
package com.hk.core.redis.annotations;

import java.lang.annotation.*;

/**
 * redis 锁使用注解的方式
 *
 * @author Kevin
 * @see com.hk.core.redis.locks.RedisLock
 * @see com.hk.core.redis.aspect.RedisLockInterceptor
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {

    /**
     * <pre>
     * 锁的 Key,支持 SPEL 表达式
     * SPEL格式如:
     *      #参数名,如方法为method(int value) ,则可以使用 #value 获取
     *      也可以使用计算公式: 如 "1+1" -> 结果为2
     *      方法参数也可以使用对象,如 method(int value, Object obj) ,如果想以obj的属性作为key,可以写成 #obj.属性名。
     *      如果方法参数有数组,如 method(int[]arr) ,可以使用 #arr[0] 获取第几个元素作为key
     *      如果方法参数有map,如 method(int[]arr,Map<String,Object> map) ,可以使用 #map[key] 获取map指定key的值作为锁的key
     *      如果方法参数有list,如 method(List<String> list) ,可以使用 #list[0] 获取list指定的索引作为锁的key
     * </pre>
     */
    String key();

    /**
     * 过期时间,单位为秒,默认20 秒
     */
    long expireSeconds() default 20;
}

Redis锁注解 AOP

注意:类上加了 @Order 注解,此AOP 会在 TransactionInterceptor 的前面。

package com.hk.core.redis.aspect;

import com.hk.commons.util.StringUtils;
import com.hk.core.redis.annotations.RedisLock;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;

import java.util.Objects;

/**
 * <pre>
 *
 * 加 Order 注解,必需要在 事务之前执行(也就是这个Aspect 拦截器要在 {@link org.springframework.transaction.interceptor.TransactionInterceptor} 之前),事务提交之后,再释放类。
 *      先使用redis上锁、再开启事物、再执行业务代码,再提交事物 ,再释放redis 锁,才能让下一个线程能获取到锁。
 * </pre>
 * <p>
 * {@link RedisLock} aspect
 *
 * @author Kevin
 */
@Aspect
@Order(value = Integer.MIN_VALUE)
public class RedisLockInterceptor {

    @Pointcut("@annotation(com.hk.core.redis.annotations.RedisLock)")
    public void redisLock() {
    }

    @Around("redisLock()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        if (pjp.getSignature() instanceof MethodSignature signature) {
            var method = signature.getMethod();
            var redisLock = method.getAnnotation(RedisLock.class);
            if (Objects.nonNull(redisLock)) {
                String key = redisLock.key();
                var parser = new SpelExpressionParser();
                var parameters = signature.getMethod().getParameters();
                var args = pjp.getArgs();
                var builder = SimpleEvaluationContext.forReadWriteDataBinding();
                int argLen = args.length;
                if (argLen > 1) {
                    builder.withRootObject(args[0]);
                }
                var context = builder.build();
                for (int i = 0; i < argLen; i++) {
                    context.setVariable(parameters[i].getName(), args[i]);
                }
                try {
                    key = parser.parseExpression(redisLock.key()).getValue(context, String.class);
                } catch (Exception e) {
                    // can not parse spel,use original key.
                }
                if (StringUtils.isEmpty(key)) {
                    key = pjp.getTarget().getClass().getName() + method.getName();
                }
                var lock = new com.hk.core.redis.locks.RedisLock(key, redisLock.expireSeconds());
                lock.lock();
                try {
                    return pjp.proceed();
                } finally {
                    //保证释放锁时,事物已提交
                    lock.unlock();
                }
            }
        }
        return pjp.proceed();
    }
}

测试
基于注解方式
@Transactional
@RedisLock(key = "#keyId")
public void transactionalAndRedisLockTest(String keyId) {
   //您的业务逻辑	
}
基于编程式

主要是使用 transactionTemplate 执行

private final TransactionTemplate transactionTemplate;

/**
* 编程式方式
*/
public void transactionalAndRedisLockTest(String keyId) {
    Lock lock = new RedisLock(keyId);
    lock.lock();
    try {
        transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus status) {
                //要执行的业务逻辑
                return null;
            }
        });
    } finally {
        lock.unlock();
    }
}
### Redis 分布式锁的实现方式、使用方法及最佳实践 #### ### 1. Redis 分布式锁的基本原理 Redis 分布式锁的核心思想是利用 Redis 的原子性操作来确保锁的唯一性。通过 Redis 的 `SET` 命令,结合参数 `NX` 和 `EX`,可以在多线程环境下实现加锁和解锁的功能[^1]。此外,为了提高可用性,还可以采用 RedLock 算法或多实例部署的方式。 #### ### 2. Redis 分布式锁的实现方式 #### #### 2.1 单实例 Redis 实现分布式锁 单实例 Redis 实现分布式锁是最简单的实现方式。通过以下命令完成加锁和解锁操作: ```python import time import redis # 初始化 Redis 客户端 client = redis.StrictRedis(host='localhost', port=6379, db=0) lock_key = "distributed_lock" lock_value = "unique_identifier" # 加锁操作 def acquire_lock(): result = client.set(lock_key, lock_value, nx=True, ex=10) # 设置过期时间为 10 秒 return result is not None # 解锁操作 def release_lock(): script = """ if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end """ client.eval(script, 1, lock_key, lock_value) ``` 上述代码中,`nx=True` 确保只有当键不存在时才设置键值对,从而实现加锁功能。`ex=10` 参数为锁设置了 10 秒的过期时间,防止死锁的发生[^1]。 #### #### 2.2 多实例 Redis 实现分布式锁(RedLock 算法) 在高可用场景下,可以使用 RedLock 算法实现分布式锁。RedLock 算法通过多个 Redis 实例来确保锁的可靠性。以下是 RedLock 的伪代码实现: ```python import redis import time class RedLock: def __init__(self, redis_nodes): self.redis_nodes = [redis.StrictRedis(**node) for node in redis_nodes] def acquire_lock(self, lock_key, lock_value, ttl): quorum = len(self.redis_nodes) // 2 + 1 start_time = time.time() success_count = 0 for node in self.redis_nodes: if node.set(lock_key, lock_value, nx=True, px=ttl): success_count += 1 elapsed_time = time.time() - start_time validity_time = ttl - int(elapsed_time * 1000) if success_count >= quorum and validity_time > 0: return True, validity_time else: self.release_lock(lock_key, lock_value) return False, 0 def release_lock(self, lock_key, lock_value): for node in self.redis_nodes: try: script = """ if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end """ node.eval(script, 1, lock_key, lock_value) except Exception: pass ``` RedLock 算法要求在大多数 Redis 实例上成功加锁,并且整个过程的时间小于锁的有效期,才能认为加锁成功[^3]。 #### ### 3. Redis 分布式锁的最佳实践 #### #### 3.1 设置合理的锁超时时间 为了避免死锁问题,必须为锁设置一个合理的超时时间。如果锁持有者在超时时间内未完成任务,锁将自动释放[^1]。 #### #### 3.2 使用唯一的锁标识符 在加锁时,应为每个锁分配一个唯一的标识符(如 UUID),以便在解锁时验证锁的拥有者身份,防止误删其他线程的锁[^3]。 #### #### 3.3 防止 GC 停顿导致锁失效 Java 程序中的垃圾回收(GC)可能导致线程长时间暂停,从而使锁提前释放。为了解决这一问题,可以使用续租机制,在锁即将到期时主动延长锁的有效期。 #### #### 3.4 监控锁的竞争情况 在高并发场景下,可以通过监控锁的竞争情况来优化系统性能。例如,记录加锁失败的次数或等待时间,分析是否存在锁争用问题[^1]。 #### ### 4. 示例代码:基于 Redisson 的分布式锁实现 Redisson 是一个成熟的 Redis 客户端库,提供了丰富的分布式锁功能。以下是使用 Redisson 实现分布式锁的示例代码: ```java import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; public class RedissonLockExample { public static void main(String[] args) throws InterruptedException { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); RLock lock = redisson.getLock("myDistributedLock"); lock.lock(); // 加锁 try { // 执行业务逻辑 System.out.println("Lock acquired, performing task..."); Thread.sleep(1000); // 模拟任务执行 } finally { lock.unlock(); // 解锁 System.out.println("Lock released."); } redisson.shutdown(); } } ``` Redisson 提供了多种锁类型,包括公平锁、可重入锁和红锁(RedLock),开发者可以根据实际需求选择合适的锁类型[^3]。 #### ### 5. 注意事项 - 在高并发场景下,应尽量减少锁的粒度,避免因锁竞争导致性能下降。 - 如果 Redis 实例发生故障,可能会导致锁丢失。因此,在关键业务场景下,建议使用哨兵模式或集群模式来提高 Redis 的可用性[^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hellowordx007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值