-
最近看了小D课堂的vip课程,干货满满,特记录下来
-
分布式锁是什么
-
-
分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现
-
如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此干扰。
-
分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。
-
上图!
-
-
分布锁设计目的
可以保证在分布式部署的应用集群中,同一个方法在同一操作只能被一台机器上的一个线程执行。
-
设计要求
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
-
分布锁实现方案分析
-
获取锁的时候,使用 setnx(SETNX key val:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返回 1;
-
若 key 存在,则什么都不做,返回 【0】加锁,锁的 value 值为当前占有锁服务器内网IP编号拼接任务标识
-
在释放锁的时候进行判断。并使用 expire 命令为锁添 加一个超时时间,超过该时间则自动释放锁。
-
返回1则成功获取锁。还设置一个获取的超时时间, 若超过这个时间则放弃获取锁。setex(key,value,expire)过期以秒为单位
-
释放锁的时候,判断是不是该锁(即Value为当前服务器内网IP编号拼接任务标识),若是该锁,则执行 delete 进行锁释放
-
上图!
-
分布式锁源码讲解
- 1导入依赖
-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
- 2这里我们用spring提供的RedisTemplate来简单实现
-
public class LockNxExJob { @Autowired private RedisService redisService; @Autowired private RedisTemplate redisTemplate; private static String LOCK_PREFIX = "prefix_"; public void lockJob() { String lock = LOCK_PREFIX + "LockNxExJob"; boolean nxRet = false; try{ //redistemplate setnx操作 nxRet = redisTemplate.opsForValue().setIfAbsent(lock,"value"); Object lockValue = redisService.genValue(lock); //获取锁失败 if(!nxRet){ String value = (String)redisService.genValue(lock); return; }else{ redisTemplate.opsForValue().set(lock,"value",3600); //获取锁成功,执行任务 Thread.sleep(5000); } }catch (Exception e){ }finally { if(nxRet){ redisService.remove(lock); } } }
-
分布锁满足两个条件,一个是加有效时间的锁,一个是高性能解锁
-
采用redis命令setnx(set if not exist)、setex(set expire value)实现
-
【千万记住】解锁流程不能遗漏,否则导致任务执行一次就永不过期
-
将加锁代码和任务逻辑放在try,catch代码块,将解锁流程放在finally
-
-
Redis分布锁思考,图解分布式锁setnx、setex的缺陷
- 其实仔细思考就会发现这样是实现方式并不完美,想想如果我们在获取锁成功后(setNx),到给锁加超时间(setEx)中间服务器或者redis服务崩溃了 ,是不是就加不到超时时间,锁就不会自动释放了,以后的服务端来获取锁都是失败了,其实在redis2.6版本以前我们是用lua脚本解决这个问题
- 上图!
-
Lua脚本讲解之Redis分布式锁
-
Lua简介
-
从 Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以使用 EVAL 命令对 Lua 脚本进行求值。
-
Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。
-
-
Lua脚本配置流程
-
1、在resource目录下面新增一个后缀名为.lua结尾的文件
-
2、编写lua脚本
-
3、传入lua脚本的key和arg
-
4、调用redisTemplate.execute方法执行脚本
-
-
这里我们写个lua脚本,忽略名字
-
-
这是内容
-
这里我们需要两个方法,一个用来加载执行lua脚本,传入参数,另一个和之前方法一样,这里只是用lua脚本的方式把setNx和setEx命令连在一起执行了,要么同时成功,要么都不执行。
-
/** * 获取lua结果 * @param key * @param value * @return */ public Boolean luaExpress(String key,String value) { lockScript = new DefaultRedisScript<Boolean>(); lockScript.setScriptSource( new ResourceScriptSource(new ClassPathResource("add.lua"))); lockScript.setResultType(Boolean.class); // 封装参数 List<Object> keyList = new ArrayList<Object>(); keyList.add(key); keyList.add(value); Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList); return result; }
public class LuaDistributeLock { @Autowired private RedisService redisService; @Autowired private RedisTemplate redisTemplate; private static String LOCK_PREFIX = "lua_"; private DefaultRedisScript<Boolean> lockScript; public void lockJob() { String lock = LOCK_PREFIX + "LockNxExJob"; boolean luaRet = false; try { luaRet = luaExpress(lock,"value"); //获取锁失败 if (!luaRet) { String value = (String) redisService.genValue(lock); return; } else { //获取锁成功 Thread.sleep(5000); } } catch (Exception e) { } finally { if (luaRet) { redisService.remove(lock); } } }
这里我们再思考一个问题,如果有两个进程来获取锁,a获取锁成功后,他的执行时间,大于锁的超时时间,在锁超时锁被释放后,b拿到锁正在执行,这是a任务执行完了,a会执行释放锁的操作,这是a就释放了b的锁,这种情况很不好。
-
上图!
-
这里我们简单实现一个释放锁的方法,具体是在释放锁的时候判断服务器的ip,如果一致,才会释放。
-
这样就可以用lua实现一个分布式锁,但是我们想为什么redistemplate不自己实现一个这样的api呢,在spring-data-redis1.5版本是这么实现的
-
/** * 重写redisTemplate的set方法 * <p> * 命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。 * <p> * 客户端执行以上的命令: * <p> * 如果服务器返回 OK ,那么这个客户端获得锁。 * 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。 * * @param key 锁的Key * @param value 锁里面的值 * @param seconds 过去时间(秒) * @return */ private String set(final String key, final String value, final long seconds) { 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); } if (!StringUtils.isEmpty(lockKeyLog) && !StringUtils.isEmpty(result)) { logger.info("获取锁{}的时间:{}", lockKeyLog, System.currentTimeMillis()); } return result; } }); }
-
在2.0版本简化了api
-
public Boolean doInRedis(RedisConnection connection) throws DataAccessException { RedisConnection redisConnection = redisTemplate.getConnectionFactory().getConnection(); return redisConnection.set(key.getBytes(), getHostIp().getBytes(), Expiration.seconds(expire), RedisStringCommands.SetOption.ifAbsent()); }