Redis实现锁机制的方式大致有两种,第一种是借助setnx命令实现锁机制,第二种是通过lua脚本实现锁机制。

RedisConfig类:

  1. package com.fly.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.data.redis.connection.RedisConnectionFactory;
  5. import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
  6. import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
  7. import org.springframework.data.redis.core.RedisTemplate;
  8. import org.springframework.data.redis.core.StringRedisTemplate;
  9. import org.springframework.data.redis.listener.RedisMessageListenerContainer;
  10. import org.springframework.data.redis.serializer.*;
  11. @Configuration
  12. public class RedisConfiguration {
  13. private StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
  14. private GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer=new GenericJackson2JsonRedisSerializer();
  15. @Bean
  16. public RedisConnectionFactory redisConnectionFactory(){
  17. RedisStandaloneConfiguration config=new RedisStandaloneConfiguration();
  18. config.setHostName("172.16.178.128");
  19. config.setPort(6379);
  20. config.setPassword("123456");
  21. LettuceConnectionFactory factory = new LettuceConnectionFactory(config);
  22. return factory;
  23. }
  24. @Bean
  25. public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
  26. RedisTemplate<String, Object> template = new RedisTemplate<>();
  27. template.setConnectionFactory(factory);
  28. /**
  29. * 设置key采用String序列化方式,Redis存取默认使用JdkSerializationRedisSerializer序列化,
  30. * 这种序列化会key的前缀添加奇怪的字符,例如\xac\xed\x00\x05t\x00user_id,
  31. * 使用StringRedisSerializer序列化可以去掉这种字符
  32. */
  33. template.setKeySerializer(stringRedisSerializer);
  34. template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
  35. // hash的key也采用String的序列化方式
  36. template.setHashKeySerializer(stringRedisSerializer);
  37. // value序列化方式采用jackson
  38. template.setValueSerializer(jackson2JsonRedisSerializer);
  39. // hash的value序列化方式采用jackson
  40. template.setHashValueSerializer(jackson2JsonRedisSerializer);
  41. template.afterPropertiesSet();
  42. /*
  43. * 开启Redis事务,默认是关闭的。也可以手动开启事务,
  44. * 通过template.multi()开启事务,template.exec()关闭事务
  45. */
  46. template.setEnableTransactionSupport(true);
  47. return template;
  48. }
  49. @Bean
  50. public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory){
  51. StringRedisTemplate template=new StringRedisTemplate();
  52. template.setConnectionFactory(factory);
  53. template.setKeySerializer(stringRedisSerializer);
  54. template.setValueSerializer(jackson2JsonRedisSerializer);
  55. template.afterPropertiesSet();
  56. template.setEnableTransactionSupport(true);
  57. return template;
  58. }
  59. /**
  60. * 注入Redis消息监听器
  61. * @param connectionFactory
  62. * @return
  63. */
  64. @Bean
  65. public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory){
  66. RedisMessageListenerContainer container = new RedisMessageListenerContainer();
  67. container.setConnectionFactory(connectionFactory);
  68. return container;
  69. }
  70. }
  1. #setnx命令复习
  2. set k1 zxp
  3. setnx k1 111 #返回nil,setnx设置的key已存在
  4. setnx k2 222 #返回OK

1.基于setnx命令实现分布式锁(写法1)

Redis实现锁机制的原理是依靠setnx命令,setnx在设置key时只有当key不存在时才会设置成功,否则会设置key失败。而锁机制的特性就是互斥,同一时间只能拥有一把锁,其他资源则等待已经获取锁的资源释放锁,且获取锁的资源能重新获取锁,即可得出锁具有互斥性和可重入性的。
把思维转变过来,我们可以把setnx设置key看作为加锁,当setnx设置key成功时就说明加锁成功,当setnx设置key失败时可以看做加锁失败,可能有其他资源在持有锁,那么剩余的资源都需要等待锁。锁应该具有瞬时性,不然一个资源可以一直持有一个锁,则会导致其他资源一直处于等待获取锁状态,这显然是不友好的,所以锁应该具有超时时间,过了超时时间锁会自动被释放掉,通过Redis key的过期时间机制非常容易实现这个功能。

具体流程如下:
(1).加锁时使用setnx命令设置一个key,key对应的value值为过期时间,过期时间为当前时间+默认的过期时间,这个过期时间也就是锁的过期时间。
(2).若setnx命令设置key成功则说明加锁成功。
(3).若setnx命令设置key失败说明Redis中可能已经存在对应key了,表示加锁失败。
(4).如果Redis存在key且对应value不为空,而且这个key已过期,这说明一直持有锁没有释放掉,所以需要重新上锁,防止死锁,避免多个线程抢锁。

package com.fly.lock;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Objects;

/**
 * 使用setnx命令实现分布式锁机制写法1。
 * 核心原理是通过setnx命令实现加锁动作,通过delete命令实现解锁动作。
 * 优点:轻量级、性能好。
 * 缺点:
 *    (1).delete命令存在误删除非当前线程持有的锁的可能
 *    (2).不支持阻塞等待、不可重入
 */
@Component
public class BaseLock01 {

    /**
     * 分布式锁前缀
     */
    private static final String LOCK_PREFIX="redis_lock";
    /**
     * 分布式锁过期时间,默认300ms
     */
    private static final Long LOCK_EXPIRE = 300L;

    @Autowired
    RedisTemplate<String,Object> redisTemplate;

    /**
     * 加锁
     * @param k
     * @return true为加锁成功,false为加锁失败
     */
    public boolean tryLock(String k){
        String keyName=LOCK_EXPIRE+'_'+k;
        Long expire=LOCK_EXPIRE+System.currentTimeMillis();
        /**
         * setIfAbsent 对标setnx命令,若设置key在Redis不存在则设置成功,
         * 否则设置失败。
         */
        Boolean result=redisTemplate.opsForValue().setIfAbsent(keyName,expire);
        if(result){
            return true;
        }
        /**
         * 若Redis存在当前设置的key则获取到key对应的值
         */
        Long value =(Long) redisTemplate.opsForValue().get(keyName);
        /**
         * 如果key(锁)过期,此时可能会出现死锁,一个线程长期持有锁却未释放掉,
         * 需要重新加锁。下面代码有防止死锁、避免多个线程抢锁的作用。
         */
        if(null!=value && value < System.currentTimeMillis()){
            /**
             * 重新加锁,getAndSet对标getset命令,获取key对应的value并设置key的value值
             */
            Object o=redisTemplate.opsForValue().getAndSet(keyName,value+System.currentTimeMillis());
            //判断重新加锁是否成功
            if(Objects.nonNull(o) && o.equals(value)){
                return true;
            }
        }
        return false;
    }

    /**
     * 解锁
     */
    public Boolean unLock(String k){
        String keyName=LOCK_PREFIX+"_"+k;
        return redisTemplate.delete(keyName);
    }
}

2.基于setnx命令实现分布式锁(写法2)

package com.fly.lock;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Objects;

/**
 * 使用setnx命令实现分布式锁机制写法2。
 */
@Component
public class BaseLock02 {
    /**
     * 分布式锁前缀
     */
    private static final String LOCK_PREFIX="redis_lock";
    /**
     * 分布式锁过期时间,默认300ms
     */
    private static final Long LOCK_EXPIRE = 100L;

    private RedisTemplate<String,Object> redisTemplate;

    @Autowired
    public BaseLock02(RedisTemplate<String,Object> redisTemplate){
        this.redisTemplate=redisTemplate;
    }

    /**
     * 加锁
     * @return
     */
    public Boolean tryLock(String k){
        /*key的全名称*/
        final byte[] keyName=(LOCK_PREFIX +'_'+k).getBytes();
        /*key过期时间,也是key对应的value值*/
        final Long expire=LOCK_EXPIRE+System.currentTimeMillis();

        /**
         * conn是一个RedisConnection对象,RedisConnection对象提供了通过二进制的方式操作Redis
         */
        redisTemplate.execute((RedisCallback)  conn->{
            //setNX()对标setnx命令
            Boolean acquire = conn.setNX(keyName, String.valueOf(expire).getBytes());
            if(acquire) return true;
            /**
             * 加锁失败说明Redis中已存在设置key
             */
            final byte[] value = conn.get(keyName);
            if(Objects.nonNull(value) && value.length > 0){
                //获取过期时间
                Long expireTime = Long.parseLong(new String(value));
                //如果锁已经过期,则需要重新加锁,防止死锁、避免多个线程抢锁
                if(expireTime < System.currentTimeMillis()){
                    byte[] oldValue = conn.getSet(keyName,String.valueOf(expireTime+System.currentTimeMillis()).getBytes());
                    //判断是否加锁成功
                    return Long.parseLong(new String(oldValue)) < System.currentTimeMillis();
                }
            }

            return false;
        });
        return false;
    }
    /**
     * 解锁
     */
    public Boolean unLock(String k){
        String keyName=LOCK_PREFIX+"_"+k;
        return redisTemplate.delete(keyName);
    }
}