Redis实现分布式锁
使用Redis SETNX实现
SETNX命令(SET if Not eXists) 语法:SETNX key value 功能:原子性操作,当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。
注意:
使用SETNX和expire实现锁的时候,一定要保证谁拿到的锁,谁去释放锁,不能出现已经过期的锁,释放了别人持有的锁,这里使用lua脚本来实现,而不是使用del key的方式
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.StringRedisTemplate; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.UUID; import lombok.extern.slf4j.Slf4j; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisCommands; /** * Redis分布式锁 使用 SET resource-name anystring NX EX max-lock-time 实现 * * <p> 该方案在 Redis 官方 SET 命令页有详细介绍。 http://doc.redisfans.com/string/set.html * * <p> 在介绍该分布式锁设计之前,我们先来看一下在从 Redis 2.6.12 开始 SET 提供的新特性, 命令 SET key value [EX seconds] [PX * milliseconds] [NX|XX],其中: * * <p> EX seconds — 以秒为单位设置 key 的过期时间; PX milliseconds — 以毫秒为单位设置 key 的过期时间; NX — 将key 的值设为value * ,当且仅当key 不存在,等效于 SETNX。 XX — 将key 的值设为value ,当且仅当key 存在,等效于 SETEX。 * * <p> 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。 * * <p> 客户端执行以上的命令: * * <p> 如果服务器返回 OK ,那么这个客户端获得锁。 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。 */ @Slf4j public class SmartRedisLock { private StringRedisTemplate redisTemplate; /** * 将key 的值设为value ,当且仅当key 不存在,等效于 SETNX。 */ private static final String NX = "NX"; /** * seconds — 以秒为单位设置 key 的过期时间,等效于EXPIRE key seconds */ private static final String EX = "EX"; /** * 调用set后的返回值 */ private static final String OK = "OK"; /** * 默认请求锁的超时时间(ms 毫秒) */ private static final long TIME_OUT = 1000; /** * 默认锁的有效时间(s) */ private static final int EXPIRE = 60; /** * 解锁的lua脚本 */ private static final String UNLOCK_LUA; /** * KEYS数组的内容 * -- KEYS[1] - key -- KEYS[2] - ttl in ms -- KEYS[3] - lock content */ 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(); } /** * 锁标志对应的key */ private String lockKey; /** * 锁对应的值 */ private String lockValue; /** * 锁的有效时间(s) */ private long expireTime = EXPIRE; /** * 锁标记:保证一个线程修改了值,其他线程立即能获取到最新的值 */ private volatile boolean locked = false; private final Random random = new Random(); /** * 使用默认的锁过期时间和请求锁的超时时间 * * @param lockKey 锁的key(Redis的Key) */ public SmartRedisLock(StringRedisTemplate redisTemplate, String lockKey) { this.redisTemplate = redisTemplate; this.lockKey = lockKey + "_lock"; } /** * 使用默认的请求锁的超时时间,指定锁的过期时间 * * @param lockKey 锁的key(Redis的Key) * @param expireTime 锁的过期时间(单位:秒) */ public SmartRedisLock(StringRedisTemplate redisTemplate, String lockKey, long expireTime) { this(redisTemplate, lockKey); this.expireTime = expireTime; } /** * 尝试获取锁 超时返回,在超时时间内会重试 */ public boolean tryLock() { // 生成随机key lockValue = UUID.randomUUID().toString(); // 请求锁超时时间, long nowTime = System.nanoTime(); while ((System.nanoTime() - nowTime) < TIME_OUT * 1000000) { if (OK.equalsIgnoreCase(this.set(lockKey, lockValue, expireTime))) { locked = true; // 上锁成功结束请求 return true; } // 每次请求等待一段时间 seleep(); } return locked; } /** * 使用场景:同一个时间点,只允许一个线程请求,其余线程拒绝 * * 尝试获取锁 立即返回 * * @return 是否成功获得锁 */ public boolean lock() { lockValue = UUID.randomUUID().toString(); //不存在则添加 且设置过期时间(单位ms) String result = set(lockKey, lockValue, expireTime); locked = OK.equalsIgnoreCase(result); return locked; } /** * 以阻塞方式的获取锁(等待前一个线程释放) * * 使用场景:多线程请求时,排队请求 * * @return 是否成功获得锁 */ public boolean lockBlock() { lockValue = UUID.randomUUID().toString(); while (true) { //不存在则添加 且设置过期时间(单位ms) String result = set(lockKey, lockValue, expireTime); if (OK.equalsIgnoreCase(result)) { locked = true; return locked; } // 每次请求等待一段时间 seleep(); } } /** * 解锁 <p> 可以通过以下修改,让这个锁实现更健壮: * * <p> 不使用固定的字符串作为键的值,而是设置一个不可猜测(non-guessable)的长随机字符串,作为口令串(token)。 * * 不使用 DEL 命令来释放锁,而是发送一个 Lua 脚本,这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除。 * * 这两个改动可以防止持有过期锁的客户端误删现有锁的情况出现。 */ public Boolean unlock() { // 只有加锁成功并且锁还有效才去释放锁 if (locked) { return redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { Object nativeConnection = connection.getNativeConnection(); Long result = 0L; List<String> keys = new ArrayList<>(); keys.add(lockKey); List<String> values = new ArrayList<>(); values.add(lockValue); // 集群模式 if (nativeConnection instanceof JedisCluster) { result = (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, values); } // 单机模式 if (nativeConnection instanceof Jedis) { result = (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, values); } if (result == 0) { log.info("Redis分布式锁,解锁{}失败!解锁时间:{}", System.currentTimeMillis()); } locked = result == 0; return result == 1; } }); } return true; } /** * 重写redisTemplate的set方法 * * 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。 * * <p> 客户端执行以上的命令: * * <p> 如果服务器返回 OK ,那么这个客户端获得锁。 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。 * * @param key 锁的Key * @param value 锁里面的值 * @param seconds 过去时间(秒) */ private String set(final String key, final String value, final long seconds) { log.debug("lockKey:{}", key); Assert.isTrue(!StringUtils.isEmpty(key), "key不能为空"); return redisTemplate.execute(new RedisCallback<String>() { @Override public String doInRedis(RedisConnection connection) throws DataAccessException { Object nativeConnection = connection.getNativeConnection(); String result = null; if (nativeConnection instanceof JedisCommands) { result = ((JedisCommands) nativeConnection).set(key, value, NX, EX, seconds); } return result; } }); } /** * @Title: seleep * @Description: 线程等待时间 */ private void seleep() { try { Thread.sleep(10, random.nextInt(50000)); } catch (InterruptedException e) { log.info("获取分布式锁休眠被中断:", e); } } }
作者:guanbin —— 纵码万里千山
出处:https://www.cnblogs.com/guanbin-529/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。