十二、redis分布式锁:单机和多机及案例
一、单机
1、三个重要元素
(1)加锁
- 加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。
(2)解锁
- 将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉,只能自己删除自己的锁
- Lua脚本
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
(3)超时
- 锁key要注意过期时间,不能长期占用
2、单机模式中,一般都是用 set/setnx+lua 脚本搞定,它的缺点是什么?
常见的 redis 异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,就挂了。
- 线程 1 首先获取锁成功,将键值对写入 redis 的 master 节点;
- 在 redis 将该键值对同步到 slave 节点之前,master 发生了故障;
- redis 触发故障转移,其中一个 slave 升级为新的 master;
- 此时新的 master 并不包含线程 1 写入的键值对,因此线程 2 尝试获取锁也可以成功拿到锁;
- 此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据。
二、多机
1、如何解决多机分布式锁
Redis也提供了Redlock算法,用来实现基于多个实例的分布式锁。锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用。
2、设计理念
该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父Antirez 只描述了差异的地方,大致方案如下:
假设我们有 N 个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,为了取到锁客户端执行以下操作:
1 | 获取当前时间,以毫秒为单位; |
2 | 依次尝试从5个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁; |
3 | 客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功; |
4 | 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。 |
5 | 如果由于某些原因未能获得锁(无法在至少 N/2 + 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。 |
该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。客户端只有在满足下面的这两个条件时,才能认为是加锁成功:
- 条件1:客户端从超过半数(大于等于 N/2+1 )的Redis实例上成功获取到了锁。
- 条件2:客户端获取锁的总耗时没有超过锁的有效时间。
3、解决方案
N = 2X + 1 (N是最终部署机器数,X是容错机器数)
什么是容错?
- 失败了多少个机器实例后,我还是可以容忍的。所谓的容忍就是数据一致性还是可以Ok的,CP数据一致性还是可以满足。
为什么是奇数?
- 最少的机器,最多的产出效果。
三、案例:Redis多主集群
1、docker中部署三台redis
3台 master 各自独立无从属关系。
docker run -p 6381:6379 --name redis-master-1 -d redis:6.0.7 docker run -p 6382:6379 --name redis-master-2 -d redis:6.0.7 docker run -p 6383:6379 --name redis-master-3 -d redis:6.0.7
2、启动redis容器实例
docker exec -it redis-master-1 /bin/bash docker exec -it redis-master-2 /bin/bash docker exec -it redis-master-3 /bin/bash
3、Springboot yml文件
spring.application.name=spring-boot-redis server.port=9090 spring.swagger2.enabled=true spring.redis.database=0 spring.redis.password= spring.redis.timeout=3000 #sentinel/cluster/single spring.redis.mode=single spring.redis.pool.conn-timeout=3000 spring.redis.pool.so-timeout=3000 spring.redis.pool.size=10 spring.redis.single.address1=192.168.111.147:6381 spring.redis.single.address2=192.168.111.147:6382 spring.redis.single.address3=192.168.111.147:6383
4、缓存配置
package com.atguigu.redis.redlock.config; import org.apache.commons.lang3.StringUtils; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @Configuration @EnableConfigurationProperties(RedisProperties.class) public class CacheConfiguration { @Autowired RedisProperties redisProperties; @Bean RedissonClient redissonClient1() { Config config = new Config(); String node = redisProperties.getSingle().getAddress1(); node = node.startsWith("redis://") ? node : "redis://" + node; SingleServerConfig serverConfig = config.useSingleServer() .setAddress(node) .setTimeout(redisProperties.getPool().getConnTimeout()) .setConnectionPoolSize(redisProperties.getPool().getSize()) .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle()); if (StringUtils.isNotBlank(redisProperties.getPassword())) { serverConfig.setPassword(redisProperties.getPassword()); } return Redisson.create(config); } @Bean RedissonClient redissonClient2() { Config config = new Config(); String node = redisProperties.getSingle().getAddress2(); node = node.startsWith("redis://") ? node : "redis://" + node; SingleServerConfig serverConfig = config.useSingleServer() .setAddress(node) .setTimeout(redisProperties.getPool().getConnTimeout()) .setConnectionPoolSize(redisProperties.getPool().getSize()) .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle()); if (StringUtils.isNotBlank(redisProperties.getPassword())) { serverConfig.setPassword(redisProperties.getPassword()); } return Redisson.create(config); } @Bean RedissonClient redissonClient3() { Config config = new Config(); String node = redisProperties.getSingle().getAddress3(); node = node.startsWith("redis://") ? node : "redis://" + node; SingleServerConfig serverConfig = config.useSingleServer() .setAddress(node) .setTimeout(redisProperties.getPool().getConnTimeout()) .setConnectionPoolSize(redisProperties.getPool().getSize()) .setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle()); if (StringUtils.isNotBlank(redisProperties.getPassword())) { serverConfig.setPassword(redisProperties.getPassword()); } return Redisson.create(config); } /** * 单机 * @return */ /*@Bean public Redisson redisson() { Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.111.147:6379").setDatabase(0); return (Redisson) Redisson.create(config); }*/ }
5、redis池配置
@Data public class RedisPoolProperties { private int maxIdle; private int minIdle; private int maxActive; private int maxWait; private int connTimeout; private int soTimeout; /** * 池大小 */ private int size; }
6、redis单机属性配置
必须和yml文件中的对应
@Data public class RedisSingleProperties { private String address1; private String address2; private String address3; }
7、读取配置属性
package com.atguigu.redis.redlock.config; import lombok.Data; import lombok.ToString; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false) @Data public class RedisProperties { private int database; /** * 等待节点回复命令的时间。该时间从命令发送成功时开始计时 */ private int timeout; private String password; private String mode; /** * 池配置 */ private RedisPoolProperties pool; /** * 单机信息配置 */ private RedisSingleProperties single; }
8、业务代码
package com.atguigu.redis.redlock.controller; import lombok.extern.slf4j.Slf4j; import org.redisson.Redisson; import org.redisson.RedissonRedLock; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.TimeUnit; @RestController @Slf4j public class RedLockController { public static final String CACHE_KEY_REDLOCK = "ZZYY_REDLOCK"; @Autowired RedissonClient redissonClient1; @Autowired RedissonClient redissonClient2; @Autowired RedissonClient redissonClient3; @GetMapping(value = "/redlock") public void getlock() { //CACHE_KEY_REDLOCK为redis 分布式锁的key RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK); RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK); RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK); RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3); boolean isLock; try { //waitTime 锁的等待时间处理,正常情况下 等5s //leaseTime就是redis key的过期时间,正常情况下等5分钟。 isLock = redLock.tryLock(5, 300, TimeUnit.SECONDS); log.info("线程{},是否拿到锁:{} ",Thread.currentThread().getName(),isLock); if (isLock) { //TODO if get lock success, do something; //暂停20秒钟线程 try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } } } catch (Exception e) { log.error("redlock exception ",e); } finally { // 无论如何, 最后都要解锁 redLock.unlock(); System.out.println(Thread.currentThread().getName()+"\t"+"redLock.unlock()"); } } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)