NoSQL简介
关系型数据库(MySQL、Oracle、SQL Server)是数据持久化的唯一选择,但随着发展,关系型数据库存在以下问题很难解决,2000次/s 秒杀 抢购等。
NoSQL产品是传统关系型数据库的功能阉割版本,通过减少用不到或很少用的功能,来大幅度提高产品性能(性能为王)
Redis简介
Redis是当前比较热门的NOSQL系统之一,它是一个开源的使用C语言编写的key-value存储系统(区别于MySQL的二维表格的形式存储)。
Redis它会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,实现数据的持久化。
Redis中不存在需要我们担心的表table,在使用Redis进行应用设计和开发时,我们首先应该考虑的是Redis原生支持的哪种数据类型适合我们的应该场景。此外,我们无法像在关系数据库中那样,使用sql来操作Redis中的数据。相反,我们需要直接使用API发送对应的命令,来操作想要操作的数据
redis 安装
- 下载安装包,并上传,一般存放在
/opt
目录。 - 然后解压,
tar -zxvf xxxx.tar.gz
。我们可以在解压产生的目录中看到redis.conf
配置文件。 yum install gcc-c++
安装编译环境,redis
是c++
编写的make
make install
- 默认的安装路径是
/usr/local/bin
,在这个目录下生成了很多以redis-
开头的文件,其中redis-server
就是启动项 - 把第2步中解压目录中的
redis.conf
配置文件复制一份到这里,以后就可以用这里的配置文件启动redis了。 vim redis.conf
更改配置文件,将里面的daemonize no
改成daemonize yes
。意思是开启后台运行。redis-server redis.conf
开启redisredis-cli -p 6379
从6379端口连接redis,可以键入ping
,回车可以看到PONG
,即为连接成功。shutdown
关闭redis。exit
退出
性能测试
在安装目录有一个redis-benchmark
文件,它是用来测试redis性能的。
# 测试 100个并发连接 100000个请求
redis-benchmark -h localhost -p 6379 -c 100 -n 100000
相关知识
单进程单线程
redis
是一个单进程,单线程的的应用程序
redis
采用I/O 多路复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)
多线程处理可能涉及到锁 (加锁是比较慢)
多线程处理会涉及到线程切换而消耗CPU jvm PC寄存器 一次cpu调度线程上下文切换(1500ns)
单进程不存在线程安全问题
- 优点:
- 单线程不需要线程切换开销,
- IO多路复用
- 没有锁的竞争
- 缺点:
- 无法发挥多核CPU性能,不过可以通过在单机开多个
Redis
实例(集群)来完善
- 无法发挥多核CPU性能,不过可以通过在单机开多个
16个数据库
默认16个数据库,类似数组下表从0开始,0-15,初始默认使用0号库
16个库都是同样密码
select index
切换数据库
默认端口
默认端口是6379
常用命令
select index
选择第index个库keys *
获取当前库所有的keydbsize
查看当前数据库的key的数量flushdb
清空当前库flushall
清空全部库del key
删除keyexpire key 10
10秒后key过期,设置key的过期时间单位是秒,放在 session 设置过期时间ttl key
查看还有多少秒过期,-1
表示永不过期,-2
表示已过期move key 1
将当前的数据库key移动到某个数据库,目标库有,则不能移动randomkey
从当前数据库中随机返回type key
获取key的类型exists key
判断是否存在keypexpire key 1000
设置key的过期时间单位是毫秒persist key
删除过期时间
数据类型
Redis支持多种数据结构:
string
(字符串)Map<String, String>
list
(列表)Map<String, List<String>>
hash
(哈希)Map<String, Map<String, String>>
set
(集合)Map<String, Set<String, String>>
zset
(有序集合) 有序的类似set的集合
string 字符串相关命令
存进去的value是一个字符串
语法:
set key value
存放key-vulueget key
获取key的值getset key new_value
设置值,返回旧值,如果key不存在,则返回nil,并设置key - new_valuemset key1 v1 key2 v2
批量设置mget key1 key2
批量获取append key value
key追加value,如果key不存在,就相当于set keyincr key
递增1incrby key 10
递增decr key
递减1decrby key 10
递减strlen
value的长度getrange key 0 -1
获取指定范围的字符串,0 -1
是全部,0 -2
是0到n-1setrange key index value
index的值替换为valuesetnx key value
不存在就插入(set if not exists) 分布式锁setex key 时间s value
设置key-value,和过期时间msetnx key1 value1 key2 value2
批量设置不存在就插入,原子性操作,要么同时成功,要么同时失败incrbyfloat key 0.3
key增减浮点数object encoding key
得到key的类型,- string里面有三种编码:
int
用于能够作用64位有符号整数表示的字符串embstr
用于长度小于或等于44字节,Redis3.x中是39字节,这种类型的编码在内存使用时性能更好raw
用于长度大于44字节的
list 集合相关命令
存进去的value是一个集合数组
语法:lpush key values
l=left r =right
lpush mylist a b c
左插入rpush mylist x y z
右插入lset mylist 2 n
索引2设置值为n,相当于更新操作,如果不存在就报错,存在就更新lrange mylist 0 -1
取出数据集合,0 -1
是取出所有,0 0
取第一个,0 1
取第一个和第二个lpop mylist
弹出集合最后一个元素,弹出之后就没有了rpop mylist
弹出第一个元素,弹出之后就没有了lindex mylist 2
指定索引2的值llen mylist
获取长度lrem mylist count value
删除。count
的值可以是以下几种:count > 0
从表头开始向表尾搜索,移除与 VALUE 相等的元素,数量为 COUNT 。count < 0
从表尾开始向表头搜索,移除与 VALUE 相等的元素,数量为 COUNT 的绝对值。count = 0
移除表中所有与 VALUE 相等的值。
ltrim mylist 0 4
列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。0 1
表示列表的第一个和第二个元素,-1
表示列表的最后一个元素,-2
表示列表的倒数第二个元素,以此类推。ltrim mylist 1 0
删除mylist中所有元素,start 要比 end大,且数值都为正数。linsert mylist before a xxxxx
在元素a之前插入xxxxxlinsert mylist after a xxxxx
在元素a之后插入xxxxx- 命令用于在列表的元素前或者后插入新元素。当指定元素不存在于列表中时,不执行任何操作。当列表不存在时,被视为空列表,不执行任何操作。
- 如果 key 不是列表类型,返回一个错误。
rpoplpush list list2
转移列表的数据,命令用于移除列表的最后一个元素,并将该元素添加到另一个列表并返回这个元素。
set 相关命令
存进去的value是一个Set,无序且唯一
sadd key value1 value2 value3
添加一个或者多个成员smembers key
返回集合中的所有成员sismember myset set1
判断元素是否在集合中scard key
获取当前key下的元素个数srem key value1 value2 value3
移除集合中一个或多个成员spop
随机弹出一个元素,就从集合中删掉了srandmember key count
随机获取集合中指定个数count的元素,原集合里面没有删掉srandmember key
随机获取集合中的一个元素,原集合里面没有删掉smove myset myset2 value
将一个指定的值,移动到另一个集合sdiff key1 key2 ……
集合之间取差集,以第一个集合为准,意思是取第一个集合中,其他集合没有的sinter key1 key2 ……
集合之间取交集sunion key1 key2 ……
集合之间取并集
hash 相关命令
存进去的value是一个hashMap。
hash更适合对象的存储,尤其是用户信息之类的,string更适合字符串存储。
语法:hset key key value
hset myhash name cxx
命令用于为哈希表中的字段赋值。如果哈希表不存在,一个新的哈希表被创建并进行 HSET 操作。如果字段已经存在于哈希表中,旧值将被覆盖。hget myhash name
获取name的值hmset myhash name cxx age 25 note "i am notes"
批量插入 hmset key k1 v1 k2 v2hmget myhash name age note
批量获取 hmget key k1 k2hgetall myhash
获取所有的hlen myhash
长度hexists myhash name
是否存在hdel myhash name
删除hsetnx myhash score 100
设置不存在的,如果存在,不做处理hincrby myhash id 1
递增hkeys myhash
只取keyhvals myhash
只取value
zset 相关命令
存进去的value是一个有序的Set,有序且唯一
zadd myset score value
添加元素value,并且设置scorezadd myset score1 value1 score2 value2
添加多个元素元素,并且设置scorezrange myset 0 -1
返回集合中所有的元素,按照分数从小到大排序zrange myset 0 -1 withscores
返回集合中所有的元素,按照分数从小到大排序,并且把排序码列出来zrevrange myset 0 -1
返回集合中所有的元素,按照分数从大到小排序zrevrange myset 0 -1 withscores
返回集合中所有的元素,按照分数从大到小排序,并且把排序码列出来zrangebyscore myset 10 25 withscores
取指定分数范围的值zcard myset
元素数量zrem myset value1 value2
删除一个或多个元素zincrby zset 1 one
增长分数zscore zset two
获取分数zrangebyscore zset 10 25 withscores
指定范围的值zrangebyscore zset 10 25 withscores limit 1 2
指定范围的值,带上分页zrevrangebyscore zset 10 25 withscores
指定范围的值zcount zset
获得指定分数范围内的元素个数
zset
常用案例:
- 班级成绩表,工资表排序等。
- 普通消息score 1,重要消息score 2,带权重进行判断
- 排行榜应用等
3中特殊数据类型
Geospatial 地理位置
Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作。
案例思路:附近的人等。
geoadd china:city 116.40 39.90 beijing 114.05 22.52 shenzhen
添加beijing和shenzhen的地理坐标geodist china:city beijing shenzhen km
计算beijing到shenzhen的距离,单位km\georadius china:city 116.40 39.90 100 km
根据给定的经纬度坐标来获取指定范围内的地理位置集合。georadiusbymember china:city beijing 100 km
和georadius
一样,用于获取指定范围的位置集合,但是以给定的位置元素为中心geopos china:city beijing shenzhen
用于从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。geohash china:city beijing shenzhen
用于保存地理位置的坐标。
Hyperloglog
基于统计的算法。
优点:占用的内存是固定的,只需要12KB,可以统计2^64次方,绝对够用。
案例:统计网页的UV(一个人访问一个网站多次,但是还是算一个人)
传统的方式:set
保存用户的id,然后就可以统计set中的元素数量作为标准判断。
这个方式如果大量保存用户id,就会比较麻烦。我们的目的是为了计数,而不是保存用户id。
这个时候使用Hyperloglog
就很适合。
pfadd mykey a b c d e f
创建第一组元素mykeypfadd mykey2 e f g k l
创建第二组元素mykey2pfmerge mykey3 mykey mykey2
合并两组mykey mykey2 到mykey3 并集,会自动去重pfcount mykey3
查看并集的数量 ,结果8
Bitmap 位存储
Bitmap 位图,数据结构,都是操作二进制位来进行记录,只有0 和1 两个状态。
统计用户信息,活跃,不活跃!登录,未登录!打卡,未打卡!两个状态的字段,都可以使用Bitmap
setbit signweek 1 0
周一未打卡setbit signweek 2 1
周二已打卡setbit signweek 3 1
周三已打卡setbit signweek 4 0
周四未打卡setbit signweek 5 1
周五已打卡getbit signweek 1
查看周一是否打卡,结果0bitcount signweek
统计打卡的天数,结果3
事务
redis单条命令是保存原子性的,但是redis事务不保证原子性。
redis事务本质:一组命令的集合!一个事务中所有命令都会被序列化,在事物执行过程中,会按照顺序执行!
一次性、顺序性、排他性的执行一系列的命令!
所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行。
multi
开启事务- 命令入队
exec
执行事务
如果开启了事务multi
,但是还没有执行事务exec
,可以取消事务discard
。取消事务之后,队列中的命令都不会执行。
redis事务中的异常分两种,编译型异常和运行时异常。
- 编译型异常(代码有问题,命令有错),事务中所有的命令都不会被执行!
- 运行时异常,如果事务队列中存在语法性错误,那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常。redis事务不保证原子性。
watch 乐观锁
悲观锁:很悲观,认为什么时候都会出问题,无论做什么都会加锁!
乐观锁:很乐观,认为什么时候都不会出问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据。一般使用version,更新的时候比较version。
使用watch
可以当做redis的乐观锁操作!案例:秒杀
watch money # 监视money
multi # 开启事务
decrby money 10
incrby out 10
exec # 执行之前,另外一个线程,修改了money的值,这个时候,会导致事务执行失败
如果事务执行失败,重新获取最新的值,再次重复执行上述事务即可!
unwatch # 如果事务执行失败,就先解锁
watch money # 获取最细的值,再次监视
multi
decrby money 10
incrby out 10
exec # 对比监视的值是否发生了变化,如果没有变化,那么可以执行成功,如果变化了就执行失败
Jedis
常用api,和命令是一样的。
public static void main(String[] args){
// new Jedis 对象
Jedis jedis = new Jedis("127.0.xx.xx", 6379);
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello","redis");
jsonObject.put("nihao","jedis");
// 开启事务
Transaction multi = jedis.multi();
String result = jsonObject.toJSONString();
try{
multi.set("user1", result);
multi.set("user2", result);
int i = 1/0; //代码抛出异常事务,不会执行事务!
multi.exec(); //执行事务
} catch (Exception e) {
multi.discard(); // 放弃事务
e.printStackTrace();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close(); // 关闭连接
}
}
SpringBoot 使用 Redis
在springboot2.x以后,原来使用的Jedis
被替换成了lettuce
。
jedis
:采用直连,多个线程操作的话,是不安全的,如果想要避免不安全,使用jedis pool连接池!更像BIO模式
lettuce
: 采用netty,实例可以在多个线程中共享,不存在线程不安全的情况!可以减少线程数据了,更像NIO模式
所以springboot配置redis连接池的时候选择lettuce
的连接池。
springboot操作redis一般使用redisTemplate
操作,api和指令是一样的。
opsForValue
操作字符串,类似stringopsForList
操作listopsForSet
opsForZSet
opsForHash
opsForGeo
opsForHyperLoglog
除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务,和基本的CRUD。
RedisConnection connetion = redisTemplate.getConnectionFactory.getConnection();
connection.flushDb();
connection.flushAll();
redisTemplate.opsForValue.set("name", "Jim");
redisTemplate.opsForValue.get("name");
自定义 redisTemplate
默认的序列化方式是jdk序列化,我们一般会使用json来序列化,解决乱码问题。
首先实体类需要实现Serializable
接口。
以下代码是从【狂神说】的笔记里抄的。
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
// 这是我(狂神说)给大家写好的一个固定模板,大家在企业中,拿去就可以直接使用!
// 自己定义了一个 RedisTemplate
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 我们为了自己开发方便,一般直接使用 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<String,Object>();
template.setConnectionFactory(factory);
// Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
就可以直接使用了
@SpringBootTest
class SpringbootRedisMysqlApplicationTests2 {
@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;
@Test
public void test() throws JsonProcessingException {
User user = new User(0,"jim","上海",new Date(),1);
redisTemplate.opsForValue().set("user",user);
Object user1 = redisTemplate.opsForValue().get("user");
System.out.println("user1 = " + user1);
}
}
自定义工具类 redisUtil
以下代码是从【狂神说】的笔记里抄的。
package com.powernode.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @program: springboot
* @description: Redis工具类
* @author: 狂神说
* @create:
*/
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
使用
@Autowired
private RedisUtil redisUtil;
@Test
public void test2(){
User user = new User(0,"lilei","深圳",new Date(),1);
redisUtil.set("user2",user);
Object user2 = redisUtil.get("user2");
System.out.println("user2 = " + user2);
}
配置文件redis.conf
默认配置文件在/usr/local/src/redis/redis.conf
如果要启动redis,必须要把这个配置文件当做第一个参数传入,如:./redis-server /path/to/redis.conf
Units 单位
配置大小单位,文件开头定义了一些基本的度量单位
includes
和我们的spring配置文件类似,可以通过includes
包含,redis.conf
可以作为总闸,包含其他
network 网络【常用】
bind
- 默认情况下,如果没有指定
bind
配置指令,则Redis侦听用于连接服务器上所有可用的网络接口。 - 事实上把它注释掉,是不行的,写成
bind 0.0.0.0
才可以连接所有网络。 - 如果只想让它在一个或多个网络接口上监听,那你就绑定一个IP或者多个IP。多个ip空格分隔即可。
- 默认情况下,如果没有指定
prot
端口号,默认6379Tcp-backlog
设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和 = 未完成三次握手队列 + 已经完成三次握手队列。
在高并发环境下你需要一个高backlog值来避免慢客户端连接问题。注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值,所以需要确认增大somaxconn和tcp_max_syn_backlog两个值来达到想要的效果timeout
当客户端闲置多少秒后关闭连接,如果设置为0表示关闭该功能。tcp-keepalive 300
单位是秒,表示将周期性的使用SO_KEEPALIVE检测客户端是否还处于健康状态,避免服务器一直阻塞,官方给出的建议值是300
,单位是秒。意思是300s发送一条消息给你,如果你没回应,这说明你不在了。
general 通用
daemonize no
是否以守护模式(后台运行)启动。默认为no,需要配置为yes
以守护模式(后台运行)启动,这时redis instance
会将进程号pid写入默认文件/var/run/redis_端口号.pid
。supervised no
可以通过upstart和systemd管理Redis守护进程,这个参数是和具体的操作系统相关的。pidfile /var/run/redis_端口号.pid
配置pid文件路径。当redis以守护模式启动时,如果没有配置pidfile,pidfile默认值是/var/run/redis.pid 。loglevel notice
日志级别,默认notice
。可选项有:debug
(记录大量日志信息,适用于开发、测试阶段);verbose
(较多日志信息);notice
(适量日志信息,使用于生产环境);warning
(仅有部分重要、关键信息才会被记录)。
logfile ""
日志文件的位置,当指定为空字符串时,为标准输出,如果redis已守护进程模式运行,那么日志将会输出到 /dev/null(linux的无底洞文件) 。syslog-enabled no
是否把日志记录到系统日志。syslog-ident
设置系统日志的id。如 syslog-ident redisdatabases 16
设置数据库的数目。默认的数据库是DB 0 ,可以在每个连接上使用select key
命令选择一个不同的数据库,key是一个介于0到设置值 - 1
之间的数值。always-show-logo yes
是否一直显示logo
snapshotting 快照
redis是内存数据库,如果没有持久化,那么数据断电就没有了。
我们在启动redis时,会自动创建一个叫dump.rdb
的文件,快照其实就是dump.rdb
的存储。
save
保存数据到磁盘。格式是:save <seconds> <changes>
,含义是在 seconds 秒之内至少有 changes个keys 发生改变则保存一次。如:save 900 1
900秒有一条数据改变就保存save 300 10
300秒有10条数据改变就保存save 60 10000
600秒有10000条数据改变就保存
stop-writes-on-bgsave-error yes
默认情况下,如果 redis 最后一次的后台保存失败,redis 将停止接受写操作,这样以一种强硬的方式让用户知道数据不能正确的持久化到磁盘,否则就会没人注意到灾难的发生。如果后台保存进程重新启动工作了,redis 也将自动的允许写操作。然而你要是安装了靠谱的监控,你可能不希望 redis 这样做,那你就改成 no 好了。rdbcompression yes
是否在dump .rdb数据库的时候压缩字符串(压缩rdb文件),默认设置为yes。如果你想节约一些cpu资源的话,可以把它设置为no,这样的话数据集就可能会比较大。rdbchecksum yes
是否CRC64
校验rdb文件,会有一定的性能损失(大概10%)dbfilename dump.rdb
rdb文件的名字。dir ./
dump.rdb
数据文件保存路径,默认指启动时的目录
security 安全
主要用于访问密码和查看,设置和取消
requirepass yourpassword
我们一般不在这里设置,一般采用命令设置config set requirepass "123456"
。
登录auth 123456
replication 主从复制
clients 客户端限制
maxclients 10000
设置redis同时可以与多少个客户端进行连接。默认情况下为10000个客户端。当你无法设置进程文件句柄限制时,redis会设置为当前的文件句柄限制值减去32,因为redis会为自身内部处理逻辑留一些句柄出来。如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出max number of clients reached
以作回应。
memory management 内存管理【淘汰策略】
LRU : 最近最少使用的算法(对最近的表现做总结,考虑不完善,但是很容易实现)
如果一个数据最近一段时间都没有被访问到,那么认为这个数据在将来访问的可能性也比较小,因此,当空间满时,最久没有访问的数据最先被淘汰
LFU : 使用频率最少的算法(对历史数据做总结,考虑更全面一些,但是会耗费总结历史的时间)
如果一个数据很少被访问到,那么认为这个数据在将来访问的可能性也比较小,因此,当空间满时,最小频率访问的数据最先被淘汰
maxmemory <bytes>
设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy
来指定。- 如果redis无法根据移除规则来移除内存中的数据,或者设置了
不允许移除
,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET
、LPUSH
等。 - 但是对于无内存申请的指令,仍然会正常响应,比如
GET
等。 - 如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是
不移除
的情况下,才不用考虑这个因素
- 如果redis无法根据移除规则来移除内存中的数据,或者设置了
maxmemory-policy
淘汰策略volatile-lru
从设置了过期时间的键集合中通过LRU算法驱逐最近最少使用的键allkeys-lru
从所有键中通过LRU算法驱逐最近最少使用的键volatile-lfu
从配置了过期时间的键中通过LFU算法驱逐使用频率最少的键allkeys-lfu
从所有键中通过LFU算法驱逐使用频率最少的键volatile-random
从设置了过期时间的键集合中随机驱逐allkeys-random
从所有键中随机驱逐volatile-ttl
从设置了过期时间的键中驱逐马上就要过期的键noeviction
当内存使用超过配置的时候,会返回错误,不驱逐任何键
maxmemory-samples
设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key并选择其中LRU的那个。
append only mode AOF持久化
appendonly no
,默认是no,是不开启aof模式的,默认使用rdb方式持久化的,在大部分情况下,rdb完全够用。appendfilename "appendonly.aof"
配置AOF保存的文件。文件的路径怎么获取:在启动redis的状态下键入config get dir
,弹出的第二行就是。默认指启动时的目录appendfsync always
同步持久,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好appendfsync everysec
异步操作,每秒记录。如果一秒内宕机,可能有1s的数据丢失。默认使用appendfsync no
不执行sync,这个时候操作系统自己同步数据,速度最快
Redis 持久化
Redis 提供了不同范围的持久性选项:
- RDB(Redis 数据库):RDB 持久性以指定的时间间隔执行数据集的时间点快照。
- AOF(Append Only File):AOF 持久化记录服务器接收到的每个写操作,在服务器启动时再次播放,重建原始数据集。命令使用与 Redis 协议本身相同的格式以仅附加方式记录。当日志变得太大时,Redis 能够在后台重写日志。
- 无持久性:如果您愿意,您可以完全禁用持久性,如果您希望您的数据只要服务器正在运行就存在。
- RDB + AOF:可以在同一个实例中结合 AOF 和 RDB。请注意,在这种情况下,当 Redis 重新启动时,AOF 文件将用于重建原始数据集,因为它保证是最完整的。
RDB【Redis DataBase】默认推荐
RDB的优点:
- RDB 是 Redis 数据的一个非常紧凑的单文件时间点表示。RDB 文件非常适合备份。例如,您可能希望在最近的 24 小时内每小时归档一次 RDB 文件,并在 30 天内每天保存一个 RDB 快照。这使您可以在发生灾难时轻松恢复不同版本的数据集。
- RDB 非常适合灾难恢复,它是一个可以传输到远程数据中心或 Amazon S3(可能已加密)的压缩文件。
- RDB 最大限度地提高了 Redis 的性能,因为 Redis 父进程为了持久化而需要做的唯一工作就是派生一个将完成所有其余工作的子进程。父进程永远不会执行磁盘 I/O 或类似操作。
- 与 AOF 相比,RDB 允许使用大数据集更快地重启,数据很稳定,文件比较小。
- 在副本上,RDB 支持重启和故障转移后的部分重新同步。
RDB 的缺点:
- 如果您需要在 Redis 停止工作时(例如断电后)将数据丢失的可能性降到最低,那么 RDB 并不好。您可以配置生成 RDB 的不同保存点(例如,在对数据集至少 5 分钟和 100 次写入之后,您可以有多个保存点)。但是,您通常会每五分钟或更长时间创建一个 RDB 快照,因此,如果 Redis 由于任何原因在没有正确关闭的情况下停止工作,您应该准备好丢失最新分钟的数据,即:最后一次持久化后的数据可能丢失。
- RDB 需要经常 fork() 以便使用子进程在磁盘上持久化。如果数据集很大,fork() 可能会很耗时,并且如果数据集很大并且 CPU 性能不是很好,可能会导致 Redis 停止为客户端服务几毫秒甚至一秒钟。AOF 也需要 fork() ,但频率较低,您可以调整要重写日志的频率,而不需要对持久性进行任何权衡。
保存过程:父进程fork
一个子进程,将数据持久化到临时文件中,持久化结束,再替换上次的RDB正式文件。
适用场景:适合大规模数据恢复且数据完整性不敏感的情况。
fork()
的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。
保存位置及配置位置
RDB 保存的是dump.rdb
文件,在配置文件中可配置
dbfilename dump.rdb
rdb文件的名字。dir ./
dump.rdb
数据文件保存路径,默认指启动时的目录
如何触发RDB快照
- save满足:命令
save 900 1
即是在15分钟内修改了1次 即会触发RDB。 - 执行
flushall
命令,也会产生dump.rdb
文件,但里面是空的,无意义。 - 退出Redis,会产生RDB文件
注意:不要在生产环境去手动save拍照
如何恢复数据
只需要将rdb文件放在redis
启动目录就可以,redis启动时会自动检查dump.rdb
恢复其中的文件。
如果rdb文件有损坏,bin
目录下有redis-check-aof
和redis-check-rdb
两个执行文件,是用于修复aof和rdb文件的。
redis-check-rdb --fix dump.rdb
查看需要存放的位置:在启动redis的状态下键入config get dir
,弹出的第二行就是。
RDB 的关闭
redis.conf
配置快照snapshotting
的时候打开save ""
AOF 【append only file】
原理
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据。
换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
AOF 优势
- 使用 AOF Redis 更加持久:您可以有不同的 fsync 策略:根本不 fsync、每秒 fsync、每次查询时 fsync。使用每秒 fsync 的默认策略,写入性能仍然很好(fsync 使用后台线程执行,当没有 fsync 正在进行时,主线程将努力执行写入。)但是您只能丢失一秒钟的写入。
- AOF 日志是仅附加日志,因此不会出现寻道问题,也不会在断电时出现损坏问题。即使由于某种原因(磁盘已满或其他原因)日志以写一半的命令结束,该
redis-check-aof
工具也能够轻松修复它。 - AOF 以易于理解和解析的格式依次包含所有操作的日志。您甚至可以轻松导出 AOF 文件。例如,即使您不小心使用FLUSHALL命令刷新了所有内容,只要在此期间没有执行日志重写,您仍然可以通过停止服务器、删除最新命令并重新启动 Redis 来保存您的数据集再次。
AOF 缺点 - AOF 文件通常比相同数据集的等效 RDB 文件大。
- 根据确切的 fsync 策略,AOF 可能比 RDB 慢。一般来说,将 fsync 设置为每秒的性能仍然非常高,并且在禁用 fsync 的情况下,即使在高负载下它也应该与 RDB 一样快。即使在巨大的写入负载的情况下,RDB 仍然能够提供关于最大延迟的更多保证。
AOF 持久化模式
- 每次修改同步:
appendfsync always
同步持久,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好 - 每秒同步:
appendfsync everysec
异步操作,每秒记录。如果一秒内宕机,有数据丢失。默认使用 - 不同步:
appendfsync no
从不同步
保存位置及配置位置
redis.conf
配置追加(持久化)append only mode
时,可以配置appendonly yes
,默认是no。
配置AOF保存的文件是appendfilename "appendonly.aof"
文件的路径怎么获取:在启动redis的状态下键入config get dir
,弹出的第二行就是。默认指启动时的目录
AOP 启动/修复/恢复
bin
目录下有redis-check-aof
和redis-check-rdb
两个执行文件,是用于修复aof和rdb文件的
redis-check-rdb --fix appendonly.aof
正常恢复
- 启动:设置Yes 修改默认的
appendonly no
,改为yes
- 将有数据的AOF文件复制一份保存到对应目录(
config get dir
) - 恢复:重启redis然后重新加载
异常恢复 - 启动:设置Yes 修改默认的
appendonly no
,改为yes
- 备份被写坏的AOF文件
- 修复:
redis-check-aof --fix appendonly.aof
(修复文件) - 恢复:重启redis然后重新加载
应该使用哪种?
RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储
AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾。Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大
- 只做缓存(RDB)。
- 做用户登录保存session会话(RDB+AOF)
- 极致追求性能(关闭RDB,开启AOF)
- 极致追求安全性(RDB+AOF (appendfsync always))
RDB(拍照)+AOF(日志)就是制定的redis持久化方案
只做缓存
如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。
同时开启两种持久化方式
在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据。因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.
RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。
那要不要只使用AOF呢? 建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。
性能建议
因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。
如果Enalbe AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。默认超过原大小100%大小时重写可以改到适当的数值。
如果不Enable AOF ,仅靠Master-Slave Replication 实现高可用性也可以。能省掉一大笔IO也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个。新浪微博就选用了这种架构
redis 发布订阅
psubscribe chanel1 chanel2 chanel3
订阅一个或多个频道publish chanel message
将信息发送到指定的频道subscribe chanel
订阅某频道unsubscribe chanel
退订某频道
通过subscribe
命令订阅某频道后,redis-server里维护了一个字典,字典的键就是一个个channel,而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。subscribe
命令的关键,就是将客户端添加到给定channel的订阅链表中。
通过publish
命令向订阅者发送消息,redis-server会使用给定的频道作为键,在它所维护的channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。
redis 主从复制
主从复制,是指将一台redis服务器的数据,复制到其他redis服务器。前者称为主节点master,后者称为从节点slave。
数据的复制是单向的,只能有主节点到从节点,master以写为主,slave以读为主。
默认情况下,每台redis服务器都是主节点,且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用主要包括:
- 数据冗余,主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复,主节点出现了问题时,可以由节点提供服务,实现快速的故障恢复,实际上是一种服务的冗余。
- 负载均衡,在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务,分担服务器负载。可以大大提高服务器的并发量。
- 高可用基石,主从复制还是哨兵模式和集群能够实施的基础,也可以说主从复制是redis高可用的基础。
复制的原理
slave启动成功连接到master后会发送一个sync同步命令。
master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,并完成一次完全同步。
全量复制:slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:master继续将新收集到的修改命令依次传给slave,完成同步。
从机如果宕机,只要重新连接master,一次完全同步(全量复制)将自动执行。
实际工作中的主从配置是在配置文件中设置的,这样的话是永久的。如果使用的是命令,那就是暂时的。
比如3台服务器,1主2从,修改各自的配置文件。
- 端口
- pid名字
- log文件名字
- dump.rdb名字
- 如果开启了aof,appendonly.aof名字也得改
在从机的配置文件中,replicaof <masterip> <masterport>
可以设置主机ip、端口号,masterauth <master-password>
设置主机密码。
设置完成后,从机执行slaveof 主机ip 端口号
,主机不用动。
info replication
可以查看当前库的信息。主从各不相同。
测试:主机断开连接,从机依旧连接到主机,但是没有写操作,这个时候,主机如果回来了,从机依旧可以直接获取到主机写的信息。
如果是使用命令行配置的主从,从机这个时候如果重启了,就会变成主机!只要再变成从机,立马就会从主机获取值。
如果主机宕机了,并且没有再次启动,那么两个从机就没有写入操作了,这是不行的。我们可以在某个从机键入slave no one
,把它变成主机,再把另一台从机挂载到新主机身上,slaveof 新主机ip 端口号
。
哨兵模式
主从切换技术的方法是:当主机宕机后,手动把一台从机切换成主机,需要人工干预,费时费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。redis 2.8以后正式提供了sentinel
(哨兵)架构来解决这个问题。
哨兵模式是一种特殊的模式,首先redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待redis服务器响应,从而监控运行的多个redis实例,如果服务器没有响应,可以认为服务器宕机了。
反客为主的自动版,能够在后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。
新建sentinel.conf
文件,名字绝不能错,路径随意,你找得到就行。填写内容sentinel monitor 文件名 主机ip 端口 1
,最后一个数字1,表示主机挂掉后salve投票看让谁接替成为主机,得票数多的成为主机。
启动哨兵,在bin
目录下执行./redis-sentinel ../conf/sentinel.conf
。
测试:主机宕机,从机中会选出一个作为主机,另一个从机自动称为新主机的从机。这时候,就算旧主机回来了,也无法改变。
redis 缓存穿透、雪崩
请求进来,先去内存redis里查找,有则返回,没有则去持久层mysql查找。mysql有则返回,并存入内存redis里,没有则本次查询失败。
-
缓存穿透(查不到)
缓存一直不命中,特别是恶意攻击,发一个没有值当key来查询,redis永远找不到,一直数据库查找
怎么解决:做参数的校验,把 null 或者是空串也存起来 -
缓存击穿(量太大了,缓存过期)
当一个热点key突然过期了,此时有大量请求进来,请求会打到数据库,数据库就炸了
怎么解决: 热点key永不过期。或者加互斥锁。
分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。 -
缓存雪崩
在一段时间内有大量的key都过期了,或者redis宕机,此时有大量请求进来,数据库也炸了
怎么解决: -
redis高可用,就是搭建集群
-
数据预热,在正式部署之前,把可能的数据预先访问一遍,这样大量访问数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,尽量均匀,避免短时内大量过期
-
限流降级,在缓存失效后,通过枷锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。