spring boot RedisTemplate 实现 setnx exptime (扩展 redisTemplate.setIfAbsent)

之前用 redisTemplate 实现setnx exptime 时 是分两步的

  1. redisTemplate.setIfAbsent
  2. redisTemplate.expire

这样的不是原子性的 可能在第一步与第二步之间 重新发布了或者服务器重启了 这个key就永远不会消失了 可以采用以下的方法

在spiring boot 1.5 版本 可以使用以下扩展

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;
}

在spiring boot 2 可以直接使用 redisTemplate.opsForValue().setIfAbsent 可以设置时间

Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit);

public boolean setIfAbsent(String key, String value, long expireTime) {

        Boolean result = redisTemplate.opsForValue().setIfAbsent(key,value,expireTime, TimeUnit.SECONDS);

        return result;
}

单机RedisLock的正确姿势 (集群用redisson):
加锁:

 1. 通过setnx 向特定的key写入一个随机数,并设置失效时间,写入成功即加锁成功
 2. 注意点:
    - 必须给锁设置一个失效时间            ----->    避免死锁
    - 加锁时,每个节点产生一个随机字符串    ----->    避免锁误删
    - 写入随机数与设置失效时间必须是同时    ----->    保证加锁的原子性
 使用:
   SET key value NX PX 3000

解锁:

解锁:
	匹配随机数,删除redis上的特定的key数据,
	要保证获取数据,判断一致以及删除数据三个操作是原子性
	为什么要保证原子性:
        为了防止误删
		例如:A 超时了释放了锁 (判断uuid.equals(jedis.get(lockKey)之后 -- 超时,B加锁)
			B获得锁 ,这时 A 完成了释放了B获得的锁
			// 判断加锁与解锁是不是同一个客户端
			if (uuid.equals(jedis.get(lockKey))) {
				// A运行到这里,超时释放锁,B获得锁
				// 若在此时,这把锁突然不是这个客户端的,则会误解锁(这时 A 完成了释放了B获得的锁)
				 jedis.del(lockKey);
              }
	执行如下lua脚本:
		if redis.call('get', KEYS[1]) == ARGV[1] then
			return redis.call('del', KEYS[1])
		else
			return 0
		end

伪代码示例:使用的是Jedis客户端

		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
			<version>2.9.0</version>
		</dependency>
package com.redis.mq.util;

import redis.clients.jedis.Jedis;

import java.util.Collections;
import java.util.UUID;

/**
 * @author qinjp
 * @date 2019-05-31
 **/
public class RedisLock {
    /**
     * RedisLock的正确姿势
     * 加锁:
     * 通过setnx 向特定的key写入一个随机数,并设置失效时间,写入成功即加锁成功
     * 注意点:
     *  必须给锁设置一个失效时间            ----->    避免死锁
     *  加锁时,每个节点产生一个随机字符串    ----->    避免锁误删
     *  写入随机数与设置失效时间必须是同时    ----->    保证加锁的原子性
     *  使用:
     *      SET key value NX PX 3000
     *
     * 解锁:
     *  匹配随机数,删除redis上的特定的key数据,
     *  要保证获取数据,判断一致以及删除数据三个操作是原子性
     *  执行如下lua脚本:
     *      if redis.call('get', KEYS[1]) == ARGV[1] then
     *          return redis.call('del', KEYS[1])
     *      else
     *          return 0
     *      end
     *
     */
    // 使用jedis 客户端的
    /**SET key value NX PX 3000 成功返回值*/
    private static final String LOCK_SUCCESS = "OK";
    /**表示 NX 模式*/
    private static final String SET_IF_NOT_EXIST = "NX";
    /**单位 毫秒**/
    private static final String SET_WITH_EXPIRE_TIME_PX = "PX";
    /**lua脚本**/
    private static final String SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    /**存储随机数**/
    private static final ThreadLocal<String> local = new ThreadLocal<>();
    /**
     * 加锁
     */
    public static boolean lock(Jedis jedis, String key, int expireTime) {
        // 产生随机数
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");

        String result = jedis.set(key, uuid, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME_PX, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            // 随机数绑定线程
            local.set(uuid);
            return true;
        }
        return false;

    }

   /**
     * 释放分布式锁
     */
    public static boolean unLock(Jedis jedis, String key) {

        String uuid = local.get();
	    //当前线程没有绑定uuid
        //直接返回
        if (uuid == null || "".equals(uuid)) {
            return false;
        }
        
        Object result = jedis.eval(SCRIPT, Collections.singletonList(key), Collections.singletonList(uuid));

        if (Long.valueOf(1).equals(result)) {
            // 解除绑定线程的随机数
            local.remove();
            return true;
        }
        return false;
    }


    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        jedis.auth("373616885");
        jedis.select(0);
        final String LOCK_KEY = "LOCK_KEY";
        
        RedisLock.lock(jedis,LOCK_KEY,5000);

        RedisLock.unLock(jedis,LOCK_KEY);

    }

}


  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 19
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值