1、分布式锁
锁的种类:
- 单机版同一个JVM虚拟机内,synchronized或者Lock接口
- 分布式不同个JVM虚拟机内,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。主要解决当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问
1.1、谈谈你对分布式锁的认知和理解
- 独占性:OnlyOne,任何时刻只能有且仅有一个线程持有。
- 高可用:若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败。
- 防死锁:杜绝死锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案。
- 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放。
- 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。
分布式锁的实现方式:
- zookeeper,zookeeper·集群cp
- redis,redis单机cp,redis集群ap
多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击)
1.1.1、单机版没有加锁,多个线程请求,出现数据不一致。
@GetMapping(" /buy_goods")
public synchronized String buy_Goods() {
String result = stringRedisTemplate.opsForValue().get("goods:e01");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:801", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + " lt服务器端口:" + serverPort);
return "你已经成功秒杀商品,此时还剩余: " + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "It 服务器端口:" + serverPort;
}
在单机环境下,可以使用synchronized或Lock来实现,来实现数据的一致性。但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建),不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
1.1.2、用分布式锁
nginx微服务架构,高并发环境下,方法加了synchronized或者Lock,还会发生超卖现象,需要用到分布式锁。
@GetMapping(" /buy_goods")
public String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY, value);
if (!flag) {
return "抢锁失败,try again. ";
}
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:801", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + " lt服务器端口:" + serverPort);
stringRedisTemplate.delete(KEY);
return "你已经成功秒杀商品,此时还剩余: " + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "It 服务器端口:" + serverPort;
}
1.1.3、finally必须关闭锁资源
inally必须关闭锁资源,如果出异常的话,可能无法释放锁,必须要在代码层面finally释放锁,加锁解锁,lock/unlock必须同时出现并保证调用
@GetMapping(" /buy_goods")
public synchronized String buy_Goods() {
try {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY, value);
if (!flag) {
return "抢锁失败,try again. ";
}
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:801", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + " lt服务器端口:" + serverPort);
stringRedisTemplate.delete(KEY);
return "你已经成功秒杀商品,此时还剩余: " + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
stringRedisTemplate.delete(KEY);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "It 服务器端口:" + serverPort;
}
1.1.4、加锁、要考虑过期时间,并且过期时间和加锁要保证原子性。
避免服务器宕机之后,锁没有得到释放。
@GetMapping(" /buy_goods")
public synchronized String buy_Goods() {
try {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY, value, 10L, TimeUnit.SECONDS);
if (!flag) {
return "抢锁失败,try again. ";
}
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:801", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + " lt服务器端口:" + serverPort);
stringRedisTemplate.delete(KEY);
return "你已经成功秒杀商品,此时还剩余: " + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
stringRedisTemplate.delete(KEY);
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "It 服务器端口:" + serverPort;
}
1.1.5、张冠李戴,删除了别人的锁。
定义一个锁,锁的超时时间为10s,A线程先进入方法,获取到锁,执行业务逻辑,但是因为网络抖动等问题,造成执行时间超过了10s,这时候锁,过期释放,线程B抢占到锁,执行方法,线程A执行完毕之后,去删除线程B获取到的锁,因为加锁时间的问题,造成了线程A误删了线程B的锁。需要判断当前线程加的锁和删除的锁是不是同一个。
@GetMapping(" /buy_goods")
public synchronized String buy_Goods() {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY, value, 10L, TimeUnit.SECONDS);
if (!flag) {
return "抢锁失败,try again. ";
}
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:801", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + " lt服务器端口:" + serverPort);
stringRedisTemplate.delete(KEY);
return "你已经成功秒杀商品,此时还剩余: " + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//判断加锁与解锁是不是同一个客户端
if (stringRedisTemplate.opsForValue().get(KEY).equals(value)) {
//若在此时,这把锁突然不是这个客户端的,则会误解锁
stringRedisTemplate.delete(KEY);
}
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "It 服务器端口:" + serverPort;
}
1.1.6、删除锁的时候,判断是否是当前线程加的锁,和删除操作要保证原子性
判断是否是当前线程加的锁,和删除操作要保证原子性,使用LUA脚本保证,单个节点实现Redis分布式锁
@GetMapping(" /buy_goods")
public synchronized String buy_Goods() throws Exception {
String value = UUID.randomUUID().toString() + Thread.currentThread().getName();
try {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY, value, 10L, TimeUnit.SECONDS);
if (!flag) {
return "抢锁失败,try again. ";
}
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:801", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + " lt服务器端口:" + serverPort);
stringRedisTemplate.delete(KEY);
return "你已经成功秒杀商品,此时还剩余: " + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
Jedis jedis = RedisUtils.getRedis();
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then " +
"return redis.call('de1', KEYS[1])" +
"else " +
" return 0 " +
"end";
try {
Object result = jedis.eval(script, Collections.singletonList(KEY), Collections.singletonList(value));
if ("1" .equals(result.toString())) {
System.out.println("------del REDIS_LOCK_KEY success");
} else {
System.out.println("------de1 REDIS_LOCK_KEY error");
}
} finally {
if (null != jedis) {
jedis.close();
}
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "It 服务器端口:" + serverPort;
}
}
public class RedisUtils {
private static JedisPool jedisPool;
static {
JedisPoolConfig jedisPoolconfig = new JedisPoolConfig();
jedisPoolconfig.setMaxTotal(20);
jedisPoolconfig.setMaxIdle(10);
jedisPool = new JedisPool(jedisPoolconfig, "192.168.10.100", 6379);
}
public static Jedis getRedis() throws Exception {;
if (null != jedisPool) {
return jedisPool.getResource();
}
throw new Exception("redisPool is not ok");
}
}
1.1.7、redis集群环境下,用Redisson解决分布式锁(官网推荐)
如何确保redisLock过期时间大于业务执行时间的问题。key的续约问题,
redis集群:redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来这条数据给从节点,master就挂了,
public static final String KEY = "lock";
@Resource
private Redisson redisson;
@GetMapping(" /buy_goods")
public synchronized String buy_Goods() throws Exception {
RLock redissonLock = redisson.getLock(KEY);
redissonLock.lock();
try {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0) {
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:801", realNumber + "");
System.out.println("你已经成功秒杀商品,此时还剩余:" + realNumber + "件" + " lt服务器端口:" + serverPort);
stringRedisTemplate.delete(KEY);
return "你已经成功秒杀商品,此时还剩余: " + realNumber + "件" + "\t 服务器端口: " + serverPort;
} else {
System.out.println("商品已经售罄/活动结束/调用超时,欢迎下次光临" + "\t 服务器端口:" + serverPort);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
redissonLock.unlock();
}
}
return "商品已经售罄/活动结束/调用超时,欢迎下次光临" + "It 服务器端口:" + serverPort;
}
@Configuration
@Slf4j
public class RedisConfig {
@Bean
public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Serializable> 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;
}
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.10.100:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
Redis分布式锁-Redlock算法 Distributed locks with Redis
单机案例
分布式锁中点三个要素
- 加锁,加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间
public static boolean tryLock(String key,String uniqueId,int saconds){
return "OK".equals(jedis.set(key,uniqueId,"NX","EX",seconds));
}
- 解锁,将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉,只能自己删除自己的锁,Lua脚本保证原子性
为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。
public static boolean releaseLock(String key,String uniqueld) {
String luaScript = "if redis.call('get' , KEYS[1]) == ARGV[1] then " +
"return redis.call('de1',KEYS[1]) else return 0 end" ;
return jedis.eval(luaScript);
}
- 超时,锁key要注意过期时间,不能长期占用。
1.1.8、基于redis主从集群配置,setnx的分布式锁有什么缺点?
1.1.8.1、redis主从集群配置,出现的问题
- 线程1首先获取锁成功,将键值对写入redis 的 master节点;
- 因为redis集群的数据同步方式是异步的,在redis将该键值对同步到slave 节点之前,master发生了故障;
- redis触发故障转移,其中一个slave升级为新的master;
- 此时新的 master并不包含线程1写入的键值对,因此线程⒉尝试获取锁也可以成功拿到锁;
- 此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据。
1.1.8.2、redis如何实现高可用
解决方式:Redis提供了Redlock算法,用来实现基于多个实例的分布式锁(又叫做多主模式)。锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。Redlock算法是实现高可靠分布式锁的一种有效解决方案,可以在实际开发中使用。
RedLock算法思想,意思是不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,n / 2 + 1,必须在大多数redis节点上都成功创建锁,才能算这个整体的RedLock加锁成功
该方案也是基于(set加锁、Lua脚本解锁)进行改良的,大致方案如下。假设我们有N个Redis主节点,例如N=5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,
为了取到锁客户端,执行以下操作:
- 获取当前时间,以毫秒为单位;
- 依次尝试从5个实例,使用相同的 key 和随机值(例如UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以防止客户端在试图与一个宕机的Redis节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个Redis 实例请求获取锁;
- 客户端通过当前时间减去步骤1记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点〉的Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功;
- 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤3计算的结果)。
- 如果由于某些原因未能获得锁(无法在至少N/2+1个Redis实例获取锁、或获取锁的时间超过了有效时间)﹐客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用master节点,同时由于舍弃了slave,为了保证可用性,引入了N个节点,官方建议是5。本次测试演示用3台实例来做说明。
客户端只有在满足下面的这两个条件时,才能认为是加锁成功。
- 条件1:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;
- 条件2:客户端获取锁的总耗时没有超过锁的有效时间。
为什么是奇数?
容错率: N=2X+1 (N是最终部署机器数,X是容错机器数)
比如。我网络中死了1台机器,我要求还是OK的,可以用,请问,最多多主集群部署几台? N = 2*1 +1 = 3
- 什么是容错?
就是指失败了多少个机器实例后我还是可以容忍的,所谓的容忍就是数据一致性还是正常的,即CP数据一致性还是可以满足。
如果加入在集群环境中,redis失败1台,可接受。2X+1=21+1=3,部署3台,死了1个剩下2个可以正常工作,那就部署3台。
如果加入在集群环境中,redis失败2台,可接受。2X+1=22+1=5,部署5台,死了2个剩下3个可以正常工作,那就部署5台。 - 为什么是奇数?
最少的机器,最多的产出效果
1.1.8.2、redis实现高可用实操
- docker部署3台redis的master机器,本次设置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
docker exec -it redis-master-1 /bin/bash
docker exec -it redis-master-2 /bin/bash
docker exec -it redis-master-3 /bin/bash
- 代码
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
```**加粗样式**
```java
spring.application.name=spring-boot-redisserver.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
@Configuration
@EnableAutoConfiguration(RedisProperties.class)
public class CacheConfiguration {
@Resource
private 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.isEmpty(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
//单机
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.10.100:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
@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;
}
@ConfigurationProperties(prefix = "spring.redis",ignoreInvalidFields = false)
@Data
public class RedisProperties {
private int database;
//等待节点回复命令的时间。该时间从命令发送成功时开始计时*/
private int timeout;
private String password;
private String mode;
//池配置
private RedisPoolProperties pool;
private RedisSingleProperties single;
}
@Data
public class RedisSingleProperties {
private String address1;
private String address2;
private String address3;
}
@RestController
@Slf4j
@Api(description = "分布式锁")
public class RedLockController {
public static final String CACHE_KEY_REDLOCK = "COMMON 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 isLockBoolean;
try {
// waitTime抢锁的等待时间, 正常情况下够秒
//leaseTime就是redis key的过期时间, 正常情况下5分钟300秒。
isLockBoolean = redLock.tryLock(3, 300, TimeUnit.SECONDS);
log.info("线程,是否拿到锁:", Thread.currentThread().getName(), isLockBoolean);
if (isLockBoolean) {
System.out.println(Thread.currentThread().getName() + "It" + "-- --come in in");
// 业务逻辑,忙10分钟
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
log.error("redlock exception ", e);
} finally {
//无论如何,最后都要解锁redLock.unlock();
}
}
}
1.1.9、缓存续命
守护线程续命:额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间,Redisson里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间;
在获取锁成功后,给锁加一个watchdog,watchdog,会起一个定时任务,在锁没有被释放且快要过期的时候会续期
客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始
流程解释
- 通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功
- 通过hexists判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功
- 如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间(代表了lockzzyy这个锁key的乘
余重存时间),加锁失败
加锁成功后,在redis的内存数据中,就有一条hash结构的数据。
Key为锁的名称;field为随机字符串+线程ID;值为1。见下
底层: 看门狗+ 三段LUA脚本,首次新建,同线程可重入,最后根据返回时间判断锁还有多久过期,完了之后,触发unlock解锁和消除看门狗
unlock解锁的LUA脚本:
2、Redis的三大删除策略
2.1、redis内存配置
2.1.1、redis默认内存多少可以用?
如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小。
2.1.2、redis 一般生产上你如何配置?
一般推荐Redis设置内存为最大物理内存的四分之三
- 配置文件修改
- 命令修改
2.1.3、什么命令查看redis内存使用情况?
info memory
2.1.4、 设置了maxmemory的选项,假如redis内存使用达到上限,会报OOM
2.2、3种redis的删除策略
如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被被删除呢?
2.2.1、立刻删除
Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除。所以立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放,但是立即删除对cpu是最不友好的。因为删除操作会占用cpu的时间,如果刚好碰上了cpu很忙的时候,比如正在做交集或排序等计算的时候,就会给cpu造成额外的压力,会产生大量的性能消耗,同时也会影响数据的读取操作。
总结:对CPU不友好,用处理器性能换取存储空间(拿时间换空间)
2.2.2、惰性删除
惰性删除是指在数据到达过期时间,不做处理,等下次访问该数据时,如果未过期,返回数据,发现已过期,删除,返回不存在。
惰性删除策略的缺点是,它对内存是最不友好的,如果一个键已经过期,而这个键又仍然保留在redis中,那么只要这个过期键不被删除,它所占用的内存就不会释放。在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作是一种内存泄漏–无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖于内存的Redis服务器来说,肯定不是一个好的选择。
总结:对memory不友好,用存储空间换取处理器性能(拿空间换时间)
2.2.3、定期删除
定期删除策略是前两种策略的折中:定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响,周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度。
- 特点1:CPU性能占用设置有峰值,检测频度可自定义设置
- 特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
总结:周期性抽查存储空间(随机抽查,重点抽查)
redis默认每、隔100ms检查,是否有过期的key,有过期key则删除。注意: redis不是每隔100ms将所有的key检查一次而是随机抽取进行检查(如果每隔100ms,全部key进行检查,对redis的性能影响较大)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
定期删除策略的难点是确定删除操作执行的时长和频率:
- 如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成立即删除策略,以至于将CPU时间过多地消耗在删除过期键上面。
- 如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除束略一样,出现浪费内存的情况。
因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。
2.3、过期淘汰策略:
有哪些(redis6.0.8版本)
noeviction:不会驱逐任何key
allkeys-lru:对所有key使用LRU算法进行删除
volatile-lru:对所有设置了过期时间的key使用LRU算法进行删除
allkeys-random:对所有key随机删除
volatile-random:对所有设置了过期时间的key随机删除
volatile-ttl:删除马上要过期的key
allkeys-lfu:对所有key使用LFU算法进行删除
volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除
从四个方面LRU、LFU、random、ttl,2个维度所有键中删除,过期键中删除
默认的删除策略是noeviction
LRU means Least Recently Used:最近最少使用
LFU means Least Frequently Used:最少使用频率
allkeys-lru:对所有key使用LRU算法进行删除
如何修改过期淘汰策略?
配置文件修改
命令修改
2.4、LRU算法
2.4.1、概要
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰。
2.4.2、设计思想
所谓缓存,必须要有读+写两个操作,按照命中率的思路考虑,写操作+读操作时间复杂度都需要为O(1)
特性要求
- 必须要有顺序之分,一次 区分最近使用的和很久没有使用的数据排序。
- 写和读操作一次搞定。
- 如果容量(坑位)满了要删除最不长用的数据,每次新访问还要把新的数据插入到队头(按照业务你自己设定左右那一边是队头)
查找快、插入快、删除快,且还需要先后排序,什么样的数据结构可以满足这个问题?
你是否可以在O(1)时间复杂度内完成这两种操作?
如果一次就可以找到,你觉得什么数据结构最合适??
2.4.3、原理
LRU的算法核心是哈希链表,本质就是HashMap+DoubleLinkedList,时间复杂度是o(1),哈希表+双向链表的结合体
2.4.4、两种实现方式
2.4.4.1、案例01:参考LinkedHashMap,依赖JDK
public class LRUCacheDemo<K, V> extends LinkedHashMap<K, V> {
private static final float loadFactor = 0.75F;
private static final boolean accessOrder = true;
private int initialCapacity;
public LRUCacheDemo(int initialCapacity) {
super(initialCapacity, loadFactor, accessOrder);
this.initialCapacity = initialCapacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return super.size() > initialCapacity;
}
public static void main(String[] args) {
LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);
lruCacheDemo.put(1, "a");
lruCacheDemo.put(2, "b");
lruCacheDemo.put(3, "c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(4, "c");
System.out.println(lruCacheDemo.keySet());
}
}
2.4.4.2、案例02:手写LRU
public class LRUCacheDemo1 {
//map负责查找,构建一个虚拟的双向链表,它里面安装的就是一个个Node节点,作为数据载体。
// 构造一个Node节点,作为数据载体
class Node<K, V> {
K key;
V value;
Node<K, V> prev;
Node<K, V> next;
public Node() {
this.prev = this.next = null;
}
public Node(K key, V value) {
this.key = key;
this.value = value;
this.prev = this.next = null;
}
}
//2构建一个虚拟的双向链表,里面安放的就是我们的Node
class DoubleLinkedList<K, V> {
private Node<K, V> head;
private Node<K, V> tail;
//2.1构造方法
public DoubleLinkedList() {
head = new Node<>();
tail = new Node<>();
head.next = tail;
tail.prev = head;
}
//2.2添加到头
public void addHead(Node<K, V> node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
//2.3 删除节点
public void removeNode(Node<K, V> node) {
node.next.prev = node.prev;
node.prev.next = node.next;
node.prev = null;
node.next = null;
}
//2.4获得最后一个节点
public Node getLast() {
return tail.prev;
}
}
private int cacheSize;
Map<Integer, Node<Integer, Integer>> map;
DoubleLinkedList<Integer, Integer> doubleLinkedList;
public LRUCacheDemo1(int cacheSize) {
this.cacheSize = cacheSize;//坑位
map = new HashMap<>();//查找
doubleLinkedList = new DoubleLinkedList<>();
}
public int getNode(int key) {
if (!map.containsKey(key)) {
return -1;
}
Node<Integer, Integer> node = map.get(key);
doubleLinkedList.removeNode(node);
doubleLinkedList.addHead(node);
return node.value;
}
public void put(int key, int value) {
if (map.containsKey(key)) {
Node<Integer, Integer> node = map.get(key);
node.value = value;
map.put(key, node);
doubleLinkedList.removeNode(node);
doubleLinkedList.addHead(node);
} else {
//坑位满了
if (map.size() == cacheSize) {
Node<Integer, Integer> lastNode = doubleLinkedList.getLast();
map.remove(lastNode.key);
doubleLinkedList.removeNode(lastNode);
}
//才是新增
Node<Integer, Integer> newNode = new Node<>(key, value);
map.put(key, newNode);
doubleLinkedList.addHead(newNode);
}
}
public static void main(String[] args) {
LRUCacheDemo1 lruCacheDemo = new LRUCacheDemo1(3);
lruCacheDemo.put(1, 2);
lruCacheDemo.put(2, 2);
lruCacheDemo.put(3, 2);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(4, 4);
System.out.println(lruCacheDemo.map.keySet());
}
}
双向链表添加数据
双向链表删除数据