springboot利用redis实现分布式锁(redis为单机模式)

1.pom文件添加redis支持

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

2.application.properties或者(application.yml)添加redis配置

spring.redis.database=1
spring.redis.host=172.xx.xx.xx
spring.redis.password=123456
spring.redis.port=6379

上面的spring.redis.host替换成自己的redis服务地址,如果没有用到密码则删除spring.redis.password配置即可

 

3.redis工具类

package com.example.demo;

import com.example.demo.extend.FastJsonRedisSerializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;

@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate<String,String> template;

    @PostConstruct
    public void init(){
        template.setKeySerializer(template.getStringSerializer());
        template.setValueSerializer(template.getStringSerializer());
        template.setHashKeySerializer(template.getStringSerializer());
        template.setHashValueSerializer(template.getStringSerializer());
    }

    /**
     * 通过lua脚本 加锁并设置过期时间
     * @param key 锁key值
     * @param value 锁value值
     * @param expire 过期时间,单位秒
     * @return true:加锁成功,false:加锁失败
     */
    public boolean getLock(String key,String value,String expire){
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<String>();
        redisScript.setResultType(String.class);
        String strScript = "";
        strScript +="    if redis.call('setNx',KEYS[1],ARGV[1])==1 then ";
        strScript +="        return redis.call('expire',KEYS[1],ARGV[2]) ";
        strScript +="    else";
        strScript +="        return 0 ";
        strScript +="    end ";
        redisScript.setScriptText(strScript);
        try{
            Object result = this.template.execute(redisScript,template.getStringSerializer(),template.getStringSerializer(), Collections.singletonList(key),value,expire);
            System.out.println("redis返回:"+result);
            return "1".equals(""+result);
        }catch (Exception e){
            //可以自己做异常处理
            return false;
        }

    }

    /**
     * 通过lua脚本释放锁
     * @param key 锁key值
     * @param value 锁value值(仅当redis里面的value值和传入的相同时才释放,避免释放其他线程的锁)
     * @return true:释放锁成功,false:释放锁失败(可能已过期或者已被释放)
     */
    public boolean releaseLock(String key,String value){
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(String.class);
        String strScript = "";
        strScript +="if redis.call('get',KEYS[1]) == ARGV[1] then ";
        strScript +="    return redis.call('del',KEYS[1]) ";
        strScript +="else ";
        strScript +="    return 0 ";
        strScript +="end ";
        redisScript.setScriptText(strScript);
        try{
            Object result = this.template.execute(redisScript,template.getStringSerializer(),template.getStringSerializer(), Collections.singletonList(key),value);
            return "1".equals(""+result);
        }catch (Exception e){
            //可以自己做异常处理
            return false;
        }
    }
}

 

redis锁用到的是setNx命令,这个命令的意思是如果redis里面存在这个key则不再添加,如果key不存在则添加成功,当setNx设置成功之后再给这个值设置一个超期时间来防止出现极端情况(断网,服务终止)导致锁没有被及时释放的情况。

上面的加锁和解锁都是通过lua脚本进行,redis里面lua脚本执行时是原子操作,可以保证加锁和设置超时同时成功或者失败,不会出现设置值成功 添加超时时间失败的情况

4.使用

在需要加锁的地方注入RedisUtil对象即可。有问题的可以留言一起探讨细节问题

@Autowired
private RedisLockUtil redisUtil;
boolean lock = this.redisUtil.getLock("FORM_SUBMIT"+formId,formId,"2");
            if(!lock){
                //未获得锁
                throw new ServiceException("当前已经有任务在执行!");
            }

//---------执行业务逻辑

if(lock){
                //释放锁不关心成功与否
                this.redisUtil.releaseLock("FORM_SUBMIT"+formId,formId);
            }

上面的业务逻辑最好放在try catch中执行,释放锁的代码放到finally里面执行。

 

5.考虑各种情况下会不会导致bug的出现

5.1:加锁失败

  拿不到锁业务逻辑不执行,没问题

5.2:加锁成功,释放锁失败

  网络原因或者其他原因没有释放掉,没关系 ,超时时间过了就会自己释放,没问题

5.3:加锁成功,业务执行时间过长,锁已经被redis自己释放

   此种情况需要根据业务的实际情况设置合理的超时时间,可能会出问题,原因在于 基于分布式的系统是无法避免类似的问题,具体可以参考如下博客的文章,

此文章引入了 关于Redis分布式锁的安全性问题,在分布式系统专家Martin Kleppmann和Redis的作者antirez之间发生过的一场争论,内容很精彩。https://blog.csdn.net/paincupid/article/details/75094550

posted @ 2019-07-04 14:56  xuxianshun  阅读(895)  评论(0编辑  收藏  举报