一:基于 Redis 的 NX EX 参数

既然是选用了 Redis,那么它就得具有排他性才行。同时它最好也有锁的一些基本特性:

  • 高性能(加、解锁时高性能)
  • 可以使用阻塞锁与非阻塞锁。
  • 不能出现死锁。
  • 可用性(不能出现节点 down 掉后加锁失败)。

这里利用 Redis set key 时的一个 NX 参数可以保证在这个 key 不存在的情况下写入成功。并且再加上 EX 参数可以让该 key 在超时之后自动删除。

所以利用以上两个特性可以保证在同一时刻只会有一个进程获得锁,并且不会出现死锁(最坏的情况就是超时自动删除 key)。

yml文件:

redis:
    host: xxxx
    password: test2
    port: 6379
    maxIdle: 100
    maxWaitMillis: 10000
    maxTotal: 600
    testOnBorrow: true
    testOnReturn: true
    timeout: 5000

redis配置:

package com.fhgl.shop.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
public class RedisConfiguration {

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

	@Value("${spring.redis.host}")
	private String host;
	@Value("${spring.redis.password}")
	private String password;
	@Value("${spring.redis.port}")
	private int port;
	@Value("${spring.redis.maxWaitMillis}")
	private int maxWaitMillis;
	@Value("${spring.redis.maxTotal}")
	private int maxTotal;
	@Value("${spring.redis.testOnBorrow}")
	private boolean testOnBorrow;
	@Value("${spring.redis.testOnReturn}")
	private boolean testOnReturn;
	@Value("${spring.redis.timeout}")
	private int timeout;
	// @Value("${spring.redis.database}")
	// private int database;
	@Value("${spring.redis.maxIdle}")
	private int maxIdle;
	// @Value("${spring.redis.pool.min-idle}")
	// private int minIdle;

	/**
	 * 连接池配置
	 * 
	 * @Description:
	 * @return
	 */
	@Bean
	public JedisPoolConfig jedisPoolConfig() {
		JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
		jedisPoolConfig.setMaxIdle(maxIdle);
		// jedisPoolConfig.setMinIdle(minIdle);
		jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
		jedisPoolConfig.setMaxTotal(maxTotal);
		jedisPoolConfig.setTestOnBorrow(testOnBorrow);
		jedisPoolConfig.setTestOnReturn(testOnReturn);
		return jedisPoolConfig;
	}
	/**
	 * redis连接的基础设置
	 * 
	 * @Description:
	 * @return
	 */
	@Primary
	@Bean(name = "redisConnectionFactory")
	public RedisConnectionFactory redisConnectionFactory() {
		JedisConnectionFactory factory = new JedisConnectionFactory();
		factory.setHostName(host);
		factory.setPort(port);
		factory.setPassword(password);
		// 存储的库
		factory.setDatabase(3);
		// 设置连接超时时间
		factory.setTimeout(timeout);
		factory.setUsePool(true);
		factory.setPoolConfig(jedisPoolConfig());
		logger.info("redis factory3 inited finish...host:" + host + ",port:" + port);
		return factory;
	}

	@Primary
	@Bean(name = "redisTemplate")
	public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
		redisTemplate.setConnectionFactory(redisConnectionFactory());
		setRedisTemplateSerializer(redisTemplate);
		logger.info("redis restTemplate3 inited finish...");
		return redisTemplate;
	}


	/// database 1
	/**
	 * redis连接的基础设置1
	 * 
	 * @Description:
	 * @return
	 */
	@Bean(name = "redisConnectionFactory1")
	public RedisConnectionFactory redisConnectionFactory1() {
		JedisConnectionFactory factory = new JedisConnectionFactory();
		factory.setHostName(host);
		factory.setPort(port);
		factory.setPassword(password);
		// 存储的库
		factory.setDatabase(1);
		// 设置连接超时时间
		factory.setTimeout(timeout);
		factory.setUsePool(true);
		factory.setPoolConfig(jedisPoolConfig());
		logger.info("redis factory1 inited finish...host:" + host + ",port:" + port);
		return factory;
	}

	@Bean(name = "redisTemplate1")
	public RedisTemplate<String, Object> redisTemplate1(RedisConnectionFactory factory) {
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
		redisTemplate.setConnectionFactory(redisConnectionFactory1());
		setRedisTemplateSerializer(redisTemplate);
		logger.info("redis restTemplate1 inited finish...");
		return redisTemplate;
	}


	/// database 0
	/**
	 * redis连接的基础设置0
	 * 
	 * @Description:
	 * @return
	 */
	@Bean(name = "redisConnectionFactory0")
	public RedisConnectionFactory redisConnectionFactory0() {
		JedisConnectionFactory factory = new JedisConnectionFactory();
		factory.setHostName(host);
		factory.setPort(port);
		factory.setPassword(password);
		// 存储的库
		factory.setDatabase(0);
		// 设置连接超时时间
		factory.setTimeout(timeout);
		factory.setUsePool(true);
		factory.setPoolConfig(jedisPoolConfig());
		logger.info("redis factory0 inited finish...host:" + host + ",port:" + port);
		return factory;
	}

	@Bean(name = "redisTemplate0")
	public RedisTemplate<String, Object> redisTemplate0(RedisConnectionFactory factory) {
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
		redisTemplate.setConnectionFactory(redisConnectionFactory0());
		setRedisTemplateSerializer(redisTemplate);
		logger.info("redis restTemplate0 inited finish...");
		return redisTemplate;
	}

	/// database 8
	/**
	 * redis连接的基础设置8
	 * 
	 * @Description:
	 * @return
	 */
	@Bean(name = "redisConnectionFactory8")
	public RedisConnectionFactory redisConnectionFactory8() {
		JedisConnectionFactory factory = new JedisConnectionFactory();
		factory.setHostName(host);
		factory.setPort(port);
		factory.setPassword(password);
		// 存储的库
		factory.setDatabase(8);
		// 设置连接超时时间
		factory.setTimeout(timeout);
		factory.setUsePool(true);
		factory.setPoolConfig(jedisPoolConfig());
		logger.info("redis factory8 inited finish...host:" + host + ",port:" + port);
		return factory;
	}

	@Bean(name = "redisTemplate8")
	public RedisTemplate<String, Object> redisTemplate8(RedisConnectionFactory factory) {
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
		redisTemplate.setConnectionFactory(redisConnectionFactory8());
		setRedisTemplateSerializer(redisTemplate);
		logger.info("redis restTemplate8 inited finish...");
		return redisTemplate;
	}
	
	/**
	 * 设置序列化方式
	 */
	private void setRedisTemplateSerializer(RedisTemplate<String, Object> redisTemplate) {
		StringRedisSerializer redisSerializer = new StringRedisSerializer();
		// 设置Key的序列化方式
		redisTemplate.setKeySerializer(redisSerializer);
		redisTemplate.setHashKeySerializer(redisSerializer);
		// JdkSerializationRedisSerializer序列化方式;
		// JdkSerializationRedisSerializer jdkRedisSerializer=new
		// JdkSerializationRedisSerializer();
		// redisTemplate.setValueSerializer(jdkRedisSerializer);
		// redisTemplate.setHashValueSerializer(jdkRedisSerializer);
		// 设置value的序列化方式
		redisTemplate.setValueSerializer(redisSerializer);
		redisTemplate.setHashValueSerializer(redisSerializer);
		redisTemplate.afterPropertiesSet();
	}
}

config中添加脚本配置

@Bean
	public DefaultRedisScript<Long> defaultRedisScript() {
		DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
		defaultRedisScript.setResultType(Long.class);
		defaultRedisScript.setScriptText("if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end");
//        defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("delete.lua")));
		return defaultRedisScript;
	}

添加工具类:

package com.fhgl.user.config;

import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * Created by hui.yunfei@qq.com on 2019/6/26
 */
public class RedisLock {

    private static final Long RELEASE_SUCCESS = 1L;


    //不安全
    public static boolean lock(String key, String value,RedisTemplate<String, Object> template) {
        //执行set命令
        //Boolean absent = template.opsForValue().setIfAbsent(key, value, timeout, TimeUnit.MILLISECONDS);//1
        template.setEnableTransactionSupport(true);
        template.multi();
        template.opsForValue().setIfAbsent(key,value);
        template.expire(key,2, TimeUnit.SECONDS);
        List result = template.exec(); // 这里result会返回事务内每一个操作的结果,如果setIfAbsent操作失败后,result[0]会为false。
        if(true == (Boolean) result.get(0)){
            // todo something...
            return true;
        }
        return false;
    }

    public static boolean unlock(String key, String value,DefaultRedisScript<Long> redisScript,RedisTemplate<String, Object> template) {
        //使用Lua脚本:先判断是否是自己设置的锁,再执行删除
        Long result = template.execute(redisScript, Arrays.asList(key,value));
        //返回最终结果
        return RELEASE_SUCCESS.equals(result);
    }

    public static Boolean setLock(String key, String value,RedisTemplate<String, Object> template) {
        SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
            List<Object> exec = null;
            @Override
            @SuppressWarnings("unchecked")
            public Boolean execute(RedisOperations operations) throws DataAccessException {
                operations.multi();
                template.opsForValue().setIfAbsent(key, value);
                template.expire(key,5, TimeUnit.SECONDS);
                exec = operations.exec();
                if(exec.size() > 0) {
                    return (Boolean) exec.get(0);
                }
                return false;
            }
        };
        return template.execute(sessionCallback);
    }



}

setLock加锁、unLock解锁,lock方法废弃。在springboot2.0以上redis自带了设置超时的命令,2.0以下要自己控制超时。具体

参考

Test:

@Test
    public void testThread(){
        ExecutorService executorService= Executors.newFixedThreadPool(3);
        for (int i=0;i<3;i++){

            String uuid= UUID.randomUUID().toString();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("thread in ..."+Thread.currentThread().getName());

                    boolean flag= RedisLock.setLock("lock:timer", uuid,redisTemplate1);
                    System.out.println(Thread.currentThread().getName()+"开启锁:"+flag);
                    if(flag){
                        System.out.println("没有线程在操作"+Thread.currentThread().getName()+"正常执行");
                        try {
                            Thread.sleep(200);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        boolean result= RedisLock.unlock("lock:timer",uuid,redisScript,redisTemplate1);
                        System.out.println(Thread.currentThread().getName()+"释放锁:"+result);
                    }else{
                        System.out.println("已有线程在操作"+Thread.currentThread().getName()+"退出");
                    }
                }
            });


        }

        try {
            Thread.sleep(2500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String uuid= UUID.randomUUID().toString();
        boolean flag= RedisLock.lock("lock:timer", uuid,redisTemplate1);
        System.out.println("主线程获取锁:"+flag);
        if(flag){
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result= RedisLock.unlock("lock:timer",uuid,redisScript,redisTemplate1);
            System.out.println("主线程释放锁:"+result);

        }
    }

result:

thread in ...pool-7-thread-1
thread in ...pool-7-thread-2
thread in ...pool-7-thread-3
pool-7-thread-3开启锁:false
已有线程在操作pool-7-thread-3退出
pool-7-thread-2开启锁:false
已有线程在操作pool-7-thread-2退出
pool-7-thread-1开启锁:true
没有线程在操作pool-7-thread-1正常执行
pool-7-thread-1释放锁:true
主线程获取锁:true
主线程释放锁:true

有一种风险就是操作线程阻塞后,锁超时自动退出,这个时候新进线程会获取到阻塞线程的锁,同样造成并发问题。这个可以把超时时间设置长一点,尽量保证操作线程正常退出。

二:redis官方推荐使用的Redisson高性能分布式锁

导入pom

<dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
         <version>2.7.0</version>
 </dependency>

redissonConfig配置:

package com.fhgl.user.utils;

import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class RedissonConfig {
	@Value("${spring.redis.host}")
	private String host;
	@Value("${spring.redis.password}")
	private String password;
	@Value("${spring.redis.port}")
	private int port;
	@Value("${spring.redis.maxWaitMillis}")
	private int maxWaitMillis;
	@Value("${spring.redis.maxTotal}")
	private int maxTotal;
	@Value("${spring.redis.testOnBorrow}")
	private boolean testOnBorrow;
	@Value("${spring.redis.testOnReturn}")
	private boolean testOnReturn;
	@Value("${spring.redis.timeout}")
	private int timeout;
	// @Value("${spring.redis.database}")
	// private int database;
	@Value("${spring.redis.maxIdle}")
	private int maxIdle;
	
	private static final String REDIS_PROTOCOL = "redis://";
	
	public String getHost() {
		return host;
	}
	public String getPassword() {
		return password;
	}
	public int getPort() {
		return port;
	}
	public int getMaxWaitMillis() {
		return maxWaitMillis;
	}
	public int getMaxTotal() {
		return maxTotal;
	}
	public boolean isTestOnBorrow() {
		return testOnBorrow;
	}
	public boolean isTestOnReturn() {
		return testOnReturn;
	}
	public int getTimeout() {
		return timeout;
	}
	public int getMaxIdle() {
		return maxIdle;
	}
	
	@Override
	public String toString() {
		return "RedissonConfig [host=" + host + ", password=" + password + ", port=" + port + ", maxWaitMillis="
				+ maxWaitMillis + ", maxTotal=" + maxTotal + ", testOnBorrow=" + testOnBorrow + ", testOnReturn="
				+ testOnReturn + ", timeout=" + timeout + ", maxIdle=" + maxIdle + "]";
	}
	
	/**
	 * 获取redisson配置类
	 * */
	public Config getConfig() {
		Config config = new Config();
		config.useSingleServer().setAddress(REDIS_PROTOCOL + host + ":" + port)
		.setPassword(password)
		.setDatabase(3)
		.setConnectionMinimumIdleSize(maxIdle)
		.setConnectionPoolSize(maxTotal)
		.setConnectTimeout(timeout);
		System.out.println("================config:" + host + "," + port);
		return config;
	}
}

component类:

package com.fhgl.user.utils;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

@Component
public class RedissonManager {
	private Config config;
	private RedissonClient client;
	private final static String LOCK_NAME = "RM:LOCK";
	@Autowired
	private RedissonConfig redissonConfig;
	@PostConstruct
	public void init() {
		System.out.println("====================redisson init");
		if(null == client) {
			System.out.println("====================redisson creating");
			config = redissonConfig.getConfig();
			client = Redisson.create(config);
			System.out.println("====================redisson create fin");
		}
	}
	
	public RedissonClient getClient() {
		config = redissonConfig.getConfig();
		RedissonClient c = Redisson.create(config);
		return c;
	}
	
	/**
	 * 获取锁
	 * @param timeout 自动解锁时间(秒),不需要则传-1
	 * */
	public RLock getLock(String key,long timeout) {
//		System.out.println("====================locking");
		RLock lock = client.getLock(key);
		if(-1 == timeout) {
			lock.lock();
		}else {
			lock.lock(timeout, TimeUnit.SECONDS);
		}
//		System.out.println("====================locked");
		return lock;
	}
	
	public boolean releaseLock(String key) {
		RLock lock = client.getLock(key);
		if(null != lock) {
//			System.out.println("hold count:" + lock.getHoldCount());
			lock.unlock();
			return true;
		}else {
//			System.out.println("=============没有找到锁");
			return false;
		}
//		System.out.println("=============unlock");
	}

	public boolean tryLock(String key){
		RLock lock = client.getLock(key);
		try {
			return lock.tryLock(0,3,TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			e.printStackTrace();
			return false;
		}
//		System.out.println("====================locked");
	}
}

getLock实现了阻塞获取锁队列。

tryLock返回Boolean类型可以根据结果来处理业务逻辑。tryLock的第一个参数是获取锁阻塞时间,就是获取不到会在这段时间一直尝试获取。第二个参数是超时时间。

Test:

@Test
    public void testRedis(){
        ExecutorService executorService= Executors.newFixedThreadPool(3);
        for (int i=0;i<3;i++){

            String uuid= UUID.randomUUID().toString();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("thread in ..."+Thread.currentThread().getName());

                    boolean result=manager.tryLock("lock:timer");

                    System.out.println(Thread.currentThread().getName()+"开启锁:"+result);
                    if(result){

                        System.out.println("没有线程在操作"+Thread.currentThread().getName()+"正常执行");
                        try {
                            Thread.sleep(200);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        boolean result2=manager.releaseLock("lock:timer");
                        System.out.println(Thread.currentThread().getName()+"释放锁:"+result2);
                    }else{
                        System.out.println("已有线程在操作"+Thread.currentThread().getName()+"退出");
                    }
                }
            });


        }

        try {
            Thread.sleep(3100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String uuid= UUID.randomUUID().toString();
        boolean result =manager.tryLock("lock:timer");
        System.out.println("主线程获取锁:"+result);
        if(result){
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean flag= manager.releaseLock("lock:timer");
            System.out.println("主线程释放锁:"+flag);

        }
    }

 

result:

thread in ...pool-7-thread-1
thread in ...pool-7-thread-2
thread in ...pool-7-thread-3
pool-7-thread-1开启锁:false
已有线程在操作pool-7-thread-1退出
pool-7-thread-2开启锁:true
没有线程在操作pool-7-thread-2正常执行
pool-7-thread-3开启锁:false
已有线程在操作pool-7-thread-3退出
pool-7-thread-2释放锁:true
主线程获取锁:true
主线程释放锁:true