回顾一下最近使用到的Redis知识点,并不齐全。Redis与传统的数据库不同的是它的数据是存放在内存中的,而且还是单线程的,所以读写速度非常快。Redis还支持丰富的数据类型以及支持数据的持久化。
常用命令:
set key value #设置 key-value 类型的值
get key # 根据 key 获得对应的 value
mset k1 v1 k2 v2 k3 v3 #同时设置多个值
mget k1 k2 k3 #同时获取多个值
incr num #类似于java的自增操作,num+1
incrby num increment #递增increment
decr num #递减num
decrby num decrement #递减decrement
exists key # 判断某个 key 是否存在
strlen key # 返回 key 所储存的字符串值的长度。
del key # 删除某个 key 对应的值
expire key 60 # 数据在 60s 后过期
setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
ttl key # 查看数据还有多久过期
set key value [EX seconds] [PX milliseconds] [NX|XX]
#EX 在多少秒后过期
#PX 在多少毫秒后过期
#NX 当key不存在时才创建
#XX 当key存在时,覆盖key
使用场景:用在一些计数的场景,比如点赞、文章的点击量。
常用命令
hmset key field v1 field2 v2
hexists key field # 查看 key 对应的 field 中指定的字段是否存在。
hget key field # 获取存储在哈希表中指定字段的值。
hgetall key # 获取在哈希表中指定 key 的所有字段和值
hkeys key # 获取 key 的所有field
hvals key # 获取key下面field的所有value
常用命令
rpush myList value1 # 向 list 的右边添加元素
rpush myList value2 value3 # 向list的最右边添加多个元素
lpop myList # 将 list的最左边元素取出
lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end
lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第一
llen myList #查询长度
常用命令
sadd mySet value1 value2 value3 # 添加元素进去,不允许有重复元素
srem value3 #删除元素
smembers mySet # 查看 set 中所有的元素
scard mySet # 查看 set 的长度
srandmember mySet [数字] #从set中随机弹出一个元素,元素不删除
spop mySet [数字] #从set中随机弹出一个元素,元素删除
sismember mySet value1 # 检查某个元素是否存在set 中,只能接收单个元素
sadd mySet2 value2 value3
sdiff mySet1 mySet2 #差集运算
sinter mySet1 mySet2 #交集运算
sunion mySet1 mySet2 #并集运算
使用场景:
常用命令:
zadd myZset 3.0 value1 # 添加元素到 zset 中 3.0 为权重
zadd myZset 2.0 value2 1.0 value3 # 一次添加多个元素
zcard myZset # 查看 zset 中的元素数量
zscore myZset value1 # 查看某个 value 的权重
zrange myZset 0 -1 # 顺序输出某个范围区间的元素,0 -1 表示输出所有元素
zrange myZset 0 1 # 顺序输出某个范围区间的元素,0 为 start 1 为 stop
zrevrange myZset 0 1 # 逆序输出某个范围区间的元素,0 为 start 1 为 stop
使用场景:
首先我们有一个系统的首页需要展示大量不经常修改的数据,首页是一个系统访问量较大的页面,如果我们每次访问都需要去mysql数据库查询数据回显,那肯定会比较慢,而且还会给数据库添加压力,这时候我们就可以使用缓存,流程如下图。如果是单机系统,那我们可以使用Map把我们的数据存在本地即可,但如果是分布式系统,使用Map的话就会造成数据不一致。所以我们一般使用中间件来作为缓存,这时我们就可以使用我们的Redis了。
以下是我自己项目中的例子:
public Map<String, List<Catelog2Vo>> getCatalogJson() {
//1、加入缓存逻辑,缓存中存的数据是json字符串。
//JSON跨语言,跨平台兼容。
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
//2、缓存中没有,查询数据库
System.out.println("缓存不命中....调用方法查询数据库并存入缓存...");
Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
return catalogJsonFromDb;
}
System.out.println("缓存命中....直接返回....");
//转为我们指定的对象。
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
以上就是最简单的使用逻辑,但是使用缓存还会有很多问题,我们先介绍一下使用缓存会出现的问题。
介绍:
指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
解决:
null结果缓存,并加入短暂过期时间。
介绍:
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决:
原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
介绍:
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。
解决:
加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db。
了解了上述三个问题,我们想要解决前两个问题比较简单,就是每次缓存时即使是空数据也缓存,但是过期时间要设置短一些,然后再加一个随机时间。难点是第三个加锁,这我们需要看下一个知识点。
说到加锁,我们最先想到的就是synchronized,我们就先试试使用synchronized。
代码如下(省略了部分代码):
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
synchronized (this){
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.isEmpty(catalogJSON)) {
//缓存不为null直接返回
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
System.out.println("查询数据库并返回数据");
。。。。。。
return result;
}
}
通过本地锁我们只可以锁住当前线程,如果想要锁住全部,那还是得用分布式锁。
阶段一
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1、占分布式锁。去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
if (lock) {
System.out.println("获取分布式锁成功...");
Map<String, List<Catelog2Vo>> = getDataFromDb();
redisTemplate.delete("lock");//删除锁
return dataFromDb;
} else {
//加锁失败...重试
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
阶段二
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1、占分布式锁。去redis占坑
//2、设置过期时间,必须和加锁是同步的,原子的
//3、给锁设置一个标记
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
Map<String, List<Catelog2Vo>> = getDataFromDb();
String lockValue = redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockValue)){
//删除我自己的锁
redisTemplate.delete("lock");//删除锁
}
return dataFromDb;
} else {
//加锁失败...重试
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
阶段三
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1、占分布式锁。去redis占坑
//2、设置过期时间,必须和加锁是同步的,原子的
//3、给锁设置一个标记
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
//加锁成功... 执行业务
Map<String, List<Catelog2Vo>> dataFromDb;
try {
dataFromDb = getDataFromDb();
} finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//4、删除锁,获取值对比+对比成功删除=>原子操作 lua脚本解锁
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
, Arrays.asList("lock"), uuid);
}
return dataFromDb;
} else {
//加锁失败...重试
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
redis官方并不推荐我们自己去实现分布式锁,而是向我们推荐使用Redlock,而Java对应的实现就是Redisson,官方网址:https://github.com/redisson/redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient对象
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson(@Value("${spring.redis.host}") String url) throws IOException {
//1、创建配置
//Redis url should start with redis:// or rediss://
Config config = new Config();
config.useSingleServer().setAddress("redis://"+url+":6379").setTimeout(1000).setPingConnectionInterval(1000).setPassword("密码");
//2、根据Config创建出RedissonClient示例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
@Controller
public class Lock {
@Autowired
RedissonClient redisson;
@ResponseBody
@GetMapping("lock")
public String lock(){
RLock lock = redisson.getLock("lock");
lock.lock();
try {
System.out.println("加锁成功");
}finally {
lock.unlock();
}
return "hello";
}
}
我们可以看到Redisson的使用与我们本地的ReentrantLock是一样的。然后它还会给我们的锁自动续期,如果业务运行时间超过锁的时间,它会自动给锁续上新的时间。然后业务运行完成后,锁会自动删除。其他锁的操作大家可以看官网:https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers
然后我们就可以使用Redisson来实现我们上面的业务代码了,就会变得非常简单。
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
RLock lock = redisson.getLock("CatalogJson-lock");
lock.lock();
Map<String, List<Catelog2Vo>> dataFromDb;
try {
dataFromDb = getDataFromDb();
} finally {
lock.unlock();
}
return dataFromDb;
}
上面,我们通过存储空值、加随机的过期时间和加锁解决了缓存穿透,缓存雪崩和缓存击穿等问题,那现在还有最后一个问题就是如何保证缓存的一致性。
当我们写修改数据时,同时修改我们的缓存。但是可能会存在下图的问题:
当我们修改数据时,同时删除对应的缓存。
这个方案也存在问题:当2号修改数据时,3号去读缓存,发现没有缓存,去读数据库,然后准备更新,此时2号修改完毕,删除缓存,但是3号已经读取到了旧数据,此时更新缓存,那还是会造成短期的数据不一致的问题。
解决:
1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新。
2、读写数据的时候,加上分布式的读写锁。
以上两种方案都还是存在短期的数据不一致问题,即多个线程同时更新会有问题。但是我们使用缓存存放数据本就不应该是实时性、一致性要求高的数据,所以只要保存最终一致性即可。
但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。
怎么解决这个问题呢?答案就是:Redis 内存淘汰机制。
代码如下(示例):
data = pd.read_csv(
'https://labfile.oss.aliyuncs.com/courses/1283/adult.data.csv')
print(data.head())
该处使用的url网络请求的数据。
Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:
appendonly yes
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存 server.aof_buf 中,然后再根据 appendfsync 配置来决定何时将其同步到硬盘中的 AOF 文件。
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显式地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步