目录
Redis(Remote Dictionary Server )即远程字典服务,是 C 语言开发的一个开源的key/value存储系统(官网:http://redis.io),是一个分布式缓存数据库。
官方只提供Linux版本,windows公司提供了Windows版的(Redis的次版本号(第一个小数点后的数字为偶数的版本是稳定版本,奇数为非稳定版本)
Redis作为一种key/value结构的数据存储系统,为了便于对数据进行进行管理,提供了多种数据类型,Reids中基础数据结构包含字符串、散列,列表,集合,有序集合
优势: 并发处理能力非常好 , 更好的保证数据的可靠性, 有丰富的数据类型
Bootnb 相关:https://www.runoob.com/redis/redis-tutorial.html
Redis 官网:https://redis.io/
源码地址:https://github.com/redis/redis
Redis 在线测试:http://try.redis.io/
Redis 命令参考:http://doc.redisfans.com/
Redis数据存于磁盘中,采用TCP协议进行通讯
双写: 数据同时存入分布式缓存和数据库中
下载安装于linux操作系统中的docker容器中
基础常用命令
启动及关闭redis服务
docker ps #查看进程 docker stop 618 #关闭进程 docker start redis #启动redis进程 ps -ef | grep redis # 查看正在运行的进程过滤redis进程 docker exec -it redis01 bash #redis01 为容器名 进入redis容器 redis-cli #登陆本地redis服务 或者 redis-cli -p 6379 或者 redis-cli -p 6379 -a password #-a后面为password,此操作需要开启redis.conf文件中的 requirepass选项 redis-cli -h ip -p 6379 -a password #登录远端redis服务 info #查看当前redis节点的详细配置信息 shutdown #保护模式关闭redis服务 exit #关闭redis服务
select 1 #单机模式Redis默认支持16个数据库,下标是0-15 flushdb #清除当前数据库数据 flushall #清除所有数据库数据 set key1 100 #存入数据 type key1 #查看数据类型 ①set key2 100 EX 5 #设置数据存储的有效时长 EX秒 PX毫秒 ②set key3 100 expire key3 5 #数据保留5秒 ttl key3 #查看数据有效时长 keys * #查看当前数据库的所有key get key1 #查询key1的数据
常见数据类型
String数据类型
存储的值可以是字符串,其最大字符串长度支持到512M
于此类型,可以实现博客的字数统计,将日志不断追加到指定key,实现一个分布式自增id,实现一个博客的的点赞操作等set num 1 #如果值存在则覆盖 setnx num 9 #判断是否存在num,存在就不存入数据,不存在则存入数据 incr num #2 数据在内存中递增,数据库自增是在数据库程序中 订单分布式id incr num #3 如果num不存在,则自动会创建,如果存在自动+1 incrby num 2 #每次递增2 decr num #递减 decrby num 3 #每次递减3 del key1 key2 #删除 append test "abc" #向尾部追加值。如果键不存在则创建该键 strlen test #字符串长度,返回数据的长度,如果键不存在则返回0。注意,如果键值为空串,返回也是0 mset a 1 b 2 c 3 #同时设置/获取多个键值 mget a b c exists a b #判断某个值是否存在,0为不存在
Hash数据类型
Redis散列类型相当于Java中的HashMap,实现原理跟HashMap一致,一般用于存储对象信息,存储了字段(field)和字段值的映射,K V中V又是KV结构
hset user username chenchen hset user password 123 hset user username chenchen password 123 hsetnx user password 456 hget user username hgetall user hmget user username password hvals user #chenchen 123 hkeys user #username password hincrby article total -1 #没有hdecrby自减命令 hexists user username hdel user username # 操作的是小key del user #操作的是大key
List数据类型
Redis的list类型相当于java中的LinkedList,其原理就就是一个双向链表。支持正向、反向查找和遍历等操作,插入删除速度比较快。经常用于实现热销榜,最新评论等的设计。K V 中的V为很多个有序可重复的V
lpush list1 a b c c d rpop list1 5 #a b c c d llen list1 #0 lpush list1 a b c c d lpop list1 5 #d c c b a lpush list1 a b c #最先进去的是最后的 linsert list1 before a f rpop list1 4 #a f b c lpush list1 a b c #c的下标是0 lset list1 0 g rpop list1 3 #a b g lpush num1 a b c lrem num1 1 a lrange num1 0 -1 #c b ltrim num1 0 0 lindex num 0 lpush g1 a b lpush g2 g f rpoplpush g2 g1 rpop g1 3 #a b g lpush num1 a b c brpop num1 20 #a 当元素移除完后,线程会指定阻塞时间20秒 brpop num1 20 #b brpop num1 20 #c
Set数据类型
Redis的Set类似Java中的HashSet,是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。Redis中Set集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。K V 中的V为很多个无序不可重复的V
sadd set1 a a b b c c c d d smembers set1 #d b a c spop set1 1 #c spop set1 2 #a b sadd set1 a a b b c c c d d scard set1 #4 sadd set2 7 8 9 smove set1 set2 c spop set2 #7 c 8 9 sunion set1 set2
Zset数据类型
zet是Redis提供的一个非常特别的数据结构,常用作排行榜等功能,以用户d为value,关注时间或者分数作为score进行排序。K score1 user1 score2 user2
Java中操作redis
Redis 是一种C/S 架构的分布式缓存数据库,它有自带的命令行客户端,也有对应的Java或其它语言客户端,可以在这些客户端中通过一些API对redis进行读写操作。
创建父工程,pom文件添加编译配置
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build>
Jedis的基本应用
Jedis是Java中操作redis的一个客户端,类似通过jdbc访问mysql数据库.
创建redis-jedis 工程添加如下依赖
<!--这组API是redis官方推荐的一组用于在java中操作 redis数据库的API--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!--这是Google公司推出的用于实现Java对象和JSON字符串之间 相互转换的一组API--> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.6</version> </dependency>
在Jedis工程中的src/test/java目录创建单元测类
public class JedisTests { /** * 测试hash类型数据 */ @Test public void testHashOper01(){ //1.建立连接(TCP) Jedis jedis=new Jedis( "192.168.126.128", 6379); //2.添加数据 Map<String,String> map1=new HashMap<>(); map1.put("id", "201"); map1.put("title", "redis6.2.5"); jedis.hset("blog:201", map1);//不存在则直接添加 jedis.hset("blog:201", "title", "redis6.2.6"); //3.读取数据 map1=jedis.hgetAll("blog:201"); System.out.println(map1); //4.释放资源 jedis.close(); } /** * 测试字符串操作 */ @Test public void testStringOper02() { //1.建立连接 Jedis jedis=new Jedis( "192.168.126.128", 6379); //2.将一个map对象转换为json字符串,然后写入到redis并给定有效期 Map<String,String> map1=new HashMap<>(); map1.put("id", "101"); map1.put("title", "redis6.2.5"); Gson gson=new Gson(); String jsonStr1=gson.toJson(map1); System.out.println(jsonStr1); jedis.set("blog:101", jsonStr1); jedis.expire("blog:101", 20); //3.从redis将json字符串读出,并转换为map然后输出. String jsonStr2 = jedis.get("blog:101"); Map<String,String> map2=gson.fromJson(jsonStr2,Map.class); System.out.println(map2); //4.释放资源 jedis.close(); } /** * 测试字符串操作 */ @Test public void testStringOper01(){ //1.建立连接 Jedis jedis=new Jedis( "192.168.126.128", 6379); //2.添加数据(Create) jedis.set("id", "100"); Long setnx = jedis.setnx("lock", "AAA"); jedis.set("logs", "abc"); //3.获取数据(Retrieve) String id = jedis.get("id"); System.out.println("update.before.id="+id); //4.修改数据(Update) jedis.incr("id"); id = jedis.get("id"); System.out.println("update.after.id="+id); jedis.expire("lock", 10); jedis.append("logs","EF"); //5.删除数据(Delete) jedis.del("id"); id = jedis.get("id"); System.out.println("delete.after.id="+id); //6.释放资源 jedis.close(); } @Test public void testGetConnection(){ //1.建立连接 //假如无法建立连接,要从如下几个方面进行分析 //1)ip //2)port //3)防火墙 //4)redis服务是否启动ok Jedis jedis=new Jedis( "192.168.126.128", 6379); //jedis.auth("123456"); //2.执行ping操作 String result = jedis.ping(); System.out.println(result); } }
连接池JedisPool连接池
public class JedisPoolTests { /** * Jedis连接池应用测试 * 注意:所有连接池都有享元模式的应用,以实现对象重用. */ @Test public void testJedisPool(){ //1.创建池对象 //GenericObjectPoolConfig poolConfig= //new GenericObjectPoolConfig(); JedisPoolConfig poolConfig=//这个类型是上面类型的子类类型. new JedisPoolConfig(); poolConfig.setMaxTotal(128);//最大连接数 poolConfig.setMaxIdle(8);//最大空闲连接数 JedisPool jedisPool= //new JedisPool("192.168.126.128", 6379); new JedisPool(poolConfig, "192.168.126.128", 6379); //2.获取连接 Jedis resource = jedisPool.getResource(); //3.读写数据 resource.set("id", "100"); String id = resource.get("id"); System.out.println(id); //4.释放资源 resource.close(); jedisPool.close();//以后这个池是关服务的时候关. } }
提取连接池配置到配置类中,需要时直接通过方法获取即可
可以参考HikariDataSource类中连接池的创建,以上代码还是存在线程不停切换的问题,需要将成员变量赋值给局部变量,再进行判断,可以减少线程之间切换的问题; 还可以进一步优化,将连接池的配置参数提取到pom配置文件中public class JedisDataSource { //初始化池对象并获取连接 (何时需要何时创建池对象~懒加载) private static volatile JedisPool jedisPool; /** * volatile 关键字特点: * 1)修饰属性 * 2)禁止指令重排序(JVM底层执行指令时,可能考虑到一定优化,会对指令进行重排序) * 3)保证线程可见性(一个线程修改了这个值,其它线程立刻可见.) */ public static Jedis getConnection(){ //JedisDataSource.class if(jedisPool==null) { synchronized (JedisDataSource.class) { if (jedisPool == null) { JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(128);//最大连接数 poolConfig.setMaxIdle(8);//最大空闲连接数 jedisPool = new JedisPool(poolConfig, "192.168.126.128", 6379); //1.对象创建的过程? //1)分配内存 //2)初始化属性 //3)执行构造方法 //4)将JedisPool对象赋值给jedisPool变量 } } } return jedisPool.getResource(); } public static void close(){ jedisPool.close(); } }
RedisTemplate基本应用(lettuceAPI)
创建子工程redis-template,添加添加依赖
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.2.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <!--假如项目中配置lettuce池对象,需要添加如下依赖--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> </dependencies>
spring: redis: host: 192.168.126.129 #写自己的ip port: 6379
创建启动类测试连接是否成功
RedisTemplate 对象在存取数据时默认会按照如下方式执行:
1)key和value在存储时,会基于JDK方式进行序列化
2)基于key取值时,默认会基于JDK做反序列化.基于RedisTemplate对象的incrment方法实现key值的递增时,注意值的结构类型
@SpringBootTest public class RedisTemplateTests { /** * RedisTemplate 对象在存取数据时默认会按照如下方式执行: * 1)key和value在存储时,会基于JDK方式进行序列化 * 2)基于key取值时,默认会基于JDK做反序列化. */ @Autowired private RedisTemplate redisTemplate; /** * 应用redisTemplate对象的另一种方式 */ @Test void testFlushdb(){ redisTemplate.execute(new RedisCallback() { @Override public Object doInRedis( RedisConnection redisConnection) throws DataAccessException { //这里的RedisConnection表示连接 //通过这里的连接对象,你可以执行一些其它数据库操作 redisConnection.flushAll(); return "ok"; } }); } @Test void testHashOper02() throws InvocationTargetException, IllegalAccessException { //1.创建Blog对象 Blog blog1=new Blog(); blog1.setId(100L); blog1.setTitle("Title-A"); blog1.setContent("Content-AAA"); //2.将Blog对象写入到redis //2.1序列化存取方案1: ValueOperations valueOperations = redisTemplate.opsForValue(); valueOperations.set("blog:100", blog1, Duration.ofSeconds(60)); Blog blog2=(Blog)valueOperations.get("blog:100"); System.out.println(blog2); //2.2序列化存取方案2: HashOperations hashOperations = redisTemplate.opsForHash(); //Map map=hashOperations.entries("blog:200"); //System.out.println(map); //反射存储(序列化) Class<?> clazz=blog1.getClass();//字节码对象(反射起点) Method[] methods=clazz.getDeclaredMethods();//获取类中所有方法 for(Method m:methods){ if(m.getName().startsWith("get")){//getId,getTitle String temp=m.getName().substring(3);//去掉get单词,Id,Title, hashOperations.put("blog:200", temp.substring(0,1).toLowerCase()//去掉get后的第一个字母小写 +temp.substring(1), m.invoke(blog1));// m.invoke(blog1)表示执行blog1的get方法 } } Map entries = hashOperations.entries("blog:200"); System.out.println(entries); } @Test void testHashOper01(){ //1.获取Hash类型操作对象 //也可以自己定义序列化方式 //redisTemplate.setKeySerializer(RedisSerializer.string()); //redisTemplate.setHashKeySerializer(RedisSerializer.string()); //redisTemplate.setHashValueSerializer(RedisSerializer.string()); HashOperations hashOperations = redisTemplate.opsForHash(); //2.读写数据 hashOperations.put("blog:1","id","1"); hashOperations.put("blog:1", "title", "hello redis"); Map entries = hashOperations.entries("blog:1"); System.out.println(entries); } @Test void testStringOper02(){ //1.修改key/value的序列化方式 redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setValueSerializer(RedisSerializer.string()); //2.获取redis操作对象 ValueOperations valueOperations = redisTemplate.opsForValue(); valueOperations.set("code:11", "ABCD",Duration.ofSeconds(60)); Object o = valueOperations.get("code:11"); System.out.println(o); } @Test void testStringOper01(){ //1.获取redis操作对象 ValueOperations valueOperations = redisTemplate.opsForValue(); //2.读写数据 valueOperations.set("x", 100);//采用jdk默认序列化规则对key/value进行序列化 //valueOperations.increment("x");不可以 Object x = valueOperations.get("x"); System.out.println(x); valueOperations.increment("y");//第一次执行这个操作时,要确保redis中没有y valueOperations.increment("y"); Long y = valueOperations.increment("y"); System.out.println(y);//3,6,9,.... //存储数据时,给定一个有效期. valueOperations.set("z", 200, Duration.ofSeconds(10)); } @Test void testGetConnection(){ RedisConnection connection = redisTemplate.getConnectionFactory() .getConnection(); String result = connection.ping(); System.out.println(result); } }
StringRedisTemplate 是一个特殊的RedisTemplate对象,默认基于字符串序列化方式存取数据
@SpringBootTest public class StringRedisTemplateTests { /** * StringRedisTemplate 继承RedisTemplate对象, * 此对象默认采用string类型进行序列化方式的数据操作 */ @Autowired private StringRedisTemplate stringRedisTemplate; @Test void testStringOper() throws JsonProcessingException { ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue(); valueOperations.set("x", "100"); valueOperations.increment("x"); String x = valueOperations.get("x"); System.out.println(x); //存储一个对象 Blog blog1=new Blog(); blog1.setId(100L); blog1.setTitle("Title-AA"); blog1.setContent("Content-AAA"); valueOperations.set("blog:100", //将对象转换为json格式字符串进行存储 new ObjectMapper().writeValueAsString(blog1)); //读取对象内容 String blogJsonStr=valueOperations.get("blog:100"); System.out.println(blogJsonStr); //将json字符串转换为Blog对象(ObjectMapper为jack中的api) Blog blog2 = new ObjectMapper().readValue(blogJsonStr, Blog.class); System.out.println(blog2); } }
基于业务定制RedisTemplate对象
参考renren.io,若依
简单定制
@Configuration public class RedisConfig { /** * 基于业务定制RedisTemplate对象 * @param connectionFactory * @return */ @Bean public RedisTemplate<Object, Object> simpleRedisTemplate( RedisConnectionFactory connectionFactory){ RedisTemplate<Object, Object> redisTemplate= new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); //设置序列化方式 redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); redisTemplate.setHashValueSerializer(RedisSerializer.string()); //redisTemplate.setValueSerializer(RedisSerializer.java()); redisTemplate.setValueSerializer(RedisSerializer.json()); return redisTemplate; } }
高度定制
@Configuration public class RedisConfig { public RedisSerializer<?> redisSerializer(){ //1.定义序列化对象,并指定可以对哪些类型对象进行序列化 Jackson2JsonRedisSerializer serializer= new Jackson2JsonRedisSerializer(Object.class); //2.构建对象映射对象(将对象转换为json或者将json转换对象) ObjectMapper objectMapper=new ObjectMapper(); //2.1设置序列化时的可见方法 objectMapper.setVisibility(PropertyAccessor.GETTER,//序列化时只调用get方法 JsonAutoDetect.Visibility.ANY);//get方法的访问修饰符可以任意 //2.2序列化时值的规则定义(例如值为null还是否要序列化) objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); //2.3激活类型存储(这个一般在反序列化时有用,否则反序列化时数据默认存储在LinkedHashMap中) objectMapper.activateDefaultTyping( objectMapper.getPolymorphicTypeValidator(),//开启多态校验 ObjectMapper.DefaultTyping.NON_FINAL,//序列化的来不能使用final修饰 JsonTypeInfo.As.PROPERTY);//将类型以属性形式存储到json串中 //2.4设置对象映射对象 serializer.setObjectMapper(objectMapper); return serializer; } @Bean public RedisTemplate<Object, Object> redisTemplate( RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); //设置序列化方式 redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); redisTemplate.setHashValueSerializer(redisSerializer()); redisTemplate.setValueSerializer(redisSerializer()); //建议更新了序列化方式后,执行一下如下方法 redisTemplate.afterPropertiesSet(); return redisTemplate; } }
/** * redis配置 * * @author ruoyi */ @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Bean @SuppressWarnings(value = { "unchecked", "rawtypes" }) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); serializer.setObjectMapper(mapper); // 使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); // Hash的key也采用StringRedisSerializer的序列化方式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } }
SpringBoot工程中Redis与Aop技术的整合
@Cacheable
从缓存中查询指定的key,如果有,从缓存中取,不再执行方法.如果没有,则执行方法,并且将方法的返回值和指定的key关联起来,放入到缓存中。
@Cacheput
和 @Cacheable 类似,执行方法,将方法的返回值覆盖到过去的缓存数据(修改缓存中指定key 的数据),主要用于数据新增和修改方法
@CacheEvict
方法执行成功后会从缓存中移除相应数据。
@CacheConfig
所有的@Cacheable()里面都有一个value=“xxx”的属性,这显然如果方法多了,写起来也是挺累的,如果可以一次性声明完 那就省事了,
所以,有了@CacheConfig这个配置,@CacheConfig is a class-level annotation that allows to share the cache names,如果你在你的方法写别的名字,那么依然以方法的名字为准。@CacheConfig("books") public class BookRepositoryImpl implements BookRepository { @Cacheable public Book findBook(ISBN isbn) {...} }
参数 解释 example value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 @CacheEvict(value=”my cache”) key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 @CacheEvict(value=”testcache”,key=”#userName”) condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @CacheEvict(value=”testcache”,condition=”#userName.length()>2”) allEntries 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 @CachEvict(value=”testcache”,allEntries=true) beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 @CachEvict(value=”testcache”,beforeInvocation=true)
//@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存; @Cacheable(value = "user", key = "#id", condition = "#id lt 10") public User conditionFindById(final Long id) //@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存; @CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'") public User conditionSave(final User user) //@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反) @CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'") public User conditionSave2(final User user) //@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存; @CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'") public User conditionDelete(final User user)
@Caching
有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id–>user;username—>user;email—>user的缓存;此时就需要@Caching组合多个注解标签了。@Caching(put = { @CachePut(value = "user", key = "#user.id"), @CachePut(value = "user", key = "#user.username"), @CachePut(value = "user", key = "#user.email") }) public User save(User user) {
自定义缓存注解
比如之前的那个@Caching组合,会让方法上的注解显得整个代码比较乱,此时可以使用自定义注解把这些注解组合到一个注解中,如:
@Caching(put = { @CachePut(value = "user", key = "#user.id"), @CachePut(value = "user", key = "#user.username"), @CachePut(value = "user", key = "#user.email") }) @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface UserSaveCache { }
这样我们在方法上使用如下代码即可,整个代码显得比较干净。
@UserSaveCache public User save(User user)
依赖及配置文件如下:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.2.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <!--假如项目中配置lettuce池对象,需要添加如下依赖--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> </dependencies>
spring: redis: host: 192.168.126.128 port: 6379 # redis: # cluster: #redis集群配置 # nodes: 192.168.126.128:8010,192.168.126.128:8011,192.168.126.128:8012,192.168.126.128:8013,192.168.126.128:8014,192.168.126.128:8015 # max-redirects: 3 # lettuce: #假如配置这个池,还需要额外添加依赖(commons-pool2) # pool: # max-active: 16 #最大活动连接数 # max-idle: 8 #最大空闲连接数 datasource: url: jdbc:mysql://localhost:3306/jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8 username: root password: root
@Service public class AopMenuServiceImpl implements MenuService { @Autowired private MenuMapper menuMapper; private static final Logger log= LoggerFactory.getLogger(AopMenuServiceImpl.class); /** * @Cacheable 注解描述的方法为缓存切入点方法, * 表示在执行这个方法时,先从缓存去取数据,缓存有则直接返回, * 缓存没有则执行数据库的查询操作 * 默认使用的是JDK的序列化方式 * 1)value属性的值一般会作为key前缀(建议写模块名) * 2)key属性的值一般会作为key的后缀(能够具备唯一性) * @param id * @return */ @Cacheable(value="menuCache",key = "#id")//menuCache::1 @Override public Menu selectById(Long id) { log.info("Get Data from MySql"); return menuMapper.selectById(id); } /** * @CachePut 注解描述的方法为缓存切入点方法, * 当执行完它描述的方法后,会将方法的返回值存储到cache * ,key由@CachePut注解指定,值为方法的返回值. * @param menu * @return */ @CachePut(value="menuCache",key = "#menu.id") @Override public Menu insertMenu(Menu menu) { menuMapper.insert(menu); return menu; } @CachePut(value="menuCache",key = "#menu.id") @Override public Menu updateMenu(Menu menu) { menuMapper.updateById(menu); return menu; } }
@EnableCaching 注解用于描述配置类或启动,告诉底层开启aop方式的缓存应用.
/** * @EnableCaching 注解用于描述配置类或启动, * 告诉底层开启aop方式的缓存应用. */ @EnableCaching @SpringBootApplication public class RedisApplication { public static void main(String[] args) { SpringApplication.run(RedisApplication.class, args); } }
默认Key的序列化方式是字符串的,value的序列化方式是JDK的,不满足业务需求时,可以对K和V的序列化方式进行配置
/** * 定义缓存配置类,在此配置类中 * 修改默认的缓存配置 */ @Configuration public class CacheManagerConfig { /** * 定制缓存管理器对象. * @param connectionFactory * @return */ @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory){ RedisCacheConfiguration redisCacheConfiguration= RedisCacheConfiguration.defaultCacheConfig() //指定key序列化方式 .serializeKeysWith(RedisSerializationContext .SerializationPair .fromSerializer(RedisSerializer.string())) //指定value的序列化方式 .serializeValuesWith(RedisSerializationContext .SerializationPair .fromSerializer(RedisSerializer.json())); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(redisCacheConfiguration).build(); } }
Redis 数据持久化
Redis中为了保证在系统宕机(类似进程被杀死)情况下,能更快的进行故障恢复,设计了两种数据持久化方案,分别为rdb和aof方式。
配置准备工作
https://redis.io/topics/config/该网址下载以下文件,替换原配置文件并进行修改,基于vim打开redis.conf文件,然后注释 bind 127.0.0.1这一行,并修改protected-mode的值修改为no.(java连接redis需要改这两项目)
Rdb方式持久化
Rdb方式是通过快照方式保存redis中key/value的一种机制,默认开启。
RDB方式的持久化是默认开启的,也可按规则自己配置,打开redis.conf文件进行配置
# 这里表示每隔60s,如果有超过1000个key发生了变更,那么就生成一个新的dump.rdb文件,就是当前redis内存中完整的数据快照,这个操作也被称之为snapshotting(快照)。 save 60 1000 # 持久化 rdb文件遇到问题时,主进程是否接受写入,yes 表示停止写入,如果是no 表示redis继续提供服务。 stop-writes-on-bgsave-error yes # 在进行快照镜像时,是否进行压缩。yes:压缩,但是需要一些cpu的消耗。no:不压缩,需要更多的磁盘空间。 rdbcompression yes # 一个CRC64的校验就被放在了文件末尾,当存储或者加载rbd文件的时候会有一个10%左右的性能下降,为了达到性能的最大化,你可以关掉这个配置项。 rdbchecksum yes # 快照的文件名 dbfilename dump.rdb # 存放快照的目录 dir /var/lib/redis
触发redis中rdb方式:
1)基于配置文件中的save规则周期性的执行持久化。
2)手动执行了shutdown操作会自动执行rdb方式的持久化。
3)手动调用了save或bgsave指令执行数据持久化。
4)在Master/Slave架构下,当有新的Slave连接Master时,Master会对数据进行rdb方式持久化.RDB持久化机制有哪些优点(占用空间很小, 性能较好)
RDB文件是经过压缩的二进制文件,占用空间很小,它保存了 Redis 某个时间点的数据集,很适合做冷备.
RDB 非常适用于灾难恢复,它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心。
RDB 方式持久化性能较好,执行持久化时可以fork 一个子进程,由子进程处理保存工作,父进程无须执行任何磁盘 I/O 操作。RDB持久化机制有哪些缺点(容易造成数据的丢失)
1.RDB方式在服务器故障时容易造成数据的丢失。实际项目中,我们可通过配置来控制持久化的频率。但是,如果频率太频繁,可能会对 Redis 性能产生影响。所以通常可能设置至少5分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失5分钟数据。
2.RDB 方式使用 fork 子进程进行数据的持久化,子进程的内存是在fork操作时父进程中数据快照的大小,如果数据快照比较大的话,fork 时开辟内存会比较耗时,同时这个fork是同步操作,所以,这个过程会导致父进程无法对外提供服务。3)RDB持久化过程中的fork操作,可能会导致内存占用加倍,Linux系统fork 子进程采用的是 copy-on-write 的方式(写时复制,修改前先复制),在 Redis 执行 RDB 持久化期间,如果 client 写入数据很频繁,那么将增加 Redis 占用的内存,最坏情况下,内存的占用将达到原先的2倍。
Aof方式数据持久化
Aof方式是通过记录写操作日志的方式,实现redis数据持久化的一种机制,这个机制默认是关闭的,采用的是写后日志(MySQL采用写前日志,可以执行事务回滚)。
如果Rdb和Aof都打开,先采用Rdb进行恢复,再采用Aof进行增量恢复打开redis.conf文件进行配置
# 是否开启AOF,默认关闭 appendonly yes # 指定 AOF 文件名 appendfilename appendonly.aof # Redis支持三种刷写模式: # appendfsync always #每次收到写命令就立即强制写入磁盘,类似MySQL的sync_binlog=1,是最安全的。但该模式下速度也是最慢的,一般不推荐使用。 appendfsync everysec #每秒钟强制写入磁盘一次,在性能和持久化方面做平衡,推荐该方式。 # appendfsync no #完全依赖OS的写入,一般为30秒左右一次,性能最好但是持久化最没有保证,不推荐。 #在日志重写时,不进行命令追加操作,而只是将其放在缓冲区里,避免与命令的追加造成DISK IO上的冲突。 #设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes no-appendfsync-on-rewrite yes #当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时,自动启动新的日志重写过程。 auto-aof-rewrite-percentage 100 #当前AOF文件启动新的日志重写过程的最小值,避免刚刚启动Reids时由于文件尺寸较小导致频繁的重写。 auto-aof-rewrite-min-size 64mb
AOF持久化机制有哪些优点?(数据更加可靠, 易修复,)
AOF 比 RDB更加可靠。你可以设置不同的 fsync 策略(no、everysec 和 always)。默认是 everysec,在这种配置下,redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据。
AOF文件是一个基于纯追加模式的日志文件。即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机等等), 我们也可以使用 redis-check-aof 工具也可以轻易地修复这种问题。
当 AOF文件太大时,Redis 会自动在后台进行重写。重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。整个重写是绝对安全,因为重写是在一个新的文件上进行,同时 Redis 会继续往旧的文件追加数据。当新文件重写完毕,Redis 会把新旧文件进行切换,然后开始把数据写到新文件上。
AOF 文件有序地保存了对数据库执行的所有写入操作,以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析也很轻松。如果你不小心执行了 FLUSHALL 命令把所有数据刷掉了,但只要 AOF 文件没有被重写,那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。AOF持久化机制有哪些缺点?(性能相对比较差)
对于相同的数据集,AOF 文件的大小一般会比 RDB 文件大。根据所使用的 fsync 策略(同步刷盘策略),AOF 的速度可能会比 RDB 慢。通常 fsync 设置为每秒一次就能获得比较高的性能,而关闭 fsync 可以让 AOF 的速度和 RDB 一样快。AOF 可能会因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。
Redis事务
MySQL采用的是悲观锁,Redis为了性能,采用了乐观锁方式进行事务控制,减少阻塞,减少对CPU的切换.
事务的四大特性:ACID
原子性: 没有回滚机制 一致性:从一个一致性状态切换到另一个一致性状态。 持久性 隔离性:事务之间必须相互隔离的,互不影响。
1、事务开始
multi命令的执行,multi命令会将客户端状态的 flags 属性中打开redis_multi 标识来完成的。
2、命令入队
如果客 户端发送的命令为MULTI、EXEC、WATCH、DISCARD中的一个,立即执行这个命令,否则将命令放入一 个事务队列里面,然后向客户端返回 QUEUED 回复
①如果客户端发送的命令为 exec、DISCARD、WATCH、MULTI 四个命令的其中一个,那么服务器 立即执行这个命令。
②如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令。
首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态的 flags 属 性关闭 REDIS_MULTI 标识,并且返回错误信息给客户端。
如果正确,将这个命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复事务队列是按照FIFO的方式保存入队的命令
3、事务执行
客户端发送 EXEC 命令,服务器执行 EXEC 命令逻辑。
如果客户端状态的 flags 属性不包含 REDIS_MULTI 标识,或者包含 REDIS_DIRTY_CAS 或者 REDIS_DIRTY_EXEC 标识,那么就直接取消事务的执行。
否则客户端处于事务状态(flags 有 REDIS_MULTI 标识),服务器会遍历客户端的事务队列,然 后执行事务队列中的所有命令,最后将返回结果全部返回给客户端;
redis 不支持事务回滚机制,但是它会检查每一个事务中的命令是否错误。Redis 事务不支持检查那些程序员自己逻辑错误。例如对 String 类型的数据库键执行对 HashMap 类型
的操作!
- multi 开启事务
- exec 提交事务
- discard 取消事务
- watch 监控,如果监控的值发生变化,则提交事务时会失败
- unwatch 去掉监控
Redis主从复制(读多写少 不支持高可用)
设计一主从架构,一个Master,两个Slave,其中Master负责Redis读写操作,并将数据同步到Slave,Slave只负责读.
启动Master再启动Slave,Master会将rdb文件同步给对应的Slave,当Master进行写操作时,会将对应的Aof文件同步给对应的Slave
Redis哨兵模式(观察者模式,高可用)
哨兵Sentinel是Redis主从架构模式下,实现高可用性的一种机制。
由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
Cluter模式:Cluster模式是用得比较多的模式,它支持多主多从,这种模式会按照key进行糟位的分配,可以使得不同的key分散到不同的主节点上,利用这种模式可以使得整个集群支持更大的数据容量,同时每个主节点可以拥有自己的多个从节点,如果该主节点宕机,会从它的从节点中选举一个新的主节点