Redis
1、windows安装
1、下载安装包 https://github.com/tporadowski/redis/releases
2、 打开一个 cmd 窗口 使用 cd 命令切换到对应目录运行
redis-server.exe redis.windows.conf
如果想方便的话,可以把 redis 的路径加到系统的环境变量里,这样就省得再输路径了,后面的那个 redis.windows.conf 可以省略,如果省略,会启用默认的。输入之后,会显示如下界面:
3、 使用客户端连接redis服务器
2、基础的知识
默认含有16个数据库
默认使用的是第0个
可以使用select进行切换
keys * #查看数据库所有的key
flushdb #清空数据库
FLUSHALL #清除全部数据库内容
Redis是单线程的
Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽。
Redis为什么单线程还这么快?
1、误区1:高性能的服务器一定是多线程的?
2、误区2:多线程(CPU上下文会切换!)一定比单线程效率高
核心:redis是将所有的数据全部放在内存中的,所以说使用单线程去操作效率是最高的,多线程(CPU切换会耗时),对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上,在内存上这个就是最佳的方案
3、五大数据类型
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
Redis-key
127.0.0.1:6379> set name lizeyu
OK
127.0.0.1:6379> set age 22
OK
127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> exists name #测试key是否存在
(integer) 1
127.0.0.1:6379> remove name 1 #删除key
(integer) 1
127.0.0.1:6379> keys *
1) "age"
127.0.0.1:6379> expire age 10 #设置key过期时间
(integer) 1
127.0.0.1:6379> ttl age #查看key还有多久过期
(integer) 6
127.0.0.1:6379> ttl age
(integer) 1
127.0.0.1:6379> ttl age
(integer) -2
String
127.0.0.1:6379> set key1 v1
OK
127.0.0.1:6379> get key1
"v1"
127.0.0.1:6379> append key1 hello #追加
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> strlen key1 #字符串长度
(integer) 7
#############################################################
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> incr views #自增1
(integer) 1
127.0.0.1:6379> decr views #自减1
(integer) 0
127.0.0.1:6379> incrby views 10 #设置步长
(integer) 10
127.0.0.1:6379> decrby views 5
(integer) 5
#############################################################
127.0.0.1:6379> set key1 "hello,lizeyu"
OK
127.0.0.1:6379> getrange key1 0 4 #截取部分字符串
"hello"
127.0.0.1:6379> getrange key1 0 -1 #截取全部字符串
"hello,lizeyu"
127.0.0.1:6379> setrange key1 1 xx 替换指定位置开始的字符串
(integer) 12
127.0.0.1:6379> get key1
"hxxlo,lizeyu"
#############################################################
127.0.0.1:6379> setex key3 30 "hello" #(set with expire)设置过期时间,设置key3为hello,30秒后过期
OK
127.0.0.1:6379> ttl key3
(integer) 11
127.0.0.1:6379> setnx mykey "redis" #(set if not exists) 不存在再设置
(integer) 1
127.0.0.1:6379> keys *
1) "key1"
2) "mykey"
3) "age"
4) "name"
5) "views"
127.0.0.1:6379> setnx mykey "111"
(integer) 0
127.0.0.1:6379> get mykey
"redis"
#############################################################
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 #同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> msetnx k1 v1 k4 v4 #不存在同时设置多个值,一起成功一起失败
(integer) 0
127.0.0.1:6379> get k4
(nil)
#############################################################
127.0.0.1:6379> getset db redis #先获取值再设置新的值
(nil)
127.0.0.1:6379> getset db mysql
"redis"
127.0.0.1:6379> get db
"mysql"
List
127.0.0.1:6379> lpush list one #将一个值或多个值插入到表头部(左)
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 #获取list的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1 #通过区间获取具体的值
1) "three"
2) "two"
127.0.0.1:6379> rpush list four #将一个值或多个值插入到表尾部(右)
(integer) 4
127.0.0.1:6379> keys *
1) "list"
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
127.0.0.1:6379> lpop list #左侧移除一个值
"three"
127.0.0.1:6379> rpop list #右侧移除一个值
"four"
127.0.0.1:6379> lindex list 0 #根据下标获取值
"two"
127.0.0.1:6379> lindex list 1
"one"
####################################################
127.0.0.1:6379> rpush list four #移除制定的值
(integer) 3
127.0.0.1:6379> lrem list 1 four
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
Set
127.0.0.1:6379> sadd myset "hello" #添加到set
(integer) 1
127.0.0.1:6379> smembers myset #获取成员
1) "hello"
127.0.0.1:6379> sismember myset hello #判断元素是否存在
(integer) 1
127.0.0.1:6379> sadd myset "world"
(integer) 1
127.0.0.1:6379> srem myset hello #删除元素
(integer) 1
127.0.0.1:6379> smembers myset
1) "world"
127.0.0.1:6379> scard myset #计算set集合元素个数
(integer) 1
Hash
key-map,这个值是一个map
127.0.0.1:6379> hset myhash field1 lizeyu #hash中存值
(integer) 1
127.0.0.1:6379> hget myhash field1
"lizeyu"
127.0.0.1:6379> hmset myhash field2 liziwei field3 lzy
OK
127.0.0.1:6379> hgetall myhash #获取hash中的多个值
1) "field1"
2) "lizeyu"
3) "field2"
4) "liziwei"
5) "field3"
6) "lzy"
127.0.0.1:6379> hmget myhash field1 field2 field3 #获取多个值
1) "lizeyu"
2) "liziwei"
3) "lzy"
##############################
127.0.0.1:6379> hlen myhash #哈希的长度
(integer) 3
127.0.0.1:6379> hkeys myhash #哈希中的所有key
1) "field1"
2) "field2"
3) "field3"
127.0.0.1:6379> hvals myhash #哈希中的所有value
1) "lizeyu"
2) "liziwei"
3) "lzy"
Zset
在set的基础上加上排序
4、三种特殊数据类型
geospatial地理位置
可以查询一些数据: http://www.jsons.cn/lngcode/
geoadd
# getadd 添加一些地理位置
# 规则:两级无法直接添加,我们一般会下载城市数据,直接通过java程序一次性导入
# 有效的经度从-180度到180度。
# 有效的纬度从-85.05112878度到85.05112878度。
127.0.0.1:6379> geoadd china:city 116.405285 39.904989 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.472644 31.231706 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 113.280637 23.125178 guangzhou
(integer) 1
127.0.0.1:6379> geoadd china:city 114.085947 22.547 shenzhen
(integer) 1
geopos
127.0.0.1:6379> geopos china:city beijing #获取当前的定位
1) 1) "116.40528291463851929"
2) "39.9049884229125027"
geodist
127.0.0.1:6379> geodist china:city beijing shanghai km #两个位置之间的距离
"1067.5980"
127.0.0.1:6379> geodist china:city beijing shenzhen km
"1943.0240"
5、事务
Redis事务本质:一组命令的集合
Redis单条命令是保持原子性的,但是事务不保证原子性
redis的事务:
- 开启事务()
- 命令入队()
- 执行事务()
正常执行事务
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> exec #执行
1) OK
2) OK
放弃事务
discard #取消事务
编译型异常(代码有问题!命令有错!),事务中所有的命令都不会被执行
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3 #执行事务报错
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k3
(nil)
运行时异常(I/O),如果事务队列中存在语法性,那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常
127.0.0.1:6379> set k1 "v1"
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
4) "v3"
6、SpringBoot整合
说明:在springboot2.x之后,原来使用的jedis被替换为了lettuce
jedis:采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全,使用jedis连接池
lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况!
整合测试一下
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、配置连接
spring:
redis:
host: 127.0.0.1
port: 6379
3、测试
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("mykey", "lizeyu");
System.out.println(redisTemplate.opsForValue().get("mykey"));
}
Redis的序列化配置
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<String,Object>();
redisTemplate.setConnectionFactory(connectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(redisTemplate.getKeySerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
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);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
Redis工具类
/**
* RedisConfig
* 在我们真实的分发中,或者你们在公司,一般都可以看到一个公司自己封装RedisUtil
* @author lizeyu
* @date 2020/4/4 20:40
*/
@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(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;
}
}
}
7、redis持久化RDB和AOF
持久化的作用
什么是持久化
redis的所有数据保存在内存中,对数据的更新将异步的保存到硬盘上
持久化的实现方式
快照:某时某刻数据的一个完成备份,
-mysql的Dump
-redis的RDB
写日志:任何操作记录日志,要恢复数据,只要把日志重新走一遍即可
-mysql的 Binlog
-Hhase的 HLog
-Redis的 AOF
RDB
什么是RDB
触发机制-主要三种方式
'''
save(同步)
1 客户端执行save命令----》redis服务端----》同步创建RDB二进制文件
2 会造成redis的阻塞(数据量非常大的时候)
3 文件策略:如果老的RDB存在,会替换老的
4 复杂度 o(n)
'''
'''
bgsave(异步,Backgroud saving started)
1 客户端执行save命令----》redis服务端----》异步创建RDB二进制文件(fork函数生成一个子进程(fork会阻塞reids),执行createRDB,执行成功,返回给reids消息)
2 此时访问redis,会正常响应客户端
3 文件策略:跟save相同,如果老的RDB存在,会替换老的
4 复杂度 o(n)
'''
'''
自动(通过配置)
配置 seconds changes
save 900 1
save 300 10
save 60 10000
如果60s中改变了1w条数据,自动生成rdb
如果300s中改变了10条数据,自动生成rdb
如果900s中改变了1条数据,自动生成rdb
以上三条符合任意一条,就自动生成rdb,内部使用bgsave
'''
#配置:
save 900 1 #配置一条
save 300 10 #配置一条
save 60 10000 #配置一条
dbfilename dump.rdb #rdb文件的名字,默认为dump.rdb
dir ./ #rdb文件存在当前目录
stop-writes-on-bgsave-error yes #如果bgsave出现错误,是否停止写入,默认为yes
rdbcompression yes #采用压缩格式
rdbchecksum yes #是否对rdb文件进行校验和检验
#最佳配置
save 900 1
save 300 10
save 60 10000
dbfilename dump-${port}.rdb #以端口号作为文件名,可能一台机器上很多reids,不会乱
dir /bigdiskpath #保存路径放到一个大硬盘位置目录
stop-writes-on-bgsave-error yes #出现错误停止
rdbcompression yes #压缩
rdbchecksum yes #校验
触发机制-不容忽略的方式
1 全量复制 #没有执行save和bgsave没有添加rdb策略,还会生成rdb文件,如果开启主从复制,主会自动生成rdb
2 debug reload #debug级别的重启,不会将内存中的数据清空
3 shutdown save#关闭会触发rdb的生成
AOF
RDB问题
耗时,耗性能:
不可控,可能会丢失数据
AOF介绍
客户端每写入一条命令,都记录一条日志,放到日志文件中,如果出现宕机,可以将数据完全恢复
AOF的三种策略
日志不是直接写到硬盘上,而是先放在缓冲区,缓冲区根据一些策略,写到硬盘上
always:redis–》写命令刷新的缓冲区—》每条命令fsync到硬盘—》AOF文件
everysec(默认值):redis——》写命令刷新的缓冲区—》每秒把缓冲区fsync到硬盘–》AOF文件
no:redis——》写命令刷新的缓冲区—》操作系统决定,缓冲区fsync到硬盘–》AOF文件
命令 | always | everysec | no |
---|---|---|---|
优点 | 不丢失数据 | 每秒一次fsync,丢失1秒数据 | 不用管 |
缺点 | IO开销大,一般的sata盘只有几百TPS | 丢1秒数据 | 不可控 |
AOF 重写
随着命令的逐步写入,并发量的变大, AOF文件会越来越大,通过AOF重写来解决该问题
原生AOF | AOF重写 |
---|---|
set hello worldset hello javaset hello heheincr counterincr counterrpush mylist arpush mylist brpush mylist c过期数据 | set hello heheset counter 2rpush mylist a b c |
本质就是把过期的,无用的,重复的,可以优化的命令,来优化
这样可以减少磁盘占用量,加速恢复速度
实现方式
bgrewriteaof:
客户端向服务端发送bgrewriteaof命令,服务端会起一个fork进程,完成AOF重写
AOF重写配置:
配置名 | 含义 |
---|---|
auto-aof-rewrite-min-size | AOF文件重写需要尺寸 |
auto-aof-rewrite-percentage | AOF文件增长率 |
统计名 | 含义 |
---|---|
aof_current_size | AOF当前尺寸(单位:字节) |
aof_base_size | AOF上次启动和重写的尺寸(单位:字节) |
自动触发时机(两个条件同时满足):
aof_current_size>auto-aof-rewrite-min-size:当前尺寸大于重写需要尺寸
(aof_current_size-aof_base_size)/aof_base_size>auto-aof-rewrite-percentage:(增长率)当前尺寸减去上次重写的尺寸,除以上次重写的尺寸如果大于配置中的增长率
重写流程
配置
appendonly yes #将该选项设置为yes,打开
appendfilename "appendonly-${port}.aof" #文件保存的名字
appendfsync everysec #采用第二种策略
dir /bigdiskpath #存放的路径
no-appendfsync-on-rewrite yes #在aof重写的时候,是否要做aof的append操作,因为aof重写消耗性能,磁盘消耗,正常aof写磁盘有一定的冲突,这段期间的数据,允许丢失
RDB和AOF的选择
rdb和aof的比较
命令 | rdb | aof |
---|---|---|
启动优先级 | 低 | 高(挂掉重启,会加载aof的数据) |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢数据 | 根据策略决定 |
轻重 | 重 | 轻 |
rdb最佳策略
rdb关掉,主从操作时
集中管理:按天,按小时备份数据
主从配置,从节点打开
aof最佳策略
开:缓存和存储,大部分情况都打开,
aof重写集中管理
everysec:通过每秒刷新的策略
最佳策略
小分片:每个redis的最大内存为4g
缓存或存储:根据特性,使用不通策略
时时监控硬盘,内存,负载网络等
有足够内存
使用过程中的问题
- 在redis整合springboot过程中遇见了LocalDatetime序列化问题
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of
java.time.LocalDateTime
(no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
解决方法
LocalDateTime属性加上注解
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)