Redis实现分布式锁

背景

之前遇到了一个情况,接口重复提交,出现了脏数据。如何避免这个情况呢?方案一是前端请求写数据的接口后,将按钮置灰,短时间内不可点击,但是这样也没办法从根本上解决问题。方案二是后端对接口加个锁,对请求的重要参数加一个唯一标识,锁未被释放之前,重复提交获取不到锁,也就不会有脏数据了。

实现

逻辑

  • 加锁:其实就是key-value放到redis中,key是业务唯一键,value存储UUID或者什么也是唯一标记的东西
  • 释放锁:将key-value从Redis中删除。为啥需要value存储UUID呢,作用是来区分不同线程的。线程A加锁,线程来B释放锁,就不得行,会出现误删的问题。

依赖

        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
		    <version>1.5.20.RELEASE</version>
        </dependency>

配置

spring:
  redis:
    pool.max-active: 8
    pool.max-wait: -1
    pool.max-idle: 8
    pool.min-idle: 0
    timeout: 0
    isCacheEnabled: true

代码

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisCommands;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;



@Slf4j
public abstract class AbstractRedisDistributedLock {

    /**
     * 锁过期时间
     */
    long LOCK_TIMEOUT_MILLIS = 10000;

    /**
     * 尝试获取锁次数
     */
    int LOCK_RETRY_TIMES = 10;

    /**
     * 获取锁休眠时间
     */
    long LOCK_SLEEP_MILLIS = 500;


    private RedisTemplate<String, Object> redisTemplate;

    private ThreadLocal<Map<String, String>> lockThreadLocal = new ThreadLocal<>();

    protected static final String UNLOCK_LUA_STR;

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA_STR = sb.toString();
    }

    public boolean lock(String key) {
        return this.lock(key, LOCK_TIMEOUT_MILLIS, LOCK_RETRY_TIMES, LOCK_SLEEP_MILLIS);
    }

    public boolean lock(String key, int retryTimes) {
        return this.lock(key, LOCK_TIMEOUT_MILLIS, retryTimes, LOCK_SLEEP_MILLIS);
    }

    public boolean lock(String key, int retryTimes, long sleepMillis) {
        return this.lock(key, LOCK_TIMEOUT_MILLIS, retryTimes, sleepMillis);
    }

    public boolean lock(String key, long expire) {
        return this.lock(key, expire, LOCK_RETRY_TIMES, LOCK_SLEEP_MILLIS);
    }

    public boolean lock(String key, long expire, int retryTimes) {
        return this.lock(key, expire, retryTimes, LOCK_SLEEP_MILLIS);
    }

    public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
        boolean result = setRedisLock(key, expire);
        // 如果获取锁失败,按照传入的重试次数进行重试
        while ((!result) && retryTimes-- > 0) {
            try {
                log.debug("redisDistributedLock setRedisLock failed for key: {} retryTimes: {}", key, retryTimes);
                Thread.sleep(sleepMillis);
            } catch (InterruptedException e) {
                log.error("redisDistributedLock setRedisLock sleep failed for key: {} retryTimes: {}", key, retryTimes);
                return false;
            }
            result = setRedisLock(key, expire);
        }
        return result;
    }


    /**
     * 释放锁
     * 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
     *
     * @param key
     * @return
     */
    public boolean releaseLock(String key) {
        // 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
        try {
            List<String> keys = new ArrayList<>();
            keys.add(key);
            List<String> args = new ArrayList<>();
            args.add(lockThreadLocal.get().get(key));

            // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
            Long result = getRedisTemplate().execute((RedisCallback<Long>) connection -> {
                Object nativeConnection = connection.getNativeConnection();
                // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
                // 集群模式
                if (nativeConnection instanceof JedisCluster) {
                    return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA_STR, keys, args);
                }
                // 单机模式
                else if (nativeConnection instanceof Jedis) {
                    return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA_STR, keys, args);
                }
                return 0L;
            });
            return result != null && result > 0;
        } catch (Exception e) {
            log.error("redisDistributedLock releaseLock failed for key: {} error: {}", key, e.getMessage(), e);
        } finally {
            lockThreadLocal.get().remove(key);
            if (lockThreadLocal.get().size() <= 0) {
                lockThreadLocal.remove();
            }
        }
        return false;
    }


    /**
     * 通过 redis命令 SET key value [EX seconds] [PX milliseconds] [NX|XX]
     * 设置redis 锁随机内容防止 方法执行时间过长  错杀别的线程获取的锁
     *
     * @param key
     * @param expireTime
     * @return
     */
    private boolean setRedisLock(String key, long expireTime) {
        try {
            String result = getRedisTemplate().execute((RedisCallback<String>) connection -> {
                JedisCommands jedisCommands = (JedisCommands) connection.getNativeConnection();
                String uuid = UUID.randomUUID().toString();
                Map<String, String> map = Optional.ofNullable(lockThreadLocal.get()).orElse(new ConcurrentHashMap<>());
                map.put(key, uuid);
                lockThreadLocal.set(map);
                return jedisCommands.set(key, uuid, "NX", "PX", expireTime);
            });
            return !StringUtils.isEmpty(result);
        } catch (Exception e) {
            log.error("redisDistributedLock setRedisLock occured an error: {} for key: {}", e.getMessage(), key, e);
            return false;
        }
    }

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public RedisTemplate<String, Object> getRedisTemplate() {
        return redisTemplate;
    }
}

 

参考

Spring Boot 整合 redisson 实现分布式锁

Redisson实现分布式锁(1)---原理

Redisson实现分布式锁(2)—RedissonLock

Redisson实现分布式锁(3)—项目落地实现

JedisCommand接口说明

分布式锁实现
我们公司使用了6 年的分布式锁,很是牛逼啊!

基于redis分布式锁实现“秒杀”(含代码)

基于redis分布式锁实现“秒杀”

如何使用 SpringBoot + Redis 优雅的解决接口幂等性问题

高并发的核心技术 - 幂等的实现方案

Redis设置值的时候如果key或者value为null的情况

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值