Redis分布式锁处理高可用并发修改问题

Redis分布式锁处理高可用并发修改问题

看到网上说了三种分布式锁的做法,综合下来觉得redis比较好,也熟悉方便处理:
a.使用redis分布式锁,主要使用了redis中的setnx和getset方法,分别对应redisTemplate的setIfAbsent和getAndSet方法
b.Redis是单线程(Redis内部是单线程,并且数据存在内存中,也就是说redis内部执行命令是不会有多线程同步问题的)
c.能保证线程的安全性,而且redis强大的读写能力能提高效率,也相对容易实现

如下,代码还未用于生产环境测试过,超时时间等需要根据自己的情况调整.
1.这里使用的是线程ID来标识锁,这样也比使用其他的内容更方便做线程的排他处理
2.看到网上几乎都没有使用Redis的过期时间的,不知道是用的Redis的版本问题还是什么原因
3.我用的是Redis含有超时参数的方法,从源码可以看出,还是使用的是和set的时候设置过期时间是一样的
4.StringRedisTemplate是继承RedisTemplate的,只是value指定只序列化String类型的数据,方便使用,避免强转的问题**

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import java.time.Duration;

/**
 * * 基于Redis的分布式锁(处理高可用服务的并发修改问题)
 * * <p>高可用--多个服务(微服务)</p>
 *
 * @author yunzhi-yangbiao
 * @date 2019/12/16
 */
@Slf4j
@Configuration
@AllArgsConstructor
public class RedisDistributedLock {
    private static int TIMEOUT_SECONDS = 25;
    private static int DEFAULT_TIMEOUT_SECONDS = 3;
    private final StringRedisTemplate redisTemplate;

    /**
     * @param loginUserId 当前操作的用户的ID
     * @param duration    超时时间(释放锁时间/单位:秒)
     * @return
     */
    public boolean addLock(String loginUserId, Duration duration) {
        //判断用户ID是否为空
        if (!StringUtils.hasText(loginUserId)) {
            throw new IllegalArgumentException("当前登陆用户ID不能为空");
        }
        //做个容错处理,防止因为设置过长的超时时间导致程序被长时间阻塞,而影响整个程序,如果有
        if (duration.getSeconds() > TIMEOUT_SECONDS) {
            throw new IllegalArgumentException("您的超时时间可能过长了");
        }
        //获取当前线程的ID作为value,这样可以很好的排开其他的线程
        String threadId = Long.toString(Thread.currentThread().getId());
        //setIfAbsent--如果不存在则set并设置超时时间(防止死锁),成功set返回true,否则返回false
        if (redisTemplate.opsForValue().setIfAbsent(loginUserId, threadId, duration)) {
            //设置值成功,也就是加锁成功
            return true;
        }
        //如果锁已经获取了(获取验证了锁的线程再未超时的情况下,还可以获取锁),获取redis里存储的线程ID
        String lockThreadId = redisTemplate.opsForValue().get(loginUserId);
        //判断获取到的线程ID是否已经失效(如果没失效,持有锁的线程是可以继续执行程序的,不能中断其运行)
        if (StringUtils.hasText(lockThreadId) && lockThreadId.equals(threadId)) {
            //如果从redis上获取的线程就是当前线程,那么返回true,让程序继续往下执行,否则返回false,中断程序
            return true;
        } else {
            //没有得到锁
            return false;
        }
    }

    /**
     * 这是使用默认超时的加锁方法
     *
     * @param loginUserId 当前操作的用户的ID
     * @return
     */
    public boolean defaultTimeoutLock(String loginUserId) {
        return addLock(loginUserId, Duration.ofSeconds(DEFAULT_TIMEOUT_SECONDS));
    }

    /**
     * @param loginUserId
     */
    public void unlock(String loginUserId) {
        try {
            //获取程序当前线程
            String currentThread = Long.toString(Thread.currentThread().getId());
            //获取redis里存储的线程ID
            String threadId = redisTemplate.opsForValue().get(loginUserId);
            //判断当前需要释放锁的线程是否是获取到锁的线程(redis里存储的线程就是获取到锁的线程)
            if (StringUtils.hasText(threadId) && currentThread.equals(threadId)) {
                //是当前线程加的锁,所以直接释放锁
                redisTemplate.opsForValue().getOperations().delete(loginUserId);
            }
        } catch (Exception e) {
            log.info("释放锁unlock()方法异常=={}", e.getMessage());
        }
    }
}

希望对您有帮助.