• Redis分布式锁


    加锁

    所以需要保证设置锁及其过期时间两个操作的原子性,spring data的 RedisTemplate 当中并没有这样的方法。
    但是在jedis当中是有这种原子操作的方法的,需要通过 RedisTemplate 的 execute 方法获取到jedis里操作命令的对象,代码如下:

    String result = redisTemplate.execute(new RedisCallback<String>() {
    	@Override
    	public String doInRedis(RedisConnection connection) throws DataAccessException {
    		JedisCommands commands = (JedisCommands) connection.getNativeConnection();
    		return commands.set(key, "锁定的资源", "NX", "PX", expire);
    	}
    });
    

    NX: 表示只有当锁定资源不存在的时候才能 SET 成功。利用 Redis 的原子性,保证了只有第一个请求的线程才能获得锁,而之后的所有线程在锁定资源被释放之前都不能获得锁。

    PX: expire 表示锁定的资源的自动过期时间,单位是毫秒。具体过期时间根据实际场景而定

    这样在获取锁的时候就能够保证设置 Redis 值和过期时间的原子性,避免前面提到的两次 Redis 操作期间出现意外而导致的锁不能释放的问题。但是这样还是可能会存在一个问题,考虑如下的场景顺序:

    1)线程T1获取锁
    2)线程T1执行业务操作,由于某些原因阻塞了较长时间
    3)锁自动过期,即锁自动释放了
    4)线程T2获取锁
    5)线程T1业务操作完毕,释放锁(其实是释放的线程T2的锁
    [[按照这样的场景顺序,线程T2的业务操作实际上就没有锁提供保护机制了。]]
    所以,每个线程释放锁的时候只能释放自己的锁,即锁必须要有一个拥有者的标记,并且也需要保证释放锁的原子性操作。

    因此在获取锁的时候,可以生成一个随机不唯一的串放入当前线程中,然后再放入 Redis 。
    释放锁的时候先判断锁对应的值是否与线程中的值相同,相同时才做删除操作。

    释放锁

    LUA脚本
    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
    -----------------------------------------------
    // 使用Lua脚本删除Redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
    // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
    Long result = redisTemplate.execute(new RedisCallback<Long>() {
    	public Long doInRedis(RedisConnection connection) throws DataAccessException {
    		Object nativeConnection = connection.getNativeConnection();
    		// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
    		// 集群模式
    		if (nativeConnection instanceof JedisCluster) {
    			return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
    		}
    
    		// 单机模式
    		else if (nativeConnection instanceof Jedis) {
    			return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
    		}
    		return 0L;
    	}
    });
    
    

    代码中分为集群模式和单机模式,并且两者的方法、参数都一样;
    原因是spring封装的执行脚本的方法中( RedisConnection 接口继承于 RedisScriptingCommands 接口的 eval 方法);
    集群模式的方法直接抛出了不支持执行脚本的异常(虽然实际是支持的),所以只能拿到 Redis 的connection来执行脚本;
    而 JedisCluster 和 Jedis 中的方法又没有实现共同的接口,所以只能分开调用。

    DistributedLock.java 顶级接口

    public interface DistributedLock {
    	
    	public static final long TIMEOUT_MILLIS = 30000;
    	
    	public static final int RETRY_TIMES = Integer.MAX_VALUE;
    	
    	public static final long SLEEP_MILLIS = 500;
    
    	public boolean lock(String key);
    	
    	public boolean lock(String key, int retryTimes);
    	
    	public boolean lock(String key, int retryTimes, long sleepMillis);
    	
    	public boolean lock(String key, long expire);
    	
    	public boolean lock(String key, long expire, int retryTimes);
    	
    	public boolean lock(String key, long expire, int retryTimes, long sleepMillis);
    	
    	public boolean releaseLock(String key);
    }
    

    AbstractDistributedLock.java 抽象类,实现基本的方法,关键方法由子类去实现

    public abstract class AbstractDistributedLock implements DistributedLock {
    
    	@Override
    	public boolean lock(String key) {
    		return lock(key, TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS);
    	}
    
    	@Override
    	public boolean lock(String key, int retryTimes) {
    		return lock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS);
    	}
    
    	@Override
    	public boolean lock(String key, int retryTimes, long sleepMillis) {
    		return lock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis);
    	}
    
    	@Override
    	public boolean lock(String key, long expire) {
    		return lock(key, expire, RETRY_TIMES, SLEEP_MILLIS);
    	}
    
    	@Override
    	public boolean lock(String key, long expire, int retryTimes) {
    		return lock(key, expire, retryTimes, SLEEP_MILLIS);
    	}
    
    }
    

    RedisDistributedLock.java Redis分布式锁的实现

    import java.util.ArrayList;
    import java.util.List;
    import java.util.UUID;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.dao.DataAccessException;
    import org.springframework.data.redis.connection.RedisConnection;
    import org.springframework.data.redis.core.RedisCallback;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.util.StringUtils;
    
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisCluster;
    import redis.clients.jedis.JedisCommands;
    
    
    public class RedisDistributedLock extends AbstractDistributedLock {
    	
    	private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);
    	
    	private RedisTemplate<Object, Object> redisTemplate;
    	
    	private ThreadLocal<String> lockFlag = new ThreadLocal<String>();
    	
    	public static final String UNLOCK_LUA;
    
        static {
            StringBuilder sb = new StringBuilder();
            sb.append("if redis.call("get",KEYS[1]) == ARGV[1] ");
            sb.append("then ");
            sb.append("    return redis.call("del",KEYS[1]) ");
            sb.append("else ");
            sb.append("    return 0 ");
            sb.append("end ");
            UNLOCK_LUA = sb.toString();
        }
    
    	public RedisDistributedLock(RedisTemplate<Object, Object> redisTemplate) {
    		super();
    		this.redisTemplate = redisTemplate;
    	}
    
    	@Override
    	public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
    		boolean result = setRedis(key, expire);
    		// 如果获取锁失败,按照传入的重试次数进行重试
    		while((!result) && retryTimes-- > 0){
    			try {
    				logger.debug("lock failed, retrying..." + retryTimes);
    				Thread.sleep(sleepMillis);
    			} catch (InterruptedException e) {
    				return false;
    			}
    			result = setRedis(key, expire);
    		}
    		return result;
    	}
    	
    	private boolean setRedis(String key, long expire) {
    		try {
    			String result = redisTemplate.execute(new RedisCallback<String>() {
    				@Override
    				public String doInRedis(RedisConnection connection) throws DataAccessException {
    					JedisCommands commands = (JedisCommands) connection.getNativeConnection();
    					String uuid = UUID.randomUUID().toString();
    					lockFlag.set(uuid);
    					return commands.set(key, uuid, "NX", "PX", expire);
    				}
    			});
    			return !StringUtils.isEmpty(result);
    		} catch (Exception e) {
    			logger.error("set redis occured an exception", e);
    		}
    		return false;
    	}
    	
    	@Override
    	public boolean releaseLock(String key) {
    		// 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
    		try {
    			List<String> keys = new ArrayList<String>();
    			keys.add(key);
    			List<String> args = new ArrayList<String>();
    			args.add(lockFlag.get());
    
    			// 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
    			// spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
    			
    			Long result = redisTemplate.execute(new RedisCallback<Long>() {
    				public Long doInRedis(RedisConnection connection) throws DataAccessException {
    					Object nativeConnection = connection.getNativeConnection();
    					// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
    					// 集群模式
    					if (nativeConnection instanceof JedisCluster) {
    						return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
    					}
    
    					// 单机模式
    					else if (nativeConnection instanceof Jedis) {
    						return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
    					}
    					return 0L;
    				}
    			});
    			
    			return result != null && result > 0;
    		} catch (Exception e) {
    			logger.error("release lock occured an exception", e);
    		}
    		return false;
    	}
    	
    }
    
  • 相关阅读:
    EditPlus等编辑器选中列(块)的方法
    构建Springboot项目的3种方式
    STS各版本下载
    Spring Boot 各版本的Java版本要求
    Maven安装
    Linux find命令:在目录中查找文件(超详解)
    rpm命令怎么指定安装位置
    CentOS6.8安装RabbitMQ
    codeforces459D:Pashmak and Parmida's problem
    codeforces 705B:Spider Man
  • 原文地址:https://www.cnblogs.com/xidianzxm/p/10413219.html
Copyright © 2020-2023  润新知