Redis - Redis分布式锁
Redis分布式锁
一丶什么是分布式锁
普通的锁,用于同一进程内不同线程在操作同一资源时,为解决冲突而加上的,使得多线程在操作统一资源时以单线程顺序执行.
主内存保存变量值, 每个线程内也有自己的内存, 一般情况下, 线程会在本内存中操作数据后,在刷入主内存, 如果多个线程都同时在各自内存操作数据后, 在刷入主内存, 可能会导致结果不正确. 如 主内存中变量a=1, 线程t1和t2, 同时读取变量a后加1, 最后刷进主内存, 则主内存a可能为2, 正确的结果为3. 如果对操作变量a的方法加上同步锁, t1先对a加1, 刷进主内存后a=2, 然后t2才对a加1, 使得主内存的结果为3.
由于jvm的内存模型, 主内存与各线程本地内存的变量会存在差异, 各线程本地内存是相互独立的, 变量也就存在不可见性. 多线程并发运行也就容易出问题
在分布式系统中, 在不同服务器部署不同的应用服务, 这时就存在多进程. 与多线程的问题类似, 当多进程操作同一资源时, 就需要加同步锁了. 但是普通的同步锁, 只能在同一进程中对多线程有效. 为解决这种问题, 就需要使用分布式锁了.
二丶redis分布式锁
redis分布式锁是基于 set key value px milliseconds nx 命令来实现的.
其中value为随机值,一般用uuid实现. 为什么需要设置过期时间? 是为了防止拿到锁的服务, 崩溃了, 而不能正常释放锁, 导致死锁. 需要由redis服务过期删除锁.
实现一般分为两步
1. 使用lua脚本获取锁 (保证了原子性) 等同于set key value px milliseconds nx (不存在key则设置)
local lockClientId = redis.call('GET', KEYS[1]) if lockClientId == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return true elseif not lockClientId then redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) return true end return false
2. 使用lua脚本释放锁
如果value为指定随机值, 说明是该客户端获取到的锁, 则可以删除该key , 否则则直接返回
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
三丶redis分布式锁使用场景
在redis单机模式下,服务a获取到锁之后, redis就挂掉了,重启后,锁消失掉了,服务b就可以获取到锁,服务a和b就可以同时执行了,z这显然和设计初衷相违背.
单机模式下, 存在单点故障, 于是使用redis集群模式, 主从同步是异步进行的, 存在延迟. 极端情况下, 服务a在主节点获取到锁后, 此时锁并未同步到从节点, 主节点就挂掉了, 在哨兵模式下, 从节点升级为主节点, 服务b可以获取到锁, 这时, 服务a和b又同时执行了.
本质上, 分布式锁要求在同一时刻上, 只能有一个服务拿到锁, 满足CAP模型中的CP模型, 属于强一致性锁. redis集群是为高可用出现的,属于AP模型, 所以redis分布式锁是AP模型. 但是, 为什么还会有人使用redis分布式锁呢? 脱离业务的架构都是耍流氓. 在最终一致性的业务上, 可以使用AP模型锁, 如发送消息, 重复发一条数据, 没有太大关系. 在强一致性的业务上, 需要使用CP模型锁, 如金融业务.
贴出大佬分析的结论,此外redis还提供了另一种强一致性锁-红锁Redlock, 有兴趣可以自行研究以下
四丶Spting提供的对redis分布式锁的支持
1. 引入依赖包
<dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-redis</artifactId> </dependency>
2. 配置RedisLockRegistry
@Bean("defaultRedisLockRegistry") public RedisLockRegistry defaultRedisLockRegistry(RedisConnectionFactory connectionFactory){ //默认1分钟超时 RedisLockRegistry lockRegistry=new RedisLockRegistry(connectionFactory, RedisKeyConfig.LOCK_DEFAULT_REGISTRY_KEY); return lockRegistry; }
3. 简单使用与测试
@SpringBootTest public class LockTests { @Autowired RedisLockRegistry defaultRedisLockRegistry; private final String lockKey ="test:lock:key"; @Autowired RedisManager<String,String> stringRedisManager; @Test public void shouldLock(){ //1分钟超时 Lock lock=defaultRedisLockRegistry.obtain(lockKey); boolean locked=lock.tryLock(); if(!locked){ return; } //获取到锁 String lockRedisKey=RedisKeyConfig.LOCK_DEFAULT_REGISTRY_KEY+":"+lockKey; try { //一般在try{}中写业务逻辑, 报错时, finally中可以释放锁 String clientId=stringRedisManager.get(lockRedisKey); Assert.state(clientId!=null && clientId.length()>0, "没有获取到redis锁"); System.out.println("获取到redis锁(clientId:"+clientId+")"); }catch (Exception e){ throw new RuntimeException(e); }finally { //释放锁, 有可能之前锁已超时,被清除, 然后报错 lock.unlock(); } String clientId=stringRedisManager.get(lockRedisKey); Assert.state(clientId==null, "锁没有被正常释放"); } }
学习资料: