高并发1-Redis分布式锁setnx,setex连用

Redis分布式锁

* 分布锁满足两个条件,一个是加有效时间的锁,一个是高性能解锁

* 采用redis命令setnx(set if not exist)、setex(set expire value)实现

 

* 【千万记住】解锁流程不能遗漏,否则导致任务执行一次就永不过期

* 将加锁代码和任务逻辑放在try,catch代码块,将解锁流程放在finally

 

package com.concurrent.schedule;

import com.concurrent.util.IpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.management.MalformedObjectNameException;
import java.util.concurrent.TimeUnit;

/**
 * Created by zhangjiawen on 2019/8/13.
 */
@Component
public class RedisSchedule {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    private StringRedisTemplate redisTemplate;
    private static final String publicKey="CommonKey";
    Boolean lock=false;
    @Scheduled(cron="*/5 * * * * *")
    public void setNx() {
        System.out.println("TimeUnit.SECONDS"+TimeUnit.SECONDS);
        try {
            String ipConfig=IpUtil.getIp() + ":" + IpUtil.getPort();
            lock= redisTemplate.opsForValue().setIfAbsent(publicKey, ipConfig);
            if(!lock){
                String value=redisTemplate.opsForValue().get(publicKey);
                logger.warn("key have exist belong to:"+value);
            }else{
                redisTemplate.opsForValue().set(publicKey,ipConfig,60, TimeUnit.SECONDS);
                //获取锁成功
                logger.info("start lock lockNxExJob success");
                Thread.sleep(5000);
            }
        }catch (Exception e){
            logger.error("setNx error!");
            e.printStackTrace();
        }finally {
            redisTemplate.delete(publicKey);
        }

    }

}

 

分布式锁setnx、setex的缺陷,在setnx和setex中间发生了服务down机

 

* 从Redis宕机讲解分布式锁执行的异常场景流程

* 从Server服务宕机讲解分布式锁执行的异常场景流程

* 在setnx和setex中间发生了服务down机 那么key将没有超时时间 会一直存在,新的请求永远进不来

解决方案:

由于setnx与setex是分步进行,那么我们将两步合成一步,放在同一个原子中即可

* 怎么一次性执行过一条命令而不会出现问题,采用Lua脚本

* Redis从2.6之后支持setnx、setex连用

* Lua简介

  * 从 Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以使用 EVAL 命令对 Lua 脚本进行求值。
  * Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。
* Lua脚本配置流程
  * 1、在resource目录下面新增一个后缀名为.lua结尾的文件
  * 2、编写lua脚本

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

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

  * 3、传入lua脚本的key和arg
  * 4、调用redisTemplate.execute方法执行脚本
* Lua脚本结合RedisTempalte实战演练

package com.concurrent.schedule;

import com.concurrent.util.IpUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by zhangjiawen on 2019/8/13.
 */
@Component
public class LuaRedisSchedulePlus {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Autowired
    private RedisTemplate redisTemplate;
    private static final String publicKey="DEMO_COMMON_KEY";
    Boolean lock=false;
    private DefaultRedisScript<String> lockScript;

    @Scheduled(cron="*/5 * * * * *")
    public void setNx() {

        try {
            String ipConfig=IpUtil.getIp() + ":" + IpUtil.getPort();
            lock=  luaExpress(publicKey,"30",ipConfig);
            if(!lock){
                String value=(String) redisTemplate.opsForValue().get(publicKey);
                logger.warn("key have exist belong to:"+value);
            }else{
                //获取锁成功
                logger.info("start lock lockNxExJob success");
               // Thread.sleep(5000);
            }
        }catch (Exception e){
            logger.error("setNx error!");
            e.printStackTrace();
        }finally {
           // redisTemplate.delete(publicKey);
        }
    }


    /**
     * 获取lua结果
     * @param key
     * @param value
     * @return
     */
    public Boolean luaExpress(String key,String  time,String value) {
        lockScript = new DefaultRedisScript<String>();
        lockScript.setScriptSource(
                new ResourceScriptSource(new ClassPathResource("add.lua")));
        lockScript.setResultType(String.class);
        // 封装参数
        List<Object> keyList = new ArrayList<Object>();
        keyList.add(key);
        keyList.add(time);
        keyList.add(value);
        String result= (String)redisTemplate.execute(lockScript, keyList);
        System.out.println(result);
        logger.info("redis set result:"+result);
        if (!"ok".equals(result.toLowerCase())){
            return false;
        }
        return true;
    }
}

 


* Lua脚本其他工作场景剖析和演练
* lua eval http://doc.redisfans.com/script/eval.html

lua脚本做高可用分布式锁的优化

-当某个锁需要持有的时间小于锁超时时间时会出现两个进程同时执行任务的情况,
​ 这时候如果进程没限制只有占有这把锁的人才能解锁的原则就会出现,
​ A解了B的锁。

解决方案:删除key的时候判断一下value是否是当前value,是的话删除,否则不执行删除,将原来代码finally修改如下

}catch (Exception e){
            logger.error("setNx error!");
            e.printStackTrace();
        }finally {
           // redisTemplate.delete(publicKey);
           // releaseLock(publicKey,ipConfig);
        }
    /**
     * 释放锁操作
     * @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;
    }

添加解锁lua脚本

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

-- get key
local result_1 = redis.call('get', lockKey)
if result_1 == lockValue
then
local result_2= redis.call('del', lockKey)
return result_2
else
return false
end

 

 

 

源码地址:https://gitee.com/jiawenzhang/gaobingfa.git

 

posted @ 2019-08-14 10:02  valar-dohaeris  阅读(30632)  评论(0编辑  收藏  举报