Redis 分布式锁

目前Java操作redis的客户端有jedis跟lettuce。   在springboot1.x系列中,其中使用的是jedis,但是到了springboot2.x其中使用的是Lettuce。 因为我们的版本是springboot2.x系列,所以今天使用的是Lettuce。


jedis跟lettuce的区别:

  • Lettuce 和 Jedis 的定位都是Redis的client,所以他们当然可以直接连接redis server。
  • Jedis在实现上是直接连接的redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个Jedis实例增加物理连接
  • Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,应为StatefulRedisConnection是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。
     

基于Spring-boot-start-data-redis的实现:

1.引入jar包

   使用lettuce, 直接引入spring-boot-start-data-redis

2.配置文件:bootstrap-dev.yml

使用lettuce:

server:
  port: 8989
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    # 密码 没有则可以不填
    password: 123456
    # 如果使用的jedis 则将lettuce改成jedis即可
    lettuce:
      pool:
        # 最大活跃链接数 默认8
        max-active: 8
        # 最大空闲连接数 默认8
        max-idle: 8
        # 最小空闲连接数 默认0
        min-idle: 0

 3 RedisConfig.java  

我们需要配置redis的key跟value的序列化方式,默认使用的JdkSerializationRedisSerializer 这样的会导致我们通过redis desktop manager显示的我们key跟value的时候显示不是正常字符。 所以我们需要手动配置一下序列化方式 新建一个config包,在其下新建一个RedisConfig.java 具体代码如下
 

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
 
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}
  1. @Configuration 代表这个类是一个配置类;
  2. @AutoConfigureAfter(RedisAutoConfiguration.class) :是让我们这个配置类在内置的配置类(RedisAutoConfiguration.class)之后在配置,这样就保证我们的配置类生效,并且不会被覆盖配置。
  3. 其中需要注意的就是方法名一定要叫redisTemplate 因为@Bean注解是根据方法名配置这个bean的name的。

4 CacheService.java
 

@Service
public class CacheService {
 
    private static Logger logger = LoggerFactory.getLogger(CacheService.class);
 
    private static final Long SUCCESS = 1L;
 
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
 
    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return false;
        }
    }
 
    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
 
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }
 
    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return false;
        }
    }
 
    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }
 
    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
 
    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return false;
        }
    }
 
    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return false;
        }
    }
 
    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }
 
    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于0)
     * @return 147
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
 
       /**
     * 获取分布式锁
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 单位秒   锁key的超时时间:防止死锁
     * @param waitTimeout 单位毫秒  获取锁的超时时间
     * @return 是否获取成功
     */
    public boolean tryLock(String lockKey, String requestId, int expireTime,long waitTimeout) {
        long nanoTime = System.nanoTime(); // 当前时间
        try{
            String script = "if redis.call('setNx',KEYS[1],ARGV[1]) then if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end end";
 
            logger.info("开始获取分布式锁-key[{}]",lockKey);
            int count = 0;
            do{
                RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
 
                logger.debug("尝试获取分布式锁-key[{}]requestId[{}]count[{}]",lockKey,requestId,count);
                Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey),requestId,expireTime);
 
                if(SUCCESS.equals(result)) {
                    logger.debug("尝试获取分布式锁-key[{}]成功",lockKey);
                    return true;
                }
 
                Thread.sleep(500L);//休眠500毫秒
                count++;
            }while ((System.nanoTime() - nanoTime) < TimeUnit.MILLISECONDS.toNanos(waitTimeout));
 
        }catch(Exception e){
            logger.error("尝试获取分布式锁-key[{}]异常",lockKey);
            logger.error(e.getMessage(),e);
        }
 
        return false;
    }
 
    /**
     * 释放锁
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public boolean releaseLock(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";
 
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
 
        Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId);
        if (SUCCESS.equals(result)) {
            return true;
        }
 
        return false;
 
    }
}

 代码地址:https://github.com/tengxvincent/spring-boot-vincent.git


public static StringRedisTemplate redisTemplate;

//    public static final long expireTime = 20;
    private static final Long SUCCESS = 1L;
    private static final String redisKey = "RedisLock:";
    private static final int DEFAULT_SLEEP_TIME = 100;



    /**
     *
     * @param lockKey
     * @param value
     * @param expireTime 过期时间 秒
     * @return
     */
    public static boolean tryLock(String lockKey, String value,int expireTime) {
        int count = 5;
        Boolean result = false;
        try {
            while (count > 0) {
                result = setIfAbsent(redisKey+lockKey,value,expireTime);
                count --;
                if (result) {
                    return true;
                } else {
                    Thread.sleep(DEFAULT_SLEEP_TIME);
                }
            }
        } catch (Exception e) {
            log.info("redis lock  {} error {}!",lockKey, e);
        }
        if(!result){
            log.info("try redis lock {} fail ",lockKey);
        }
        return result;
    }


public static boolean setIfAbsent(final String key, final Serializable value, final long exptime) {
    Boolean result = (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
        @Override
        public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
            RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
            RedisSerializer keySerializer = redisTemplate.getKeySerializer();
            Object obj = connection.execute("set", keySerializer.serialize(key),
                    valueSerializer.serialize(value),
                    "NX".getBytes(StandardCharsets.UTF_8),
                    "EX".getBytes(StandardCharsets.UTF_8),
                    String.valueOf(exptime).getBytes(StandardCharsets.UTF_8));
            return obj != null;
        }
    });
    return result;
}

public static boolean unlock(String lockKey, String value) {
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
    Object result = redisTemplate.execute(redisScript, Collections.singletonList(redisKey+lockKey), value);
    if (SUCCESS.equals(result)) {
       // log.info("release redis lock {} success",lockKey);
        return true;
    }
    log.info("release redis {} lock fail",lockKey);
    return false;
}


redis setIfAbsent的使用:

 如果为空就set值,并返回1
 如果存在(不为空)不进行操作,并返回0

可以在多线程/多实例中控制程序取得锁的执行,未取得锁的不执行。可用于多实例的定时任务或者接口方法; 

 public boolean getLock(){
  Boolean setLockFalg =false;
  try {
   setLockFalg =redisTemplate.opsForValue().setIfAbsent(nameSpaceLock, "1"); //设置锁是否成功,如果锁被占用则返回false,
   if(setLockFalg!=null){
    while(setLockFalg==false){ //没有获取到锁,继续等
//      Thread.sleep(500);
     TimeUnit.MILLISECONDS.sleep(500);
     setLockFalg =redisTemplate.opsForValue().setIfAbsent(nameSpaceLock, "1");
    }
    if(setLockFalg){
     redisTemplate.expire(nameSpaceLock, 20, TimeUnit.SECONDS);//当获得锁成功以后,设置20秒失效时间,防止死锁
    }
    return setLockFalg;
   }
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
  return false;
 }


/**
 * 取得锁,
 * @param key String  设置key
 * @param value String 设置value
 * @param  expireTime 过期时间
 * */
@Override
public boolean getLock(String key, String value, int expireTime){

 // 设置锁,设置前判断Key是否已存在,不存在则,设置key,value,
 // 然后返回true。
 // 如果锁已存在 则返回false,
 if(redisTemplate.opsForValue().setIfAbsent(key,value)){
  // 设置Key过期时间,防止死锁
  redisTemplate.expire(key,expireTime, TimeUnit.SECONDS);
  return true;
 } else {
  return false;
 }
}

/**
 * 释放锁,如果锁不存在
 * */
@Override
public void releaseLock(String key){
 redisTemplate.delete(key);
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值