redis实现分布式锁

项目地址

https://gitee.com/quancong/redis

1、通过setnx、setex命令与springboot整合实现

    @Scheduled(cron = "0/10 * * * * *")
    public void lockJob() {
        String lock = LOCK_PREFIX + "LockNxExJob";
        boolean nxRet = false;
        try{

            //redistemplate setnx操作
            nxRet = redisTemplate.opsForValue().setIfAbsent(lock,getHostIp());
            Object lockValue = redisService.genValue(lock);

            //获取锁失败
            if(!nxRet){
                String value = (String)redisService.genValue(lock);
                //打印当前占用锁的服务器IP
                //logger.info("get lock fail,lock belong to:{}",value);
                return;
            }else{
                //redistemplate setex操作
                redisTemplate.opsForValue().set(lock,getHostIp(),3600);

                //获取锁成功
                //logger.info("start lock lockNxExJob success");
                Thread.sleep(5000);
            }
        }catch (Exception e){
            logger.error("lock error",e);

        }finally {
            if(nxRet){
                //logger.info("release lock success");
                redisService.remove(lock);
            }
        }
    }

问题:在执行redistemplate setnx操作和redistemplate setex操作之间如果分布式中某个服务应用宕机或者某台redis宕机,则这把锁永远也无法释放,造成死锁,如下图所示:

在这里插入图片描述

2、解决方案:通过setnx和setex命令连用实现

2.1、虽然redis原生命令形式支持setnx和setex连用,语法如下:

SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]

  • EX seconds - 设置指定的到期时间,单位为秒。
  • PX milliseconds - 设置指定到期时间,单位为毫秒。
  • NX - 只有设置键,如果它不存在。
  • XX - 只有设置键,如果它已经存在。

例子:

127.0.0.1:6379> SET a redis EX 6 NX
OK
127.0.0.1:6379> ttl a
(integer) 3

但是2.0.5.RELEASE版本的springboot没有setex和setnx连用的api,但经过了一翻探索之后,使用RedisConnection,搞定。

    public boolean setLock(String key, long expire) {
        try {
            Boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
                @Override
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    return connection.set(key.getBytes(), getHostIp().getBytes(), Expiration.seconds(expire) ,RedisStringCommands.SetOption.ifAbsent());
                }
            });
            return result;
        } catch (Exception e) {
            logger.error("set redis occured an exception", e);
        }
        return false;
    }
2.2、新版本springboot2.2.6.RELEASE支持setnx和setex连用并设置超时时间,大体思路和第1、种方式差不多,api如下:

在这里插入图片描述

@Test
    public void testIfAbsentAndExpiredTime(){
        Boolean a = redisTemplate.opsForValue().setIfAbsent("a", 1, 666, TimeUnit.SECONDS);
        System.out.println(a);
    }

3、Lua脚本实现

因lua脚本本身就是redis中的原子性操作,故是一种很有效的锁机制。

步骤:
3.1、在resource目录下面新增一个后缀名为.lua结尾的文件

在这里插入图片描述
3.2、编写lua脚本

local lockKey = KEYS[1]
local lockValue = KEYS[2]

-- setnx info
local result_1 = redis.call('SETNX', lockKey, lockValue)
if result_1 == true
then
local result_2= redis.call('SETEX', lockKey,3600, lockValue)
return result_1
else
return result_1
end

3.3、传入lua脚本的key和arg并执行脚本

@Service
public class LuaDistributeLock {

    private static final Logger logger = LoggerFactory.getLogger(LockNxExJob.class);

    @Autowired
    private RedisService redisService;
    @Autowired
    private RedisTemplate redisTemplate;

    private static String LOCK_PREFIX = "lua_";

    private DefaultRedisScript<Boolean> lockScript;


    @Scheduled(cron = "0/10 * * * * *")
    public void lockJob() {

        String lock = LOCK_PREFIX + "LockNxExJob";

        boolean luaRet = false;
        try {
            // 使用lua脚本
            luaRet = luaExpress(lock,getHostIp());

            //获取锁失败
            if (!luaRet) {
                String value = (String) redisService.genValue(lock);
                //打印当前占用锁的服务器IP
                //logger.info("lua get lock fail,lock belong to:{}", value);
                return;
            } else {
                //获取锁成功
                //logger.info("lua start  lock lockNxExJob success");
                Thread.sleep(5000);
            }
        } catch (Exception e) {
            logger.error("lock error", e);

        } finally {
            if (luaRet) {
                //logger.info("release lock success");
                redisService.remove(lock);
            }
        }
    }


    /**
     * 获取lua结果
     * @param key
     * @param value
     * @return
     */
    public Boolean luaExpress(String key,String value) {
        lockScript = new DefaultRedisScript<Boolean>();
        lockScript.setScriptSource(
                new ResourceScriptSource(new ClassPathResource("add.lua")));
        lockScript.setResultType(Boolean.class);
        // 封装参数
        List<Object> keyList = new ArrayList<Object>();
        keyList.add(key);
        keyList.add(value);
        // 调用redisTemplate.execute方法执行脚本
        Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
        return result;
    }

    /**
     * 获取本机内网IP地址方法
     *
     * @return
     */
    private static String getHostIp() {
        try {
            Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
            while (allNetInterfaces.hasMoreElements()) {
                NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
                Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
                while (addresses.hasMoreElements()) {
                    InetAddress ip = (InetAddress) addresses.nextElement();
                    if (ip != null
                            && ip instanceof Inet4Address
                            && !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255
                            && ip.getHostAddress().indexOf(":") == -1) {
                        return ip.getHostAddress();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

还存在问题:

B执行到一半Server A的任务才执行完,此时Server A会释放Server B所占有的锁,导致无锁。

在这里插入图片描述
解决方案:设置锁的时候redis存入当前服务器的ip,释放锁的时候判断当前线程的ip是否跟redis中的ip是否一致。

还存在问题

此时还可能出现一个问题,redis中存的是127.0.0.1,而并发执行的过程中导致另一个线程的ip(比如是127.0.0.2)和127.0.0.1比较了,导致不能释放锁。

解决方案:get到redis中ip的同时和当前线程ip比较一下是否相同,让两个命令同时执行,还是lua脚本。

@Component
public class JedisDistributedLock {

    private final Logger logger = LoggerFactory.getLogger(JedisDistributedLock.class);

    private static String LOCK_PREFIX = "JedisDistributedLock_";

    private DefaultRedisScript<Boolean> lockScript;

    @Resource
    private RedisTemplate<Object, Object> redisTemplate;

    @Autowired
    private RedisService redisService;

    public static final String UNLOCK_LUA;

    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 = sb.toString();
    }


    @Scheduled(cron = "0/10 * * * * *")
    public void lockJob() {

        String lock = LOCK_PREFIX + "JedisNxExJob";
        boolean lockRet = false;
        try {
            lockRet = this.setLock(lock, 600);

            //获取锁失败
            if (!lockRet) {
                String value = (String) redisService.genValue(lock);
                //打印当前占用锁的服务器IP
                logger.info("jedisLockJob get lock fail,lock belong to:{}", value);
                return;
            } else {
                //获取锁成功
                logger.info("jedisLockJob start  lock lockNxExJob success");
                Thread.sleep(5000);
            }
        } catch (Exception e) {
            logger.error("jedisLockJob lock error", e);

        } finally {
            if (lockRet) {
                logger.info("jedisLockJob release lock success");
                releaseLock(lock,getHostIp());
            }
        }
    }


    public boolean setLock(String key, long expire) {
        try {
            Boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
                @Override
                public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                    return connection.set(key.getBytes(), getHostIp().getBytes(), Expiration.seconds(expire) ,RedisStringCommands.SetOption.ifAbsent());
                }
            });
            return result;
        } catch (Exception e) {
            logger.error("set redis occured an exception", e);
        }
        return false;
    }

    public String get(String key) {
        try {
            RedisCallback<String> callback = (connection) -> {
                JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                return commands.get(key);
            };
            String result = redisTemplate.execute(callback);
            return result;
        } catch (Exception e) {
            logger.error("get redis occured an exception", e);
        }
        return "";
    }

    /**
     * 释放锁操作
     * @param key
     * @param value
     * @return
     */
    private boolean releaseLock(String key, String value) {
        lockScript = new DefaultRedisScript<Boolean>();
        lockScript.setScriptSource(
                new ResourceScriptSource(new ClassPathResource("unlock.lua")));
        lockScript.setResultType(Boolean.class);
        // 封装参数
        List<Object> keyList = new ArrayList<Object>();
        keyList.add(key);
        keyList.add(value);
        Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
        return result;
    }

    /**
     * 获取本机内网IP地址方法
     *
     * @return
     */
    private static String getHostIp() {
        try {
            Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
            while (allNetInterfaces.hasMoreElements()) {
                NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
                Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
                while (addresses.hasMoreElements()) {
                    InetAddress ip = (InetAddress) addresses.nextElement();
                    if (ip != null
                            && ip instanceof Inet4Address
                            && !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255
                            && ip.getHostAddress().indexOf(":") == -1) {
                        return ip.getHostAddress();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

还存在问题:

任务执行时间很长,等redis分布式锁失效了,这个任务还在执行,此时是无锁状态,其它线程又会并发执行任务,又会造成线程安全问题。

解决方案:加一个看门狗。说白了,就是监听这把锁有没有失效,任务执行时间是否已经超过了锁的时间。可以加一个定时任务(看门狗),去redis中get一下这把锁,如果任务没执行完成就为这把锁加多一点超时时间,执行完成了这个定时器不做任何操作,任务代码里finally块给它删除了,相当于为这把锁续命。

还存在问题:

极端情况。redis主从复制的时候这把锁还没同步到从机,这个时候刚好有这把锁的主机崩了,那么redis中无锁。也就是说代码中刚上锁,redis主从复制时主机刚好崩了,其它线程又会继续执行任务,又会造成并发安全问题,解决方案暂时还没有。

还存在问题:

任务执行过程中还没等finally语句块执行删除锁的时候redis宕掉了,此时分布式锁是有过期时间的,如果这个过期时间很长,那么其他线程只能等这把锁过期了,那么这段时间其它线程也是阻塞的,也会造成服务不可用。

解决方案:使用Redisson。既然这把锁是redis的,redis宕掉直接影响这把锁能不能释放掉,而Redisson中的分布式锁保证了redis任务执行的原子性。如果任务没执行完,锁没被释放掉,那么一开始上的这把锁也不生效的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值