日常开发中经常使用redis,但每次项目基本只配置过一次,或者复制粘贴,对于从零搭建redis,其中的原理流程模糊不清,所以在这里做个梳理,日后回顾也很方便。
Redis的官网打开比较慢,而且全英文对英文不好的同学看起来不方便,这块推荐去redis中文网查看官方文档,官网地址:http://www.redis.cn/。
其实大部分内容官网都介绍的很清楚了,在这里只是对知识做一个梳理,方便自己日后查看。
redis分windows版本和linux版本,这块为了演示方便使用windows版本,在实际开发中,多数基于linux。安装也很简单,将下载的文件解压到一个文件夹即可。然后运行redis-server.exe来启动服务,再运行redis-cli.exe来启动客户端,相当于mysql的客户端一样,用来操作数据库。
对于Redis的使用者来说, Redis作为Key-Value型的内存数据库, 其Value有多种类型.
这些Value的类型, 只是"Redis的用户认为的, Value存储数据的方式". 而在具体实现上, 各个Type的Value到底如何存储, 这对于Redis的使用者来说是不公开的。底层的数据结构略显复杂,有兴趣的读者可以参考:http://www.pianshen.com/article/7522221332/和https://www.cnblogs.com/ysocean/p/9102811.html
这两篇文章基本能把底层的数据结构摸透了。
存储string,通用格式如下,key和value分别代表键值,
EX
seconds – 设置键key的过期时间,单位时秒PX
milliseconds – 设置键key的过期时间,单位时毫秒NX
– 只有键key不存在的时候才会设置key的值XX
– 只有键key存在的时候才会设置key的值注意: 由于SET
命令加上选项已经可以完全取代SETNX, SETEX, PSETEX的功能,所以在将来的版本中,redis可能会不推荐使用并且最终抛弃这几个命令。
set key value [EX seconds] [PX milliseconds] [NX|XX]
举例,代码如下:
set username 123 ex 2 nx
上边代码表示,存储username,值为123,2秒后过期会被删除,nx代表如果username已经存在,则存储失败。
使用get username 来查看存储的值。
使用del username可以删除该值,1代表删除成功,0代表删除失败。
使用type命令可以查看该值的类型,string\hash\list\set\sset
使用expire username 3来设置3秒后过期。
ttl命令用来查看key对应的值剩余存活时间。
使用exist命令返回1或0标识给定key的值是否存在,1代表存在,0代表不存在.
使用mset来实现多值存储,如mset a 1 b 2 c 3,
使用mget来获取多值,如mget a b c
使用incr来实现原子递增,如incr a,代表a++。
使用Incrby来实现执行值增加,如incrby a 100,代表a=a+100.
同理递减有decr和decrby。
注意type、del和exists是通用的命令(string\hash\list\set\sset),其他命令只针对string
因为list是一个队列,所以对应的操作命令有头添加和尾添加,也可以理解为左添加和右添加,对应的命令是lpush和rpush,
lpush将所有指定的值插入到存于 key 的列表的头部。如果 key 不存在,那么在进行 push 操作前会创建一个空列表。 如果 key 对应的值不是一个 list 的话,那么会返回一个错误,可以使用一个命令把多个元素 push 进入列表,只需在命令末尾加上多个指定的参数。元素是从最左端的到最右端的、一个接一个被插入到 list 的头部。 所以对于这个命令例子 LPUSH mylist a b c
,返回的列表是 c 为第一个元素, b 为第二个元素, a 为第三个元素。
执行命令后会返回list的长度。
rpush同理lpush,这里就不重复介绍。
通过lrange来查看list,
lrange返回存储在 key 的列表里指定范围内的元素。 start 和 end 偏移量都是基于0的下标,即list的第一个元素下标是0(list的表头),第二个元素下标是1,以此类推。偏移量也可以是负数,表示偏移量是从list尾部开始计数。 例如, -1 表示列表的最后一个元素,-2 是倒数第二个,以此类推。
所以查看整个队列就可以写命令:lrange mylist 0 -1
pop,它从list中删除元素并同时返回删除的值。可以在左边lpop或rpop右边操作.
使用LTRIM把list从左边截取指定长度。如:ltrim mylist 0 3,执行操作后,mylist变为原来的0到3.
使用llen获取list的长度,
对于lpop和rpop还有阻塞版本,阻塞版本的出现是为了解决生产者消费者的问题,由于需求比较繁琐这里不做介绍参见http://www.redis.cn/commands/blpop.html。
正如你可以从上面的例子中猜到的,list可被用来实现聊天系统。还可以作为不同进程间传递消息的队列。关键是,你可以每次都以原先添加的顺序访问数据。这不需要任何SQL ORDER BY 操作,将会非常快,也会很容易扩展到百万级别元素的规模。
例如在评级系统中,比如社会化新闻网站 reddit.com,你可以把每个新提交的链接添加到一个list,用LRANGE可简单的对结果分页。
在博客引擎实现中,你可为每篇日志设置一个list,在该list中推入博客评论,等等
使用hset来存储键值对,如:hset myhash username zhangsan.
使用hget来获取键值对,如:hget myhash username
使用hdel来删除键值对,如:hdel myhash
使用hmset来存储多个键值对,如:hmset myhash username zhangsan password lisi
使用hmget来获取多个键值对,如:hmget myhahs username password
使用hgetall来获取所有键值对,如:hgetall myhash
也可以像string一样,通过hincrby来增加某个键值对的值,如 hincrby myhash username 50.
我们知道set元素是唯一的,所以不允许存储重复的key。
使用sadd来存储值,如:sadd myset a b c d e
使用smembers来查看所有元素,如:smembers myset;
使用srem来删除值,如:srem myset a;
使用spop来随机获取一个值,并删除set中的该元素。如:mpop myset b。
要求元素唯一并且有序,排序按照给定的score来排序。
使用zadd key score value 来存储,如:zadd myzset 1 a
使用zrange key start end (增加with scores可以显示出分数)来查看元素,如:zrange myzset 0 -1
使用zrem key value来删除元素,如:zrem myzset a
keys * :查看当前redis所有的key;
type:查看key的类型
del:删除key。
flushDB:清空redis数据。
Redis 提供了不同级别的持久化方式:
rdb是默认持久化方式,aof需要修改配置文件开启,rdb可以通过命令save、bgsave来触发,正常关闭服务shutdown也会触发,在者就是默认触发方式:
save 900 1:表示900 秒内如果至少有 1 个 key 的值变化,则保存 save 300 10:表示300 秒内如果至少有 10 个 key 的值变化,则保存 save 60 10000:表示60 秒内如果至少有 10000 个 key 的值变化,则保存
当然如果你只是用Redis的缓存功能,不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能。可以直接一个空字符串来实现停用:save ""。但是记住,在主从复制情况中,是无法关闭的。
redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
参考文章:https://www.cnblogs.com/ysocean/p/9114268.html
aof持久化的策略,共有三种:
1)always:将aof_buf中的内容写入并同步到AOF文件中。
2)everysec:将aof_buf中内容写如AOF文件中,如果上次同步AOF文件的时间距离现在超过1s,那么再次同步AOF文件。这个操作是由一个线程专门负责执行的。
3)no:将aof_buf中的内容写入到AOF文件中,但并不对AOF同步,何时同步交给OS。
一般选用第二种,
AOF模式的一个问题是AOF文件可能会变得非常大。
通过分析AOF文件,往往发现里面有太多的重复和冗余数据,可以生成一个新的AOF文件来代替旧的AOF文件,这就是AOF重写。这个操作满足一定条件是,Redis会自动触发。一般生产环境一般要求在达到几个g或者几十个g才会重写,因为重写会影响redis性能。
参考文章:https://www.jianshu.com/p/8d54b6d01045
更多详细内容参见官网:http://www.redis.cn/topics/persistence.html
redis采用的是定期删除+惰性删除策略。
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。本段落引用:https://www.cnblogs.com/rjzheng/p/9096228.html#!comments
当maxmemory限制达到的时候Redis会使用的行为由 Redis的maxmemory-policy配置指令来进行配置。
以下的策略是可用的:
如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。
选择正确的回收策略是非常重要的,这取决于你的应用的访问模式,不过你可以在运行时进行相关的策略调整,并且监控缓存命中率和没命中的次数,通过RedisINFO命令输出以便调优。
一般的经验规则:
allkeys-lru 和 volatile-random策略对于当你想要单一的实例实现缓存及持久化一些键时很有用。不过一般运行两个实例是解决这个问题的更好方法。
为了键设置过期时间也是需要消耗内存的,所以使用allkeys-lru这种策略更加高效,因为没有必要为键取设置过期时间当内存有压力时。
以上引自官网,详细内容参见:http://www.redis.cn/topics/lru-cache.html
redis也是支持事务的,所以这块可以了解下:http://www.redis.cn/topics/transactions.html
Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务:
详细内容参见官网:http://www.redis.cn/topics/sentinel.html
参照官网已经写的很详细了,http://www.redis.cn/topics/cluster-tutorial.html
jedis相当与通过java代码来操作redis,类似通过jdbc操作mysql一样。
通过IDEA搭建简单的测试环境,导入两个依赖,分别是jedis依赖和juint测试依赖:
redis.clients
jedis
3.1.0
junit
junit
4.13-beta-3
然后新建一个测试类,通过创建jedis对象就可以调用各种api了。
Jedis jedis=new Jedis();//构造方法中什么都不写,默认为localhost:6379
如果熟悉原生redis调用指令,那么使用jedis也很容易,比如获取string类型的某个key,原生命令是get(key)。jedis中只需要输入jedis.get就可以弹出get相关的方法,由于非常简单,这里就不对各个api做介绍了。
spring为了操作redis整合的redistemplate框架,
参考文章:https://www.cnblogs.com/zeng1994/p/03303c805731afc9aa9c60dbbd32a323.html,感谢该作者
使用IDEA,新建空的maven项目,然后导入pom如下:
4.0.0
com.ming
redistemplate-test
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-parent
2.2.0.RELEASE
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-test
com.fasterxml.jackson.core
jackson-databind
2.9.9
在java包下创建自己的包如com.test,然后在该目录下创建springboot启动类,如下:
package com.ming;
import org.springframework.boot.SpringApplication;
@org.springframework.boot.autoconfigure.SpringBootApplication
public class SpringBootApplication {
public static void main(String[] args){
SpringApplication.run(SpringBootApplication.class);
}
}
接着在resource目录下,创建application.yml配置文件:
spring:
redis:
database: 0 # Redis数据库索引(默认为0)
host: localhost #Redis服务器地址
port: 6379 #端口
password: #链接服务器的密码默认为空
jedis:
pool:
max-active: 200 #连接池最大连接数(使用负值表示没有限制)
max-wait: -1 #连接池最大阻塞时间(使用负值表示没有限制)
max-idle: 10 #连接池中的最大空闲连接
min-idle: 0 #连接池中的最小空闲连接
timeout: 1000 #连接超时时间
接着创建测试类,在test文件夹下创建跟java文件夹下同样的目录结构,然后创建测试类,如下:
package com.ming;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Test
public void test(){
System.out.println("test");
redisTemplate.opsForValue().set("zhangsan","123");
}
}
因为在pom中已经导入了redis依赖,我们现在就可以使用redistemplate来测试程序了。比如实现存储一个字符串,如下:
@Test
public void test(){
redisTemplate.opsForValue().set("zhangsan","123");
}
查询一个字符串,如下:
@Test
public void getStringTest(){
Object object=redisTemplate.opsForValue().get("zhangsan");
System.out.println(object.toString());
}
以上代码没什么问题,都可以正常执行,那我们通过命令行查看下redis的数据,结果发现如下:
我们可以清楚看到,正常的key应该是zhangsan,结果显示很长一堆字符串。
其实这个是用于存储进去的是object导致的,这也是spring提供给我们的RedisTemplate的默认方式,默认方式的配置类参见org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,这里就不贴了,然后我们需要自己配置一个Redis的配置类,因为默认的配置类中有这样的设置@ConditionalOnMissingBean,就是说假如你没有自定义配置类时,系统会使用默认的,假如你自定义了配置类,就会使用你自定义的配置类。
配置类如下:
package com.ming.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.omg.PortableInterceptor.NON_EXISTENT;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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 {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory){
RedisTemplate redisTemplate=new RedisTemplate();
redisTemplate.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper=new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
// key采用String的序列化方式,默认是object
redisTemplate.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
配置类写好了,为了以后调用方便,再封装一个redis工具类就over了。
工具类如下:
package com.ming.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.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @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);
}
// ================================Map=================================
/**
* 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
redistemplate的api,里边有分布式锁加强版的代码:https://www.jianshu.com/p/19e851a3edba
参考文章:https://www.cnblogs.com/webwangbao/p/9247318.html
先用redis实现了简单的分布式锁,然后再用redisson实现分布式锁,并分析了redisson分布式锁实现的原理,参考文章:https://www.jianshu.com/p/47fd7f86c848
下边这片文章详细的分析了redis分布式锁的常见错误实现方式,最后给了一个正确实现方式,很有参考价值;
参考文章:https://blog.csdn.net/chanllenge/article/details/102983597
https://www.cnblogs.com/rjzheng/p/9096228.html这篇文章写的很好,适合有redis基础,复习面试等使用。涉及到的问题如下:
1、为什么使用redis
2、使用redis有什么缺点
3、单线程的redis为什么这么快
4、redis的数据类型,以及每种数据类型的使用场景
5、redis的过期策略以及内存淘汰机制
6、redis和数据库双写一致性问题
7、如何应对缓存穿透和缓存雪崩问题
8、如何解决redis的并发竞争问题
Redis常见面试题精简版:https://blog.csdn.net/ThinkWon/article/details/103522351
这个系列,作者写的很不错,可以看他的系列文章:https://blog.csdn.net/qq_35190492/article/details/102841400
本文内容多数摘自互联网,感谢以上文章的作者,在这里我只是对其汇总,做个总结。希望对大家有帮助。