15.Redis分布式锁的正确实现方式(Java版)

原文链接: https://www.cnblogs.com/love-cj/p/8242439.html

前言
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。

可靠性
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

互斥性。在任意时刻,只有一个客户端能持有锁。
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

代码实现
组件依赖
首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

加锁代码
正确姿势
Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}


可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

第一个为key,我们使用key来当锁,因为key是唯一的。

第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

解锁代码
正确姿势
还是先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

redis 加锁解锁代码

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;


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

@Component
public class RedisLockUtils {
    @Autowired
    private RedisTemplate redisTemplate;

    private static DefaultRedisScript<Long> redisScript;

    private static RedisSerializer<String> argsSerializer;

    private static RedisSerializer resultSerializer;

    private static final Long EXEC_RESULT = 1L;

    private static RedisLockUtils that;

    //过期时间(秒)
    private static Long expireTime = 120L;

 @PostConstruct
    public void init() {
        that = this;
        redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Long.class);
        argsSerializer = new StringRedisSerializer();
        resultSerializer = new StringRedisSerializer();

      RedisSerializer stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setValueSerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
       redisTemplate.setHashValueSerializer(stringSerializer);
    } 

  public static String lock(String key){
        String requestId = UUID.randomUUID().toString().replace("-", "");
        Boolean lockTag = lock(key, requestId, expireTime);
        if(lockTag){
            return requestId;
        }else{
          return null;
        }
    }   

  public static String realTimeLock(String key){
        String requestId = UUID.randomUUID().toString().replace("-", "");
        Boolean lockTag = realTimeLock(key, requestId, expireTime);
        if(lockTag){
               return requestId;
        }else{
            return null;
        }
    }

  /**
     * 加锁 默认60秒过期
     * @param key
     * @param requestId
     * @return
     */
    public static boolean lock(String key, String requestId){
        return lock(key, requestId, expireTime);
    }
 /**
     *
     * @param key
     * @param requestId
     * @param expireTime
     * @return
     */
 public static boolean lock(String key, String requestId, Long expireTime) {
        key += "_lock";
        String script = "if redis.call('setNx',KEYS[1],ARGV[1]) then\n" +
            "    if redis.call('get',KEYS[1])==ARGV[1] then\n" +
                "        return redis.call('expire',KEYS[1],ARGV[2])\n" +
                "    else\n" +
                "        return 0\n" +
                "    end\n" +
              "end";
        //redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/lock.lua")));
        redisScript.setScriptText(script);
  long nano = System.nanoTime();
        long timeOut = 10 * 1000 * 1000 * 1000L;//至多尝试10秒
        try{
            while (System.nanoTime() - nano < timeOut) {
  //Object result = that.redisTemplate.execute(redisScript, argsSerializer, resultSerializer, Collections.singletonList(key), requestId, expireTime.toString());
              Object result = that.redisTemplate.execute(redisScript,Collections.singletonList(key), requestId, expireTime.toString());
                if (EXEC_RESULT.equals(result)) {
                    return true;
                }
                //休眠200毫秒
  Thread.sleep(200);
            }
        }catch (Exception ex){
            ex.printStackTrace();
        }
        return false;
    }
  /**
     * 实时锁,不等待解锁
     * @param key
     * @param requestId
     * @param expireTime
     * @return
     */
   public static boolean realTimeLock(String key, String requestId, Long expireTime) {
        key += "_lock";
        String script = "if redis.call('setNx',KEYS[1],ARGV[1]) then\n" +
    "    if redis.call('get',KEYS[1])==ARGV[1] then\n" +
                "        return redis.call('expire',KEYS[1],ARGV[2])\n" +
                "    else\n" +
                "        return 0\n" +
                "    end\n" +
                 "end";
        redisScript.setScriptText(script);
        try{
 Object result = that.redisTemplate.execute(redisScript,Collections.singletonList(key), requestId, expireTime.toString());
            if (EXEC_RESULT.equals(result)) {
                return true;
            }
        }catch (Exception ex){
         ex.printStackTrace();
        }
        return false;
    }


    /**
     *
     * @param key
     * @param requestId
     * @return
     */
    public static boolean unlock(String key, String requestId) {
        //System.out.println(key + ":" + requestId);
        if(key == null || requestId == null){
   return false;
        }
        key += "_lock";
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return '0' end";

        List<String> keys = Collections.singletonList(key);
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        Object result = null;
        try {
            result = that.redisTemplate.execute(redisScript, keys, requestId);
        }
        catch (Exception ex){
            ex.printStackTrace();
        }
  if(EXEC_RESULT.equals(result)) {
            return true;
        }
        return false;
    }
}


调用方法,加锁解锁

  /**
     * 修改存款审核数据状态
     *
     * @param rootCompanyCode:总公司(代号)
     * @param id:自增ID
     * @param state:状态
     * @param auditAccount:审核人
     * @return 返回结果 是否修改成功
     */
 public ResponseModel<Boolean> updateState(String rootCompanyCode, Long id, Integer state, String auditAccount) {
        ResponseModel<Boolean> responseModel = new ResponseModel<>();
 //获取存款审核数据对象
        DepositModel depositModel = loadById(rootCompanyCode, id).getData();
        if (depositModel == null) {
            responseModel.setCode(EnumResponseCode.FAILED)
                    .setMsg("存款审核数据不存在");
            return responseModel;
        }

        //加锁
        String lockKey = "pf_deposit_updatestate_" + id.toString();
        String lockId = RedisLockUtils.realTimeLock(lockKey);
        if (lockId == null) {
             responseModel.setCode(EnumResponseCode.FAILED)
                    .setMsg("请不要频繁操作");
            return responseModel;
        }
  try {
            if (state.equals(EnumDepositState.SUCCESS.getCode())) {
                if (state.equals(depositModel.getState())) {
                    responseModel.setCode(EnumResponseCode.FAILED)
                            .setMsg("该笔存款已存入");
   PaymentconfigModel payConfig;
                payConfig = paymentconfigService.computationConfig(rootCompanyCode, depositModel.getMemberid(), depositModel.getMoney(), null, false);

                if (payConfig == null) {
                  //支付配置为null的话赋默认值
                    payConfig = new PaymentconfigModel();
                    //放宽打码量
                    payConfig.setRelaxEffmoney(0);
                    //常态稽核(默认一倍)
                    payConfig.setNormalEffmoney(depositModel.getMoney());
                    //常态稽核手续费(默认百分之50)
                    payConfig.setNormalCharge(depositModel.getMoney().multiply(new BigDecimal(0.5)));
                    //综合稽核、综合稽核手续费(默认同上)
              payConfig.setAuditEffmoney(depositModel.getDiscuntsmoney())
                            .setAuditCharge(depositModel.getDiscuntsmoney().multiply(new BigDecimal(0.5)));
                }
   responseModel = financialServiceFeignClient.getDepositAPI().confirm(rootCompanyCode, id, auditAccount,
                        payConfig.getAuditEffmoney(), payConfig.getNormalEffmoney(),     payConfig.getNormalCharge(), payConfig.getAuditCharge());
            } else {
                if (state.equals(depositModel.getState()) && state.equals(EnumDepositState.WAIT.getCode())) {
                     responseModel.setCode(EnumResponseCode.FAILED)
                            .setMsg("该笔存款已待审核");
                    return responseModel;
                }
 if (state.equals(depositModel.getState()) && state.equals(EnumDepositState.NOT_SUCCESS.getCode())) {
                    responseModel.setCode(EnumResponseCode.FAILED)
                            .setMsg("该笔存款已取消");
                    return responseModel;
                }

                DepositEntity entity = new DepositEntity();
                entity.setId(id)
                        .setState(state)
                        .setAuditaccount(auditAccount);
                  responseModel = financialServiceFeignClient.getDepositAPI().updateStateByEntity(rootCompanyCode, entity);
            }
        } catch (Exception e) {
            responseModel.setCode(EnumResponseCode.INNER_ERROR)
            responseModel.setCode(EnumResponseCode.INNER_ERROR)
                    .setMsg("内部错误" + e);
        } finally {
            //解锁
   RedisLockUtils.unlock(lockKey, lockId);
        }
        return responseModel;
    }



猜你喜欢

转载自blog.csdn.net/weixin_33895475/article/details/90849464