10.1 Redlock 红锁算法
1.解决手写分布式锁的单点故障问题
- Redis 提供了 Redlock 算法,用来实现基于多个实例的分布式锁
- 锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作
- Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用
2.设计理念
假设我们有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是容错机器数)
1 先知道什么是容错
失败了多少个机器实例后我还是可以容忍的,所谓的容忍就是数据一致性还是可以Ok的,CP数据一致性还是可以满足
加入在集群环境中,redis失败1台,可接受。2X+1 = 2 * 1+1 =3,部署3台,死了1个剩下2个可以正常工作,那就部署3台。
加入在集群环境中,redis失败2台,可接受。2X+1 = 2 * 2+1 =5,部署5台,死了2个剩下3个可以正常工作,那就部署5台。
2 为什么是奇数?
最少的机器,最多的产出效果
加入在集群环境中,redis失败1台,可接受。2N+2= 2 * 1+2 =4,部署4台
加入在集群环境中,redis失败2台,可接受。2N+2 = 2 * 2+2 =6,部署6台
10.2 Redisson进行代码改造
Redisson 就是 Redlock算法 的实现
-
POM
<!--redisson--> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.4</version> </dependency>
-
RedisConfig
import org.redisson.Redisson; import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @author 晓风残月Lx * @date 2023/4/1 10:31 */ @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) { RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(lettuceConnectionFactory); //设置key序列化方式string redisTemplate.setKeySerializer(new StringRedisSerializer()); //设置value的序列化方式json redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } // v 8.0 引入 redisson @Bean public Redisson redisson() { Config config = new Config(); config.useSingleServer() .setAddress("redis://192.168.238.111:6379") .setDatabase(0) .setPassword("123456"); return (Redisson) Redisson.create(config); } }
-
InventoryController
import com.xfcy.service.InventoryService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @author 晓风残月Lx * @date 2023/4/1 10:32 */ @RestController @Api(tags = "redis分布式锁测试") public class InvetoryController { @Autowired private InventoryService inventoryService; @ApiOperation("扣减库存sale,一次卖一个") @GetMapping(value = "/inventory/sale") public String sale() { return inventoryService.sale(); } @ApiOperation("扣减库存saleByRedisson,一次卖一个") @GetMapping(value = "/inventory/saleByRedisson") public String saleByRedisson() { return inventoryService.saleByRedisson(); } }
-
InventoryService
/** * v 9.0 引入Redisson对应的官网推荐RedLock算法实现 * * @return */ @Autowired private Redisson redisson; public String saleByRedisson() { String retMessage = ""; RLock redissonLock = redisson.getLock("xfcyRedisLock"); redissonLock.lock(); try { //1 查询库存信息 String result = stringRedisTemplate.opsForValue().get("inventory001"); //2 判断库存是否足够 Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result); //3 扣减库存 if (inventoryNumber > 0) { stringRedisTemplate.opsForValue().set("inventory001", String.valueOf(--inventoryNumber)); retMessage = "成功卖出一个商品,库存剩余: " + inventoryNumber; } else { retMessage = "商品卖完了,o(╥﹏╥)o"; } System.out.println(retMessage); } finally { // 改进点,只能删除属于自己的 key,不能删除别人的 if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()){ redissonLock.unlock(); } } return retMessage + "\t" + "服务端口号:" + port; }
10.3 多机案例(解决单点故障)
使用 Redisson 的 MultiLock 多重锁
-
使用docker 启动 3 台redis master ,3台master 并无从属关系
docker run -p 6381:6379 --name redis-master-1 -d redis docker run -p 6382:6379 --name redis-master-2 -d redis docker run -p 6383:6379 --name redis-master-3 -d redis
-
进入到redis容器实例
docker exec -it redis-master-1 redis-cli docker exec -it redis-master-2 /bin/bash docker exec -it redis-master-3 /bin/bash
-
建 Module redis_redlock
-
POM
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.10.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.atguigu.redis.redlock</groupId> <artifactId>redis_redlock</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.19.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.14</version> </dependency> <!--swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <!--swagger-ui--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> <scope>compile</scope> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.11</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
-
YML
server.port=9090 spring.application.name=redlock spring.swagger2.enabled=true spring.redis.database=0 #没配置密码 spring.redis.password= spring.redis.timeout=3000 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.238.111:6381 spring.redis.single.address2=192.168.238.111:6382 spring.redis.single.address3=192.168.238.111:6383
-
主启动
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RedisRedlockApplication { public static void main(String[] args) { SpringApplication.run(RedisRedlockApplication.class, args); } }
-
配置类
- CacheConfiguration
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; /** * @author 晓风残月Lx * @date 2023/4/2 17:18 */ @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); }*/ }
-
RedisPoolProperties
import lombok.Data; /** * @author 晓风残月Lx * @date 2023/4/2 17:21 */ @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; }
-
RedisProperties
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; /** * @author 晓风残月Lx * @date 2023/4/2 17:19 */ @ConfigurationProperties(prefix = "spring.redis", ignoreUnknownFields = false) @Data public class RedisProperties { private int database; /* 等待节点回复命令的时间。 该时间从命令发送成功时开始计时 */ private int timeout = 3000; private String password; private String mode; /* 池配置 */ private RedisPoolProperties pool; /* 单机信息配置 */ private RedisSingleProperties single; }
-
RedisSingleProperties
import lombok.Data; /** * @author 晓风残月Lx * @date 2023/4/2 17:18 */ @Data public class RedisSingleProperties { private String address1; private String address2; private String address3; }
-
业务类
-
RedLockController
import cn.hutool.core.util.IdUtil; import lombok.extern.slf4j.Slf4j; import org.redisson.Redisson; import org.redisson.RedissonMultiLock; 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.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author 晓风残月Lx * @date 2023/4/2 17:25 */ @RestController @Slf4j public class RedLockController { public static final String CACHE_KEY_REDLOCK = "ATGUIGU_REDLOCK"; @Autowired RedissonClient redissonClient1; @Autowired RedissonClient redissonClient2; @Autowired RedissonClient redissonClient3; boolean isLockBoolean; @GetMapping(value = "/multiLock") public String getMultiLock() throws InterruptedException { String uuid = IdUtil.simpleUUID(); String uuidValue = uuid+":"+Thread.currentThread().getId(); RLock lock1 = redissonClient1.getLock(CACHE_KEY_REDLOCK); RLock lock2 = redissonClient2.getLock(CACHE_KEY_REDLOCK); RLock lock3 = redissonClient3.getLock(CACHE_KEY_REDLOCK); RedissonMultiLock redLock = new RedissonMultiLock(lock1, lock2, lock3); redLock.lock(); try { System.out.println(uuidValue+"\t"+"---come in biz multiLock"); try { TimeUnit.SECONDS.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(uuidValue+"\t"+"---task is over multiLock"); } catch (Exception e) { e.printStackTrace(); log.error("multiLock exception ",e); } finally { redLock.unlock(); log.info("释放分布式锁成功key:{}", CACHE_KEY_REDLOCK); } return "multiLock task is over "+uuidValue; } }
-
-
测试
- 就是在发送请求时,模拟一台机器挂掉,查看系统是否还能运行
10.4 Redis 的缓存淘汰策略
1.Redis 内存满了怎么办?
-
查看 Redis 最大占用内存
-
redis默认内存多少
- 如果在 64位操作系统, maxmemory 设置0或者不设置最大内存大小表示不限制Redis内存使用
-
一般生产上如何配置
- 推荐Redis设置为最大物理内存的四分之三
-
如何修改redis内存设置
-
通过配置文件修改 (单位是 byte)
-
通过命令修改
-
-
查看redis内存使用情况
- info memory
- config get maxmemory
设置了maxmemory的选项,假如redis 内存使用达到上限,没有加上过期时间就会导致数据写满 maxmemory,这就需要内存淘汰策略
2.Redis 过期键的删除策略
-
立即删除
- 对CPU不友好,用处理器性能换取存储空间(拿时间换空间)
-
惰性删除
-
对memory 不友好,用存储空间换取处理器性能(拿空间换时间)
-
开启惰性删除, lazyfree-lazy-eviction=yes
-
-
定期删除
- 每隔一段时间执行一次删除过期键操作并通过限制删除操作执行时长和频率来减少删除操作对CPU时间的影响。
- 定期抽样key,判断是否过期
- 容易出现漏网之鱼
3.redis 缓存淘汰策略
3.1 LRU 和 LFU
- LRU
- 最近最少使用的页面置换算法,淘汰最长时间未被使用的页面,看页面最后一次被使用到发生调度的时间长短,首先淘汰最长时间未被使用的页面
- LFU
- 最近最不常用页面置换算法,淘汰一定时期内被访问次数最少的页面,看一定时间段内被访问次数最少的页,看一定时间段内页面被使用的频率,淘汰一定时期内被访问次数最少的页
3.2种缓存淘汰策略
- noevication : 不会驱逐任何key,表示即使内存达到上限也不进行置换,所有能引起内存增加的命令都返回 error
- allkeys-lru: 对所有key使用 LRU算法进行删除,优先删除掉最近不经常使用的key,用以保存新数据
- volatie-lru : 对所有设置了过期时间的key使用LRU 算法删除
- allkeys-random :对所有key随机删除
- volatie-random : 对所有设置了过期时间的key随机删除
- volatie-ttl :对所有设置了过期时间的key随即删除
- allkeys-lfu:对所有key使用LFU算法进行删除
- volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除
推荐使用
- 在所有的 key 都是最近经常使用的,那么就需要选择 allkeys-lru 进行置换最近最不经常使用的key,如果不确定使用哪种策略,那么推荐使用 allkeys-lru
- 如果所有的key的访问概率都是差不多的,那么可以选用 allkeys-random 策略去置换数据
- 如果对数据有足够的了解,能够为key指定hint(expire/ttl指定),那么可以选择 volatile-ttl 进行置换(不大推荐,要求过高)
4.性能配置建议
- 避免存储 bigkey
- 开启惰性淘汰 lazyfree-lazy-eviction=yes