Redis分布式锁

目前几种分布式锁的实现方式:

  • 数据库实现(不适合数据量比较大的互联网公司)、
  • 基于ZK的实现(1、Zk的节点改变时候的watcher事件通知。2、节点类型中的有序节点可实现先到先得公平策略)
  • 基于Redis的实现(setNX + 有效期,实现相对ZK简单一些)    

工作中经常用到Redis,所以决定采用redis实现分布式锁, 首先先要明确目标,目标明确了才有技术方案

分布式锁实现的目标

  •  高性能(加锁和解锁性能高)
  •  互斥访问(一个线程持有锁,另一个线程不能持有)
  •  不能产生死锁(例如redis客户端挂了,或者设置过期时间时候挂了导致key永久有效)
  •   解锁(只能解除自己的锁)

具体实现:


  1 package com.brightcns.wuxi.citizencard.common.feature.util;
  2 
  3 import javafx.beans.binding.ObjectExpression;
  4 import lombok.extern.slf4j.Slf4j;
  5 import org.springframework.data.redis.connection.RedisConnection;
  6 import org.springframework.data.redis.connection.ReturnType;
  7 import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
  8 import redis.clients.jedis.JedisPoolConfig;
  9 import redis.clients.util.SafeEncoder;
 10 
 11 
 12 /**
 13  * 分布式锁的常用类
 14  * @author maxianming
 15  * @date 2018/8/13 15:50
 16  */
 17 @Slf4j
 18 public class LockUtils {
 19 
 20     private JedisConnectionFactory jedisConnectionFactory;
 21     private static final Long RELEASE_SUCCESS = 1L;
 22     private static final String LOCK_SUCCESS = "OK";
 23     private static final String SET_IF_NOT_EXIST = "NX";
 24     private static final String SET_WITH_EXPIRE_TIME = "PX";
 25 
 26     public LockUtils(JedisConnectionFactory jedisConnectionFactory) {
 27         if (jedisConnectionFactory == null) {
 28           throw new IllegalArgumentException("jedisConnectionFactory not be allowed null");
 29         }
 30         this.jedisConnectionFactory = jedisConnectionFactory;
 31     }
 32     /**
 33      * 尝试获取分布式锁
 34      * @param lockKey 锁
 35      * @param requestId 请求标识
 36      * @param expireTime 超期时间 ms
 37      * @return 是否获取成功
 38      */
 39     public void lock(String lockKey, String requestId, int expireTime) throws InterruptedException {
 40         RedisConnection redisConnection = getRedisConnection();
 41         try {
 42           while (true) {
 43                Object result = redisConnection.execute("SET", new byte[][]{
 44                        SafeEncoder.encode(lockKey), SafeEncoder.encode(requestId), SafeEncoder.encode(SET_IF_NOT_EXIST),
 45                        SafeEncoder.encode(SET_WITH_EXPIRE_TIME), SafeEncoder.encode(String.valueOf(expireTime))});
 46                if (result != null) {
 47                    if (LOCK_SUCCESS.equals(new String((byte[])(result)))) {
 48                        return;
 49                    }
 50                }
 51               try {
 52                   Thread.sleep(expireTime);
 53               } catch (InterruptedException e) {
 54                   log.error("中断异常", e);
 55                   throw e;
 56               }
 57           }
 58        } finally {
 59            redisConnection.close();
 60        }
 61     }
 62 
 63 
 64     /**
 65      * 释放分布式锁
 66      * @param lockKey 锁
 67      * @param requestId 请求标识
 68      * @return 是否释放成功
 69      */
 70     public boolean unLock(String lockKey, String requestId) {
 71         RedisConnection redisConnection = getRedisConnection();
 72         try {
 73             String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
 74             Object result = redisConnection.eval(SafeEncoder.encode(script), ReturnType.INTEGER, 1, getByteParams(new String[]{lockKey, requestId}));
 75             if (RELEASE_SUCCESS.equals(result)) {
 76                 return true;
 77             }
 78             return false;
 79         } finally {
 80             redisConnection.close();
 81         }
 82 
 83     }
 84 
 85     private byte[][] getByteParams(String... params) {
 86         byte[][] p = new byte[params.length][];
 87         for (int i = 0; i < params.length; i++)
 88             p[i] = SafeEncoder.encode(params[i]);
 89 
 90         return p;
 91     }
 92 
 93     private RedisConnection getRedisConnection() {
 94         RedisConnection redisConnection = jedisConnectionFactory.getConnection();
 95         return redisConnection;
 96     }
 97 
 98     public static void main(String[] args) {
 99        JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
100         jedisConnectionFactory.setPort(6379);
101         jedisConnectionFactory.setHostName("*");
102         jedisConnectionFactory.setPassword("*");
103 
104         JedisPoolConfig poolConfig = new JedisPoolConfig();
105         poolConfig.setMaxTotal(40);
106         poolConfig.setMaxIdle(10);
107         poolConfig.setMinIdle(5);
108         jedisConnectionFactory.setPoolConfig(poolConfig);
109         jedisConnectionFactory.afterPropertiesSet();
110         LockUtils lockUtils =new LockUtils(jedisConnectionFactory);
111         Thread thread = new Thread(() -> {
112             try {
113                 lockUtils.lock("lock", "123", 20000);
114             } catch (InterruptedException e) {
115                 e.printStackTrace();
116             }
117             System.out.println("加锁结果:" + "成功");
118 
119             boolean result = lockUtils.unLock("lock", "123");
120 
121             System.out.println("解锁结果:" + result);
122         },"jedis-thread");
123         thread.start();
128     }
129 
130 }


 

   (1)加锁

           jedis提供了set方法将setNX和expire整合在一起的原子方法,这就避免了expire之前redis挂了导致的key永久有效,从而死锁。

          实际项目中使用的Spring boot整合的redis,所以直接获取jedis客户端不太方便,查看源码发现 存在execute支持redis命令。

          requestId的作用,解锁的时候传入加锁时候的相同值,避免错误解除别的线程的锁

          expireTime的作用,redis key的有效期,根据实际时间设置

 (2)解锁

          采用lua脚本,主要保证一、解锁操作的原子性 二、解的是自己的锁

原文地址:https://www.cnblogs.com/mxmbk/p/9470590.html