SpringBoot Redis实现分布式锁

集群情况下,JDK的锁是很容易出现问题的,这时候就需要用到分布式锁;最近用到了Redis实现分布式锁,这里记录一下。

基本原理:

       这里使用了Redis的setNX,由于当某个 key 不存在的时候,SETNX 才会设置该 key。且由于 Redis 采用单进程单线程模型,所以,不需要担心并发的问题。那么,就可以利用 SETNX 的特性维护一个 key,存在的时候,即锁被某个线程持有;不存在的时候,没有线程持有锁。

该实例使用jMeter工具进行过压测,可以抗住每秒几百的并发。

1. pom文件添加依赖

<!-- redis依赖 -->
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-data-redis</artifactId>
	    <exclusions>
	        <exclusion>
	            <groupId>io.lettuce</groupId>
	            <artifactId>lettuce-core</artifactId>
	        </exclusion>
	    </exclusions>
	</dependency>
        
       <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>   

2. Redis配置

# Redis数据库索引(默认为0)  
spring.redis.database=0
# Redis服务器地址  
spring.redis.host=127.0.0.1
# Redis服务器连接端口  
spring.redis.port=6379
# Redis服务器连接密码(默认为空)  
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)  
spring.redis.pool.max-active=200
# 连接池最大阻塞等待时间(使用负值表示没有限制)  
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接  
spring.redis.pool.max-idle=10
# 连接池中的最小空闲连接  
spring.redis.pool.min-idle=1
# 连接超时时间(毫秒)  
spring.redis.timeout=10000
#是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个  
redis.testOnBorrow=true  
#在空闲时检查有效性, 默认false  
redis.testWhileIdle=true 

3. redis 分布式锁代码实现

package com.example.demo.utils;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import redis.clients.jedis.Jedis;

/**
 * redis锁工具类
 */
@Component
public class RedisLockUtil {

    private final static Logger logger = LoggerFactory.getLogger(RedisLockUtil.class);

    @Autowired
    public StringRedisTemplate redisTemplate;

    /** 执行结果标识 */
    private static final Long SUCCESS = 1L;
    
    /** 设置成功标识 */
    private static final String LOCK_SUCCESS = "OK";
    
    /** key不存在时才设置值 */
    private static final String SET_IF_NOT_EXIST = "NX";
    
    /** 过期时间单位标识,EX:秒 */
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    
    /**
     * 锁的过期时间(单位:秒)
     */
    private static final int LOCK_EXPIRE_TIME = 30;

    /**
     * 最大尝试次数
     */
    private static final int MAX_ATTEMPTS = 100;

    /**
     * key前缀
     */
    private static final String KEY_PRE = "REDIS_LOCK_";

    /**
     * 获取锁
     * @param key
     * @return
     */
    public Boolean getLock(String key, String value) {
        return tryLock(key, value,MAX_ATTEMPTS);
    }

    /**
     * 尝试加锁
     * @param key
     * @param max_attempts 重试次数
     * @return
     */
    public Boolean tryLock(String key, String value, Integer max_attempts) {
        int attempt_counter = 0;
        key = KEY_PRE + key;
        while (attempt_counter < max_attempts) {
            if (setLock(key, value, LOCK_EXPIRE_TIME)) {
                System.out.println("获取锁成功,Redis Lock key : " + key + ", value : " + value);
                return Boolean.TRUE;
            }
          //  System.out.println("正在重试获取锁,Redis Lock key : " + key + ", value : " + value);
            attempt_counter++;
            if (attempt_counter >= max_attempts) {
                logger.error("获取锁失败!, " + key);
            }
            try {
            	// 休眠时间 10-100 毫秒
                Thread.sleep((int)(Math.random() * 90 + 10));
            } catch (InterruptedException e) {
                logger.error("获取锁等待异常:{}",e.getMessage());
            }
        }

        return Boolean.FALSE;
    }

    /**
     * 加锁 key不存在时才设置key value
     * @param lockKey   加锁键
     * @param value  	加锁客户端唯一标识
     * @param seconds   锁过期时间
     * @return
     */
    public Boolean setLock(String lockKey, String value, long seconds) {
        return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            String result = jedis.set(lockKey, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
            if (LOCK_SUCCESS.equals(result)) {
            	return Boolean.TRUE;
            }
            return Boolean.FALSE;
        });
    }
    

    /**
     * 解锁
     * @param key	加锁键
     * @param value
     * @return
     */
    public boolean unLock(String key, String value) {
        key = KEY_PRE + key;
        try {
            String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
            RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
            List<String> keys = Arrays.asList(key, value);
            Object result = redisTemplate.execute(redisScript, keys);
            if(SUCCESS.equals(result)) {
                System.out.println("释放Redis锁 : " + key + ", value : " + value);
                return true;
            }

        } catch (Exception e) {
            logger.error("释放Redis锁异常:",e);
        }

        return false;
    }

    /**
     * 生成加锁的唯一字符串
     * @return 唯一字符串
     */
    public String fetchLockValue() {
        return UUID.randomUUID().toString() + "-" + System.currentTimeMillis();
    }
}

4. 使用实例

/**
   * 使用实例
   * @return
   */
 @Transactional(rollbackFor=Exception.class)
 public String updateInfoLock(){
	  // 锁key
	  String key = "updateInfo";
	  // 锁的唯一字符串
	  String value = redisLockUtil.fetchLockValue();  
	  // 获取锁
	  if (!redisLockUtil.getLock(key,value)) {
		log.info("没抢到锁!");
		return "服务器繁忙,请稍后重试!";
	  }
	  
	  try {
		  // 业务逻辑代码
		} catch (Exception e) {
			log.error("XXX异常",e);
			// 捕获异常之后需要 手动回滚事务
			TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
		}finally{
			 // 释放锁
			 redisLockUtil.unLock(key, value);
		}
	   return "操作成功!";
  }

注意:释放锁的执行代码最好是放在 finally 代码块里面,防止出现异常或者忘记释放锁的情况;

 

发布了38 篇原创文章 · 获赞 49 · 访问量 5万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览