java架构之路-(Redis专题)简单聊聊redis分布式锁
这次我们来简单说说分布式锁,我记得过去我也过一篇 JMM 的内存一致性算法,就是说拿到锁的可以继续操作,没拿到的自旋等待。
思路与场景
我们在 Zookeeper 中提到过分布式锁,这里我们先用 redis 实现一个简单的分布式锁,这里是我们一个简单的售卖减库存的小实例,剩余库存假设存在数据库内。
@GetMapping(value = "/getLock") public String getLock() { int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if (stock > 0) { int realStock = stock - 1; stringRedisTemplate.opsForValue().set("stock", realStock + ""); System.out.println("售卖成功, 剩余" + realStock + ""); return "success"; }else{ System.out.println("剩余库存不足"); return "fail"; } }
这样简单的实现了一个售卖的过程,现在看来确实没什么问题的,但是如果是一个并发下的场景就可能会出现超卖的情况了,我们来改造一下代码。
@GetMapping(value = "/getLock") public String getLock() { synchronized (this) { int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if (stock > 0) { int realStock = stock - 1; stringRedisTemplate.opsForValue().set("stock", realStock + ""); System.out.println("售卖成功, 剩余" + realStock + ""); return "success"; } else { System.out.println("剩余库存不足"); return "fail"; } } }
貌似这回就可以了,可以抗住高并发了,但是新的问题又来了,我们如果是分布式的场景下,synchronized 关键字是不起作用的啊。也就是说还是会出现超卖的情况的啊,我们再来改造一下
@GetMapping(value = "/getLock") public String getLock() { String lockKey = "lock";Boolean bool </span>= stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "xiaocai");<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">相当于我们的setnx命令</span> <span style="color: rgba(0, 0, 255, 1)">if</span>(!<span style="color: rgba(0, 0, 0, 1)">bool){ </span><span style="color: rgba(0, 0, 255, 1)">return</span> "error"<span style="color: rgba(0, 0, 0, 1)">; } </span><span style="color: rgba(0, 0, 255, 1)">int</span> stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"<span style="color: rgba(0, 0, 0, 1)">)); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (stock > 0<span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">int</span> realStock = stock - 1<span style="color: rgba(0, 0, 0, 1)">; stringRedisTemplate.opsForValue().set(</span>"stock", realStock + ""<span style="color: rgba(0, 0, 0, 1)">); System.out.println(</span>"售卖成功,剩余" + realStock + ""<span style="color: rgba(0, 0, 0, 1)">); stringRedisTemplate.delete(lockKey); </span><span style="color: rgba(0, 0, 255, 1)">return</span> "success"<span style="color: rgba(0, 0, 0, 1)">; } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> { System.out.println(</span>"剩余库存不足"<span style="color: rgba(0, 0, 0, 1)">); stringRedisTemplate.delete(lockKey); </span><span style="color: rgba(0, 0, 255, 1)">return</span> "fail"<span style="color: rgba(0, 0, 0, 1)">; }
}
这次我们看来基本可以了,使用我们的 setnx 命令来做一次唯一的限制,万一报错了呢?解锁怎么办?再来改造一下。
@GetMapping(value = "/getLock") public String getLock() { String lockKey = "lock"; Boolean bool = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "xiaocai", 10, TimeUnit.SECONDS);//相当于我们的 setnx 命令 try { if (!bool) { return "error"; }</span><span style="color: rgba(0, 0, 255, 1)">int</span> stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"<span style="color: rgba(0, 0, 0, 1)">)); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (stock > 0<span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">int</span> realStock = stock - 1<span style="color: rgba(0, 0, 0, 1)">; stringRedisTemplate.opsForValue().set(</span>"stock", realStock + ""<span style="color: rgba(0, 0, 0, 1)">); System.out.println(</span>"售卖成功,剩余" + realStock + ""<span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 0, 255, 1)">return</span> "success"<span style="color: rgba(0, 0, 0, 1)">; } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> { System.out.println(</span>"剩余库存不足"<span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 0, 255, 1)">return</span> "fail"<span style="color: rgba(0, 0, 0, 1)">; } } </span><span style="color: rgba(0, 0, 255, 1)">finally</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (bool) { stringRedisTemplate.delete(lockKey); } }
}
这次貌似真的可以了,可以加锁,最后在 finally 解锁,如果解锁还是不成功,我们还设置了我们的超时时间,貌似完美了,我们再来提出一个场景。
就是什么意思呢?我们的线程来争抢锁,拿到锁的线程开始执行,但是我们并不知道何时执行完成,我们只是设定了 10 秒自动释放掉锁,如果说我们的线程 10 秒还没有结束,其它线程会拿到锁资源,开始执行代码,但是过了一段时间(蓝色线程还未执行完成),这时我们的绿色线程执行完毕了,开始释放锁资源,他释放的其实已经不是他自己的锁了,他自己的锁超时了,自动释放了,实则绿色线程释放的蓝色的资源,这也就造成了释放其它的锁,其它的线程又会重复的拿到锁,重复执行该操作。明显有点乱了,这不合理,我们来改善一下。
@GetMapping(value = "/getLock") public String getLock() { String lockKey = "lock"; String lockValue = UUID.randomUUID().toString(); Boolean bool = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);//相当于我们的 setnx 命令 try { if (!bool) { return "error"; }</span><span style="color: rgba(0, 0, 255, 1)">int</span> stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"<span style="color: rgba(0, 0, 0, 1)">)); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (stock > 0<span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">int</span> realStock = stock - 1<span style="color: rgba(0, 0, 0, 1)">; stringRedisTemplate.opsForValue().set(</span>"stock", realStock + ""<span style="color: rgba(0, 0, 0, 1)">); System.out.println(</span>"售卖成功,剩余" + realStock + ""<span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 0, 255, 1)">return</span> "success"<span style="color: rgba(0, 0, 0, 1)">; } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> { System.out.println(</span>"剩余库存不足"<span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 0, 255, 1)">return</span> "fail"<span style="color: rgba(0, 0, 0, 1)">; } } </span><span style="color: rgba(0, 0, 255, 1)">finally</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (lockValue.equals(stringRedisTemplate.opsForValue().get(lockKey))) { stringRedisTemplate.delete(lockKey); } }
}
这次再来看一下流程,我们设置一个 UUID,设置为锁的值,也就是说,每次上锁的 UUID 都是不一致的,我们的线程 A 的锁这次只能由我们的线程 A 来释放掉,不会造成释放其它锁的问题了,还是上次的图,我们回过头来看一下,10 秒?真的合理吗?万一 10 秒还没有执行完成呢?有的人还会问,那设置 100 秒?万一执行到 delete 操作的时候,服务宕机了呢?是不是还要等待 100 秒才可以释放锁。别说那只是万一,我们的代码希望达到我们能力范围之内的最严谨。这次来说一下我们本节的其中一个重点,Lua 脚本,后面会去说,我们来先用我们这次博文的 Redisson 吧
Redisson
刚才我们提到了我们锁的时间设置,多长才是合理的,100 秒?可能宕机,造成等待 100 秒自动释放,1 秒?线程可能执行不完,我们可不可以这样来做呢?我们设置一个 30 秒,或者说设置 10 秒,然后我们给予一个固定时间来检查我们的主线程是否执行完成,执行完成再释放我们的锁,思路有了,但是代码实现起来并不简单,别着急,我们已经有了现成的包供我们使用的,就是我们的 Redisson,首先我们来引入我们的依赖,修改一下 pom 文件。
<!-- https://mvnrepository.com/artifact/org.redisson/redisson --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.11.4</version> </dependency>
然后通过 @Bean 的方式注入容器,三种方式我都写在上面了。
@Bean public Redisson redisson(){ Config config = new Config(); //主从 (单机) config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0); //哨兵 // config.useSentinelServers().setMasterName("mymaster"); // config.useSentinelServers().addSentinelAddress("redis://192.168.1.1:26379"); // config.useSentinelServers().addSentinelAddress("redis://192.168.1.2:26379"); // config.useSentinelServers().addSentinelAddress("redis://192.168.1..3:26379"); // config.useSentinelServers().setDatabase(0); // //集群 // config.useClusterServers() // .addNodeAddress("redis://192.168.0.1:8001") // .addNodeAddress("redis://192.168.0.2:8002") // .addNodeAddress("redis://192.168.0.3:8003") // .addNodeAddress("redis://192.168.0.4:8004") // .addNodeAddress("redis://192.168.0.5:8005") // .addNodeAddress("redis://192.168.0.6:8006"); // config.useSentinelServers().setPassword("xiaocai");//密码设置 return (Redisson) Redisson.create(config); }
如果我们的是 springboot 也可以通过配置来实现的。
application.properties
## 因为 springboot-data-redis 是用到了 jedis,所已这里得配置 spring.redis.database=10 spring.redis.pool.max-idle=8 spring.redis.pool.min-idle=0 spring.redis.pool.max-active=8 spring.redis.pool.max-wait=-1 ## jedis 哨兵配置 spring.redis.sentinel.master=mymaster spring.redis.sentinel.nodes=192.168.1.241:26379,192.168.1.241:36379,192.168.1.241:46379 spring.redis.password=admin ## 关键地方 redisson spring.redis.redisson.config=classpath:redisson.json
redisson.json
## redisson.json 文件 { "sentinelServersConfig":{ "sentinelAddresses": ["redis://192.168.1.241:26379","redis://192.168.1.241:36379","redis://192.168.1.241:46379"], "masterName": "mymaster", "database": 0, "password":"admin" } }
这样我们就建立了我们的 Redisson 的连接了,我们来看一下如何使用吧。
package com.redisclient.cluster;import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class RedisCluster {@Autowired </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> StringRedisTemplate stringRedisTemplate; @Autowired </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Redisson redisson; @GetMapping(value </span>= "/getLock"<span style="color: rgba(0, 0, 0, 1)">) </span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String getLock() { String lockKey </span>= "lock"<span style="color: rgba(0, 0, 0, 1)">; RLock redissonLock </span>=<span style="color: rgba(0, 0, 0, 1)"> redisson.getLock(lockKey); </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> { redissonLock.lock(); </span><span style="color: rgba(0, 0, 255, 1)">int</span> stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"<span style="color: rgba(0, 0, 0, 1)">)); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (stock > 0<span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">int</span> realStock = stock - 1<span style="color: rgba(0, 0, 0, 1)">; stringRedisTemplate.opsForValue().set(</span>"stock", realStock + ""<span style="color: rgba(0, 0, 0, 1)">); System.out.println(</span>"售卖成功,剩余" + realStock + ""<span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 0, 255, 1)">return</span> "success"<span style="color: rgba(0, 0, 0, 1)">; } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> { System.out.println(</span>"剩余库存不足"<span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 0, 255, 1)">return</span> "fail"<span style="color: rgba(0, 0, 0, 1)">; } } </span><span style="color: rgba(0, 0, 255, 1)">finally</span><span style="color: rgba(0, 0, 0, 1)"> { redissonLock.unlock(); } }
}
使用也是超级简单的,Redisson 还有重入锁功能等等,有兴趣的可以去 Redisson 查看,地址:https://redisson.org/ 国外的地址打开可能会慢一些。Redis 的分布式锁使用就差不多说到这里了,我们来回到我们刚才说到的 Lua 脚本这里。
Lua 脚本和管道
Lua 脚本
lua 脚本就是一个事务控制的过程,我们可以在 lua 脚本中写一些列的命令,一次性的塞入到我们的 redis 客户端,保证了原子性,要么都成功,要么都失败。好处在于减少与 reidis 的多次连接,可以替代 redis 的事务操作以及保证我们的原子性。
String luaString = "";//Lua 脚本 jedis.eval(luaString, Arrays.asList("keysList"),Arrays.asList("valueList"));
脚本我就不写了(我也不熟悉),我来解释一下 eval 的三个参数,第一个是我们的写好的脚本,然后我们的脚本可能传参数的,也就是我们 KEYS[1] 或者是 ARGV[4],意思就是我们的 KEYS[1] 就是我们的 ArrayList("keysList") 中的第一项,ARGV[4] 就是我们的 ArrayList("valueList") 的第四项。
管道
管道和我们的和我们的 Lua 脚本差不多,不一样就是管道不会保证我们的事务,也就是说我们现在塞给管道 10 条命令 ,我们执行到第三条时报错了,后面的依然会执行,前面执行过的两条还是生效的。虽然可以减少我们的网络开销,也别一次塞太多命令进去,毕竟 redis 的是单线程的,不建议使用管道来操作 redis,想深入了解的可以参照https://www.runoob.com/redis/redis-pipelining.html
redis 的分布式锁差不多就说这么多了,关键是实现思路,使用 Redisson 倒是很简单的,还有我们的 Lua 脚本和管道,Lua 脚本可以保证事务,管道一次性可以执行多条命令,减少网络开销,但不建议使用,下次我们来说下,大厂用 redis 的一些使用注意事项和优化吧。
最进弄了一个公众号,小菜技术,欢迎大家的加入