spring mvc+maven+redis锁

redis锁实现

本文是为了解决定时任务多个节点只让其中一个节点执行。
采用了redis锁。
以下是具体实现和实现redis锁会遇见的相关问题说明。

redis锁业务场景说明:

单体架构(简单理解就是一个程序里包含了一个系统/产品的所有业务功能)(单节点:意思是生产环境只有一台服务器)不一定会遇见这样的问题。而分布式部署和集群部署一定会遇见数据重复的相关问题。例如多个节点在执行定时任务怎么保证只让其中一个节点执行,那么这个时候就可以用到redis锁,当然也有其它方法,例如数据库插表或获取服务器IP只让其中一个执行等等。
关于单体架构、分布式、集群部署如有想了解它们具体的区别,可留言,下一篇就讲,本文主讲redis锁实现。

java实现redis锁代码逻辑:

Test.java是测试类,本地用线程模拟正式环境三个节点(三台服务器)执行同一个方法,只让其中一个节点执行相关逻辑。
该类需要import 包,具体如下

import org.apache.commons.lang.RandomStringUtils;
/*
* 测试类
* */
public class Test {



    static class Demo implements Runnable{

        public  void addNum() {

            DistributedLock distributedLock = new DistributedLock();
            String key = "add_information_lock";
            //随机生成10位数字+字母的组合
            String value = RandomStringUtils.randomAlphabetic(10);
            long expireTime = 50L;

            System.out.println("key:"+key+"--value -- "+value+"--expireTime -- "+expireTime);
            boolean lock = distributedLock.lock(key, value, expireTime);

            try {
                System.out.println(Thread.currentThread().getName()+"线程开始抢锁");
                if(lock){
                    System.out.println(Thread.currentThread().getName()+"--抢到了锁");
                    // 查询数据
                    System.out.println("start:"+Thread.currentThread().getName());
                    // 模拟耗时操作
                    Thread.sleep(20);
                    // 保存数据
                    System.out.println("end:"+Thread.currentThread().getName());
                    distributedLock.unLock(key,value);
                }
            } catch (Exception e) {
                System.out.println("异常:"+e.getMessage()+Thread.currentThread().getName());
            }
        }

        @Override
        public void run() {
            addNum();
        }
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        Thread t1 = new Thread(demo);
        Thread t2 = new Thread(demo);
        Thread t3 = new Thread(demo);
        t1.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");

        t1.start();
        t2.start();
        t3.start();

    }


}

DistributedLock.java 是redis锁工具类,主要是进行加锁、解锁、锁的延时。
另需要注意的一点是lua脚本是Redis 2.6版本的新特性,如果Redis版本是2.6以下的,那么可能会出现问题不支持lua脚本。
该类需要import包,具体如下

其中引com.google.common.collect.Lists的时候,需要看一下项目有没有该依赖,如没有可按照图片中的内容进行maven引入。

其它的import包,只要项目集成了Redis那么应该是都会有的。

import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;

import java.io.Serializable;
import java.util.Collections;

/*
*
* redis锁
* */
@Component
public class DistributedLock {

    @Autowired
    private RedisTemplate<Serializable, Object> redisTemplate;

    private static final Long RELEASE_SUCCESS = 1L;
    private static final Long POSTPONE_SUCCESS = 1L;

    private static final String LOCK_SUCCESS = "OK";
    //NX:是否存在key,存在就不set成功  NN:只有key才把值set进去
    private static final String SET_IF_NOT_EXIST = "NX";
    //EX:key过期时间单位设置为秒(PX:单位毫秒)
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    // 解锁脚本(lua)
    private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    // 延时脚本
    private static final String POSTPONE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return '0' end";


    /**
     * 分布式锁
     * @param key
     * @param value
     * @param expireTime 单位: 秒
     * @return
     */
    public boolean lock(String key, String value, long expireTime) {
        Jedis jedis = new Jedis();
        //原子性 操作成功,返回“OK”,否则返回null
        //将String类型的value放到key的value上,返回值都是String
        String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            // 加锁成功, 启动一个延时线程, 防止业务逻辑未执行完毕就因锁超时而使锁释放
            PostponeTask postponeTask = new PostponeTask(key, value, expireTime, this);
            Thread thread = new Thread(postponeTask);
            thread.setDaemon(Boolean.TRUE);
            thread.start();
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }


    /**
     * 解锁
     * @param key
     * @param value
     * @return
     */
    public Boolean unLock(String key, String value) {
        Jedis jedis = new Jedis();
        Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(value));
        if (RELEASE_SUCCESS.equals(result)) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    /**
     * 锁延时
     * @param key
     * @param value
     * @param expireTime
     * @return
     */
    public Boolean postpone(String key, String value, long expireTime) {
        Jedis jedis = new Jedis();
        Object result = jedis.eval(POSTPONE_LOCK_SCRIPT, Lists.newArrayList(key), Lists.newArrayList(value, String.valueOf(expireTime)));
        if (POSTPONE_SUCCESS.equals(result)) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }
}

PostponeTask.java 是变量定义和延时线程调用。
该类不需要import包。

/*
*
* redis变量定义
* */
public class PostponeTask implements Runnable{

    private String key;
    private String value;
    private long expireTime;
    private boolean isRunning;
    private DistributedLock distributedLock;

    public PostponeTask() {
    }

    public PostponeTask(String key, String value, long expireTime, DistributedLock distributedLock) {
        this.key = key;
        this.value = value;
        this.expireTime = expireTime;
        this.isRunning = Boolean.TRUE;
        this.distributedLock = distributedLock;
    }

    @Override
    public void run() {
        System.out.println("---锁延时方法开始----------------------------------------"+key);
        long waitTime = expireTime * 1000 * 2 / 3;// 线程等待多长时间后执行
        while (isRunning) {
            try {
                System.out.println("延时等待时间-----"+waitTime);
                Thread.sleep(waitTime);
                if (distributedLock.postpone(key, value, expireTime)) {
                    System.out.println("延时成功....................延时名称:"+key+"...延时时间....."+expireTime);
                } else {
                    System.out.println("延时停止---stop------");
                    this.stop();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void stop() {
        this.isRunning = Boolean.FALSE;
    }

}

至此就结束了。

Redis锁引发的相关问题:

现在说明一下加入redis锁引发的各种问题,如使用的是本文的代码逻辑写法是完全没问题的。

问题1:异常导致锁没有释放
问题说明:这个问题形成的原因就是程序在获取到锁之后,执行业务的过程中出现了异常,导致锁没有被释放
解决方案:方案1、为redis的key设置过期时间,程序异常导致的死锁,在到达过期时间之后锁会自动释放。
方案2、可以在try后面加上finally在这里面写上根据key释放。不要写在catch里,这里面只作异常打印相关逻辑

问题二:如上采取方案1设置过期时间,那么可能会出现一个问题是获取锁与设置过期时间操作不是原子性的
问题说明:虽然获取到锁,也设置了过期时间,看似完美。但是在高并发的场景下仍然会出现问题,因为“获取锁”与“设置过期时间”是两个redis操作,两个redis操作不是原子性的。
出现这种情况:就在获取锁之后,设置过期时间之前程序宕机了。锁被获取到了但没有设置过期时间,最后又成为死锁。
解决方案:方案1、获取锁的同时设置过期时间。那么在上文代码中也是这样使用的,jedis.set方法是Redis特推出的,它是具有原子性的可以同时获取锁并设置过期时间。

问题三:某个方法因数据或业务逻辑执行时间比较长,而过期时间设置的短。
问题说明:如程序A没有执行完成,锁定的key就过期了。过期之后会自动释放锁,但是程序A的代码逻辑还没有执行完成,也没有异常抛出,就是执行的时间比较长,这个时候就应该对锁定的key进行续期
解决方案:在上文中已实现,是在获取到锁之后另开一个线程进行锁的延时

另本文中的解锁是根据key的value解锁的,value的生成规则是随机生成十位不同的字母加数字组合而成的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值