1. 什么是 Redis?
Redis是一个开源的使用C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
Redis 的8种特性
- 在内存运行,效率极高,读写接近每秒十万次
- 基于key-value的数据存储结构
- 键值过期实现缓存,发布订阅实现消息,支持Lua脚本,事务功能
- 单线程,使处理和开发变得简单
- 多语言支持
- RDB和AOF两种持久化策略
- 主从复制,复制多个Redis副本
- Redis Cluster实现分布式集群
应用场景
2. 数据类型
Redis 的5种基本数据类型
- String:字符串,可以对 String 进行自增自减运算,从而实现计数器功能
- Hash:散列
- List:列表,List 是一个双向链表,可以实现MQ,不过最好使用 Kafka、RabbitMQ 等消息中间件
- Set:集合,Set 可以实现交集、并集等操作,从而实现共同好友等功能
- Zset(Sorted Set):有序集合,ZSet 可以实现有序性操作,从而实现排行榜等功能
Redis 的3种特殊数据类型
- Geo:将用户给定的地理位置信息储存起来, 并对这些信息进行操作
- Hyperloglog:非常省内存的去统计各种基数,如每日访问 IP 数、在线用户数
- Bitmaps:通过一个 bit 来(即用0或1)表示某个元素对应的值或者状态
3. Docker 配置 Redis
- 获取 redis.conf 文件并放入创建好的conf映射目录
mkdir -p /home/app/redis/conf
mkdir -p /home/app/redis/data
cd /home/app/conf
wget http://download.redis.io/redis-stable/redis.conf
- redis容器启动时需要从外部目录把 redis.conf 文件和 data数据目录 映射进去
docker run -d --name myredis --privileged=true -p 6379:6379 -v /home/app/redis/conf/redis.conf:/etc/redis/redis.conf -v /home/app/redis/data:/data redis:latest redis-server /etc/redis/redis.conf --appendonly yes --requirepass "root"
启动参数说明
-d -> 以守护进程的方式启动容器
--name myredis -> 指定容器名称
-p 6379:6379 -> 绑定宿主机端口
-v /home/app/redis/conf/redis.conf:/etc/redis/redis.conf -> 映射配置文件
-v /home/app/redis/data:/data -> 映射数据目录
--appendonly yes -> 开启AOF数据持久化
--requirepass -> 设置密码
--privileged=true -> 容器获取真正的root权限
4. 持久化
RDB(默认方式)
RDB持久化对应在redis.conf配置文件中的 SNAPSHOTTING 快照设置项中
################################ SNAPSHOTTING ################################
#
# Save the DB on disk:
#
# save <seconds> <changes>
#
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
#
# Note: you can disable saving completely by commenting out all "save" lines.
#
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save "" 禁用快照
save 900 1 #900秒内至少有1次写入就保存快照
save 300 10 #300秒内至少有10次写入就保存快照
save 60 10000 #60秒内至少10000次写入就保存快照
stop-writes-on-bgsave-error yes #快照保存失败是否继续执行写命令
rdbcompression yes #是否对快照文件进行压缩
rdbchecksum yes #是否校验数据
dbfilename dump.rdb #快照文件的存储名称
rdb-del-sync-files no #是否删除同步锁
dir ./ #快照文件的存储目录
Redis默认按照配置文件中的设置进行拍摄,也可以手动拍摄快照
127.0.0.1:6379> save #立刻拍摄快照,挂起其他请求直到保存结束
OK
127.0.0.1:6379> bgsave #fork一个子进程进行快照保存
Background saving started
RDB持久化的优缺点
-
可以将快照复制到其它服务器从而创建具有相同数据的服务器副本
-
如果系统发生故障,将会丢失最后一次创建快照之后的数据
-
如果数据量很大,保存快照的时间会很长
AOF
AOF持久化对应在redis.conf配置文件中的 APPEND ONLY MODE 快照设置项中
############################## APPEND ONLY MODE ###############################
# By default Redis asynchronously dumps the dataset on disk. This mode is
# good enough in many applications, but an issue with the Redis process or
# a power outage may result into a few minutes of writes lost (depending on
# the configured save points).
#
# The Append Only File is an alternative persistence mode that provides
# much better durability. For instance using the default data fsync policy
# (see later in the config file) Redis can lose just one second of writes in a
# dramatic event like a server power outage, or a single write if something
# wrong with the Redis process itself happens, but the operating system is
# still running correctly.
#
# AOF and RDB persistence can be enabled at the same time without problems.
# If the AOF is enabled on startup Redis will load the AOF, that is the file
# with the better durability guarantees.
#
# Please check http://redis.io/topics/persistence for more information.
appendonly no #是否开启AOF持久化
appendfilename "appendonly.aof" #存储的文件名
# appendfsync always 每个写命令都同步,会严重降低服务器的性能
appendfsync everysec #每秒同步一次,崩溃时最多只会丢失一秒左右的数据
# appendfsync no 让操作系统来决定何时同步,崩溃时数据丢失的数量较大
no-appendfsync-on-rewrite no #执行压缩时是否执行同步操作
#所以随着Redis不断运行,AOF文件会越来越大,通过下面的设置使AOF自动重写,移除AOF文件中冗余的命令
auto-aof-rewrite-percentage 100 #当前AOF文件大小超过上一次重写的AOF文件大小的百分之多少才会重写
auto-aof-rewrite-min-size 64mb #AOF文件多大时开启重写
aof-load-truncated yes #redis在执行恢复时,当最后一条指令被截断时,如果配置的yes时redis会记录日志并继续,如果设置为no时,redis会失败退出。
aof-use-rdb-preamble yes #是否开启混合持久化,混合持久化的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据
AOF持久化的优缺点
- 最多丢失1秒的数据
- AOF文件可读性强
- AOF文件体积大,性能比RDB低
5. 事务
redis事务不保证原子性,也没有隔离级别的概念,其事务过程分3个阶段
- 开始事务(MULTI)
- 命令入队(写多条命令)
- 执行事务(EXEC)/取消事务(DISCARD)
java 中 redis 事务操作 (Springboot2.0后 jedis 被替换为 lettuce)
Jedis jedis = new Jedis("127.0.0.1", 6379); //建立连接
System.out.println(jedis.ping()); //执行操作
...
Transaction multi = jedis.multi(); //开启事务
try{
... //命令入队操作
multi.exec(); //执行事务
} catch(Exeception e) {
multi.discard(); //取消事务
e.printStackTrace();
} finally {
jedis.close(); //关闭连接
}
6. 主从复制
假设让6380端口服务器成为6379端口服务器的从服务器,在6380端口服务器运行
slaveof 127.0.0.1 6379
主服务器查看主从关系
127.0.0.1:6379> INFO replication
# Replication
role:master
...
从服务器查看主从关系
127.0.0.1:6380> INFO replication
# Replication
role:slave
...
- 一个从服务器只能有一个主服务器
- 主服务器可执行读写操作,从服务器只能执行读操作
- 一个服务器同时可以做一个服务器的主服务器和另一个服务器的从服务器
哨兵模式
Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器
7. 缓存
- 缓存穿透:查询的数据在缓存中不存在,于是去数据库查询,查到后放到缓存
- 缓存雪崩:某时间段内,缓存集中过期失效
- 缓存击穿:高并发请求同一个key,在key失效瞬间,并发穿透缓存访问数据库
8. SpringBoot 中使用 Redis
SpringBoot中的自动配置类RedisAutoConfiguration不全面,需编写 Redisconfig类
@Configuration
public class RedisConfig {
/*方法名一定要是 redisTemplate*/
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
//Object,Object 改成 String,Object
RedisTemplate<String,Object> template = new RedisTemplate <>();
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);
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;
}
}
编写 RedisUtils工具类把SpringBoot操作Redis的原生api改为Redis的命令
@Component
public class RedisUtils {
/**
* 注入redisTemplate bean
*/
@Autowired
private RedisTemplate<String,Object> redisTemplate;
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
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(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)
* @return
*/
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)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Hash(哈希)=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
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 对应多个键值
* @return true 成功 false 失败
*/
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)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================Set(集合)=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
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 键
* @return
*/
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代表所有值
* @return
*/
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 键
* @return
*/
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倒数第二个元素,依次类推
* @return
*/
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 值
* @return
*/
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 时间(秒)
* @return
*/
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;
}
}
}