JAVA关联学习(一)

    本着关联学习(问题关联什么学习什么)的原则,这一篇讲的是Redis。看了诸位大神的解释后详细的查了一些东西,记录下来,也感谢各位在网络上的分享!!!

    博客分享:

    https://www.jianshu.com/p/65765dd10671

    https://blog.csdn.net/u011692780/article/details/81213010

    1.什么是Redis

    Redis是一个完全开源的遵守BSD协议的高性能(NoSQL)的key-value型数据库。它使用C语言编写(所以也不需要其他编译器),是基于内存进行数据存储的一种数据库。在处理极大量的数据时,需要对数据库进行大量数据的读写操作,或者通过并发对数据库进行访问请求时,可以通过使用Redis来缓解数据库压力,避免服务器宕机的情况出现。Redis在使用中多用于使用缓存来快速提取数据,提供高速的读写能力等应用场景。

    2.什么是NoSQL数据库

    NoSQL数据库的出现是为了解决关系型数据库无法满足对于海量大数据的数据管理要求,并且无法满足数据的高并发高可用等特需求。我们平日中所接触的网络应用都是基于数据库的增删改查的过程,单体对于数据的访问次数在面对极大数据量的用户时就会成比例增长,那么对于常见的关系型数据库的访问压力就会非常大。并且NoSQL抛弃了关系型数据库的关系型特性。数据之间的关联关系的减少增强了数据的可拓展性。NoSQL数据库大体可以分为四类:

    (1).键值型数据库:例如Redis,使用哈希表,表中有一个特定的键和一个指针指向特定的数据,key-value键值对形式。通常使用内存进行缓存,并且具有哈希的查找速度快的特性。数据没有特定的结构,一般用来存储各种格式的字符串或二进制数据。

    (2).列存储数据库:例如HBase,通常用来应对分布式存储的海量数据,键仍然存在,但是指向了多个列,列由列簇进行管理。查找速度快,并且更容易进行分布式拓展。

    (3).文档型数据库:例如MangoDB,依然使用key-value比键值对形式,但是value为结构化的数据,如JSON格式的数据。文档型数据库可以看做是键值型数据库的升级,所以数据库查询效率较之键值型数据库也更高。

     (4).图形型数据库:例如Neo4J,使用图形模型来管理数据,利用图结构的关联算法进行数据查询等,但是对于大量数据的查询可能需要对整个图进行计算才能得出信息。

    NoSQL适用于:

    NoSQL的适用场景一般具有对数据库性能要求高,数据可以简单且灵活使用的特点,对于给定的key可以接受不同类型的复杂之的环境。

    3.为什么使用Redis

    (1).Redis通过RDB或AOF来支持数据持久化,可以将数据存储在内存中做缓存,也可以将内存中的数据存储在硬盘中,以便在重启服务器时可以再次加载使用。有很好的分布式集群方案和支持,也可以做消息队列。

    (2).Redis不仅仅支持简单的key-value类型数据,还提供list,set,zset,hash等数据结构存储。在数据存储类型上根据不同的应用场景可选择的类型丰富。

    (3).Redis能满足大数据量的性能问题,也能保证高并发情况下缓解数据库压力。

    (4).Redis是单线程的,能保证操作的原子性。

    4.Redis的缺点:

    由于Redis的数据是直接存储在内存中,所以需要定时快照(SnapShot)或者语句追加(AOF)来保证数据持久化。消耗内存,占用内存高。Redis由于是基于内存进行的存储,所以在操作上会更加关注内存大小,并且长时间的操作会大量消耗内存空间,所以需要定时的删除。

    Redis由于数据都存储在内存中,可以通过修改配置文件中的save配置项,该配置项规定Redis在多长时间内有多少次更新操作的情况下,生成RDB文件。以此来通过配置方式来避免断电等应急情况发生,另外还应该即时整理内存,否则会一直增加使得内存占满。Redis服务器针对过期键的删除策略主要有惰性删除和定期删除两种策略配合管理内存空间。惰性删除策略的出现是因为,在Redis数据库中每个键都有其对应的过期时间,若在创建时设定了创建时间,则可以通过对过期时间的检查来判断其是否过期,若过期则删除。定期删除策略则在数据库中的过期key中随机检查一部分key的过期时间,并删除其中的过期键。所以在使用时需要:

    1.设置超时时间(设定最大内存空间,建议不要超过1G,如maxmemory 1024MB)

    2.采用LRU(Least recently used,最近就少使用)算法动态删除key

    volatile-lru:在设定过期时间的key中,删除最近最少使用的key。

    allkeys-lru:查询所有的key,并删除最近最少使用的key,应用最广泛策略。

    volatile-random:在设定了过期时间的key中,随机删除某个key。

    allkeys-random:查询所有的key,随机删除。

    volatile-ttl:查询全部设定过期时间的key,之后排序,将最早要过期的key优先删除。

    Noeviction:如果设定该属性,则不会淘汰,在内存使用到阈值时,所有申请内存空间的命令都会报错返回。

    LFU(Least frequently used,最不常使用)算法动态删除key

    volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键。

    allkeys-lfu:从所有键中驱逐使用频率最少的键。

    5.Redis的常用通用命令:

    TYPE KEY:返回key所存储的值的类型。

    DEL KEY:删除已存在的key,若该key不存在则不进行操作。

    DUMP KEY:返回给定key的值的序列化,若该key不存在,则返回nil。

    EXPIRE KEY SECOND:为给定的key设置过期时间(若不设置,则默认永久保存),单位为秒(PEXPIRE KEY MILLISECOND:具有相同的功能,但是以毫秒为单位)。可以用在有限时操作的应用场景中,如限时优惠等。

    EXISTS KEY:用于检查给定的key是否存在。若key存在则返回1,否则返回0。

    TTL KEY:以秒为单位,返回给定key的剩余生命时间,若key存在但没设置过期时间,则返回-1。若key不存在,则返回-2。(PTTL KEY:具有相同的功能,但是以毫秒为单位)。

    PERSIST KEY:移除给定的key的过期时间,key将永久保持。

    KEYS PATTERN:寻找所有符合给定模式的key。pattern处可以使用通配符。

    RENAME OLD_KEY_NAME NEW_KEY_NAME:将给定key的名字进行修改。原有key被修改后,相关的过期时间会转移到新key上。

    在Redis中数据库并不具体包含一个名称,而是通过整数索引来进行标识。客户端默认连接到数据库0,也可以通过修改配置文件中的(database 16)对数据库数量进行控制,可见默认数据库数量为16个,标识为0~15。

    SELECT 数据库:切换数据库。

    MOVE KEY 数据库:将当前数据库中的指定KEY移动到指定数据库下。

    FLUSHDB:清除当前数据库下所有KEY。

    FLUSHALL:清除整个REDIS数据库所有KEY。

    6.Redis的数据类型及支持的命令:

    在Redis底层使用一个RedisObject对象来存储所有的key和value,该对象是在使用Redis进行数据存储时实际在内存中保存的对象。RedisObject的关键字段有数据类型(String,Hash,List,Set,Zset),编码方式(RAW,INT,HT,ZIPMAP,LINKEDLIST,ZIPLIST,INTSET,SKIPLIST,EMBSTER,QUICKLIST,STREAM),数据指针,虚拟内存等,下面按顺序学习一下Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合),Zset(有序集合)。

    1.String:

    String类型是二进制安全的(在数据传输时,保证信息安全,不被篡改破译,若被攻击,能够被即时检测,特点是编码解码都发生在客户端完成,执行效率高,不需要频繁解码,不会出现乱码),也就是说可以存储任何数据,如jpg图片或者序列化对象。也可以为了提高网站运行速度,将一些静态文件以String类型缓存在Redis中以供使用。String类型的value值最大可以容纳的数据长度是512MB。

    String命令:

    SET KEY_NAME VALUE:无关数据类型,并且key重复新值会覆盖旧值

    SETNX KEY VALUE(解决分布式锁的方案1):只有在key不存在时才能设置key的值,若存在则不作操作。

    GET KEY_NAME:取值。若key不存在,则返回nil。如果key存储的不是string类型,返回一个错误。

    GERANGE KEY START END:根据start和end来取得key值中的value的对应部分。

    GETSET KEY VALUE:若key存在则返回对应的值,若key不存在则返回nil并set一个key值为value,下次使用时便存在

    INCR KEY_NAME:将key中存储的数字值增加1。若key不存在,则会先被初始化为0,而后再执行incr操作。(incrby key_name能够自己设定步长)

    DECR KEY_NAME:将key中存储的数字值减少1。(decrby key_name能够自己设定步长)

    APPEND KEY_NAME VALUE:用于为指定的key追加字符串到末尾,若该key不存在则为其赋值。

    String应用场景:

    1.用来存储单个字符串或JSON字符串数据。在底层为关系型数据库作为存储层时,使用Redis作为缓存,进行数据预热后大量的数据便可以通过Redis快速的获得响应。可以通过降低数据库层面的读写操作次数以降低数据库压力。

    2.二进制安全,可以用来存放图片文件内容,方便Web应用快速调用。

    3.计数器:常用key-value缓存应用,记录如微博数粉丝数等会及时更改的数据,统计计数,等待时机一起更新到数据库中。使用incr,decr等操作方便数值增减。当使用incr,decr等操作时,RedisObject对象引用的字符串就会转为数值型进行计算,并且此时字段编码为int。且incr,decr等指令本身就具有原子性,不需要考虑线程安全。

    4.session共享:通过设置key及key的过期时间来存储多应用的共享session。每次获取便可以直接从Redis缓存中直接获取。

    2.Hash:

    Hash类似于JAVA BEAN,是一个String类型的field和value的映射表,特别适合用于存储对象。每一个hash中可以存储2^32-1个键值对,可以看成是key和value的map容器,非常适合于存储值对象信息,该类型数据仅占用很少的磁盘空间(相比于JSON)。存储类型格式如Users(id,name,age,remark)

    Hash命令:

    HSET KEY FIELD VALUE:将哈希表key中的域field值设置为value。HSET存储操作 KEY对象名 FIELD属性名 VALUE值。(如:hset user username "Mike")如果key不存在,则一个新的哈希表会被创建并进行该操作,若该field已经存在,则旧值会被覆盖,虽然返回值是0,但是值已经被覆盖。

    HMSET KEY FIELD VALUE [FIELD1,VALUE1]:同时将多个field-value(域-值)对设置在哈希表key中,属性和值每组之间用空格间隔。

    HGET KEY FIELD:返回哈希表key中给定域field的值,当给定值不存在或key不存在时返回nil。

    HMGET KEY FIELD:返回哈希表key中所有给定的域的对应值,若给定的域不存在则会返回nil。

127.0.0.1:6379> hmget user username age
1) "Vic"
2) (nil)

    HGETALL KEY:以列表形式返回哈希表中所有的域field和值value。若哈希表key不存在则返回空列表。

127.0.0.1:6379> hgetall user1
(empty list or set)

    HLEN KEY:返回哈希表key中域数量。当哈希表key不存在时返回0。

    HDEL KEY FIELD:删除哈希表key指定的一个或多个域值,返回被成功移出的域的数量。也可使用公共命令del直接删除整个哈希表key。

    HSETNX KEY FIELD VALUE:当且仅当域field不存在时,才将哈希表key中的域field值设置为value,若域field已经存在则该操作无效。

    HINCRBY KEY FIELD INCREMENT:为哈希表key中指定域field的value值加上步长。增量也可为负数进行减法操作。尽可以在存储数值型域中执行该操作,否则会返回一个错误。

    HEXISTS KEY VALUE:查看哈希表key中,指定域field是否存在。若不存在或不被包含则返回0。

    Hash应用场景:

    1.通过对对象及属性的存储更加直观和方便的获取value值。

    2.常用于存储对象信息(为什么不用String存储一个对象?)Hash是最接近关系型数据库结构的数据结构,可以将数据库一条记录或程序中一个对象转换成HashMap直接存放在Redis中存储。若用普通的key/value形式存储,主要有两种存储方式,(key:id,value:JSON数据)用户的id设为查找的key,其他信息封装成一个对象以序列化的方式存储,缺点就在于增加了序列化/反序列化的开销,并且在需要修改其中一项信息便将整个对象取出,会有CAS操作并发问题,即转换对象和修改值问题。或者有多少成员就存储多少key/value键值对,用用户名称+用户id(如:username:1)作为唯一标识,虽然省去了序列化开销和并发问题,但是属性越多,数据越多,就会容易出现内存不足问题。所以使用Hash存储来解决这个问题。

    3.List:

    Redis列表是简单的字符串列表,按照插入顺序排序。是一个双向链表结构,所以在头尾元素操作上效率非常高。最多可以包含2^32-1个元素,类似于java中的LinkedList。

    List指令:

    LPUSH KEY VALUE:将一个或多个值插入到列表的头部(左侧)。若有多个值则是从左到右按顺序插入。类似于栈,最左边的值将先被压入队列中。

    RPUSH KEY VALUE:将一个或多个值插入到列表的尾部(右侧)。

    LPUSHX KEY VALUE:当且仅当列表key存在且是一个列表(空列表也不行),才将一个值插入到已存在的列表头部,若列表不存在,则操作无效

    RPUSHX KEY VALUE:当且仅当列表key存在且是一个列表(空列表也不行),才将一个值插入到已存在的列表尾部,若列表不存在,则操作无效

redis> LLEN emptyList
(integer) 0
redis> LPUSHX emptyList "a"
(integer) 0

    LLEN KEY:获取列表key的长度,若key不存在,则该key被解释为是一个空列表。

    LINDEX KEY INDEX:通过索引获取列表key元素。类似于数组下标,也可以使用负数下标表示反向获取,即获取排序倒数的元素。如-1表示最后一个元素。若index参数的值超出了列表key的范围,则返回nil。

    LRANGE KEY START END:返回指定范围内的列表key元素(0表示第一个元素,-1表示最后一个元素)

    LPOP KEY:移除并获取列表的第一个元素(左侧),当列表key不存在时,返回nil。

    RPOP KEY:移除并获取列表的第一个元素(右侧),当列表key不存在时,返回nil。

    BLPOP KEY TIMEOUT:阻塞式弹出,移除并获取列表的第一个元素(左侧),如果列表没有元素会阻塞列表直到等待超时或者发现可被弹出的元素为止,否则弹出nil。

    BRPOP KEY TIMEOUT:阻塞式弹出,移除并获取列表的第一个元素(右侧),如果列表没有元素会阻塞列表直到等待超时或者发现可被弹出的元素为止,否则弹出nil。

    LTRIM KEY START END:对一个列表进行截取,不在范围内的元素都将被删除(包含边界元素)。

    LSET KEY INDEX VALUE:通过索引设置列表key下标为index的元素value值。对头元素或尾元素操作很快。

    LINSERT KEY BEFORE|AFTER WORD VALUE:在列表key的word元素前|后插入元素,若元素不存在在key中或key不存在时均不进行任何操作。

    RPOPLPUSH SOURCE DESTINATION:移除列表最后一个元素,并且将该元素添加到另一个列表并返回,也可以当做是将循环列表中的最后元素移到最左侧(两个列表变量均为一个即可)

    BRPOPLPUSH SOURCE DESTINATION TIMEOUT:从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回,如果列表中没有元素会阻塞列表直到等待超时或发现可弹出元素。

    List应用场景:

    1.对数据量大的集合数据删减(如:列表数据显示,关注列表,粉丝列表,留言),可以通过lrange命令很好的实现分页功能

    2.可以组合命令以实现队列模型,如使用lpush和brpop实现消息队列模型,使用lpush和lpop实现栈模型等。

    4.Set:

    Set类型是String类型的无序集合。集合元素是唯一的,不允许重复。Redis中的集合是通过哈希表实现的,所以添加删除查找复杂度都是O(1)。类似于HashTable。

    Set命令:

    SADD KEY MEMBER:向集合添加一个或多个成员。

    SCARD KEY:获取集合成员数,集合长度。

    SMEMBERS KEY:返回集合中的所有成员

    SISMEMBER KEY MEMBER:判断member元素是否是集合key的成员。

    SRANDMEMBER KEY:返回集合中一个或多个随机成员。

    SREM KEY MEMBER:移除集合中一个或多个成员,返回被成功移除的成员数量。

    SPOP KEY:移除并返回集合中的一个随机元素。

    SMOVE SOURCE DESTINATION MEMBER:将member元素从SOURCE集合移动到DESTINATION,若该元素不存在则不执行任何操作。

    SDIFF KEY1 KEY2:返回给定的所有集合的差集(以左侧数据为根据)。

    SDIFFSTORE DESTINATION KEY1:返回给定的所有集合的差集并存储在DESTINATION中。

    SINTER KEY1 KEY2:返回给定的所有集合的交集。

    SINTERSTORE DESTINATION KEY1:返回给定的所有集合的交集并存储在DESTINATION中。

    SUNION KEY1 KEY2:返回给定的所有集合的并集。

    SUNIONSTORE DESTINATION KEY1:返回给定的所有集合的并集并存储在destination中。

    Set应用场景:

    1.通过集合操作返回共同关注,共同喜好。

    2.利用唯一性,可以统计访问网站的所有独立IP,唯一性验证。

    5.Zset:

    Zset类型是String类型的有序集合。每个元素都会关联一个double类型的分数,通过分数来为集合中的元素排序,有序集合中的元素还是不可以重复,但是分数是可以重复的。

    Zset命令:

    ZADD KEY SCORE MEMBER:向有序集合添加一个或多个成员,或者更新已存在的成员的分数。

    ZCARD KEY:获取有序集合的成员数,集合长度。

    ZCOUNT KEY MIN MAX:计算在有序集合中指定区间分数的成员数。

    ZRANK KEY MEMBER:返回有序集合中指定成员的索引。

    ZRANGE KEY START END:通过索引区间返回有序集合或指定区间内的成员(低到高)。

    ZREVRANGE KEY START END:返回有序集合中指定区间的成员,通过索引,分数从高到低。

    ZREM KEY MEMBER:移除有序集合中的一个或多个成员。

    ZREMRANGEBYRANK KEY START END:移除有序集合中给定的排名区间的所有成员。

    ZREMRANGEBYSCORE KEY START END:移除有序集合中给定的分数区间的所有成员(包含边界值)。

    Zset应用场景:

    1.排行榜。https://www.jianshu.com/p/6d1991fef1d7

    2.权重队列。http://doc.redisfans.com/sorted_set/zadd.html

    7.普通JAVA连接Redis方式:

    在使用普通的方式进行Redis连接时,需要引入Jedis依赖。我省略了get/set方法。

package com.day_8.excercise_1;

public class Person {

	private Integer id;
	private String name;
	private Integer age;
	private String sex;
	......
}
package com.day_8.excercise_1;

import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import redis.clients.jedis.Jedis;
public class TryConnectRedis1 {
	
	public static void main(String[] args) {
		Jedis jedis = new Jedis("127.0.0.1",6379);// 默认就是127.0.0.1:6379
		//若有密码则可以通过auth进行验证登录,默认无密码
//		jedis.auth("redis");
		System.out.println(jedis.ping());
	}
	@Test
	public void setValueAndGet() {
		Jedis jedis = new Jedis("127.0.0.1",6379);
//		jedis.auth("redis");
		jedis.set("TryConnect","20191222");
		String getString = jedis.get("TryConnect");
		System.out.println(getString);
		jedis.close();
	}	
	/*
	 * 可以通过对数据库查询和Redis查询的切换来缓存机制缓解数据库查询压力
	 */
	@Test
	public void getValueWithExist() {
		Jedis jedis = new Jedis("127.0.0.1",6379);
//		jedis.auth("redis");
		String keyName = "TryConnect";
		if (jedis.exists(keyName)) {
			String keyValue = jedis.get(keyName);
			System.out.println("Redis查询:"+keyValue);
		}else {
			String keyValue = "nope";
			jedis.set(keyName, keyValue);
			System.out.println("MySQL查询:"+keyValue);
		}
		jedis.close();
	}
}

    也可以通过使用Jedis内提供的JedisPool连接池来管理对Redis的连接。

package com.day_8.excercise_1;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisPoolUtils {
	private static JedisPool pool;
	static {
		String host = "127.0.0.1";
		Integer port = 6379;
		// Redis连接池基本配置信息
		JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
		jedisPoolConfig.setMaxIdle(1);
		// Redis连接池
		pool = new JedisPool(jedisPoolConfig,host,port);
	}
	public static Jedis getJedis() {
		Jedis jedis = pool.getResource();
//		jedis.auth("redis");
		return jedis;
	}
	public static void close(Jedis jedis) {
		jedis.close();
	}
}
package com.day_8.excercise_1;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class TryConnectRedis2 {

	public static void main(String[] args) {
		Jedis jedisFromUtils = RedisPoolUtils.getJedis();
		System.out.println(jedisFromUtils.get("TryConnect"));
		RedisPoolUtils.close(jedisFromUtils);
		
	}
	
}

    8.SpringDataRedis:

    更多时间是在使用Spring连接Redis,一般不会出现自己连接Redis的场景,而在使用Spring连接时,可以使用Spring-data-redis,其中提供了很多在spring应用中配置访问Redis服务的工具,并对redis底层开发包(Jedis,JRedis,RJC)进行了高度封装。提供的RedisTemplate提供了对Redis的各种操作,异常处理和默认的序列化。

    先实现一个简单的SpringDataRedis的一个Demo:

    (1).构建 Maven 工程,并引入 JUnit 依赖,引入 Jedis 依赖和 SpringDataRedis 依赖。与此同时要导入commons-pool2依赖,因为在使用JedisPoolConfig时需要对连接池进行设置,若没有导入则没有set方法存在。我这里贴出我Demo中全部,应该可以拿来就用。

<dependencies>

	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.12</version>
		<scope>test</scope>
	</dependency>

	<!-- JACKSON -->
	<dependency>
		<groupId>com.fasterxml.jackson.core</groupId>
		<artifactId>jackson-core</artifactId>
		<version>2.8.9</version>
	</dependency>

	<dependency>
		<groupId>com.fasterxml.jackson.core</groupId>
		<artifactId>jackson-annotations</artifactId>
		<version>2.9.0</version>
	</dependency>

	<dependency>
		<groupId>com.fasterxml.jackson.core</groupId>
		<artifactId>jackson-databind</artifactId>
		<version>2.8.9</version>
	</dependency>

	<!-- <dependency>
		<groupId>org.springframework.data</groupId>
		<artifactId>spring-data-redis</artifactId>
		<version>2.2.0.RELEASE</version>
	</dependency> -->
	

	<!-- JEDIS -->
	<dependency>
		<groupId>redis.clients</groupId>
		<artifactId>jedis</artifactId>
		<version>2.9.0</version>
	</dependency>
	
	<!-- Spring Boot Cache -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-cache</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-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-devtools</artifactId>
		<scope>runtime</scope>
		<optional>true</optional>
	</dependency>
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<optional>true</optional>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
	</dependency>
</dependencies>

    此时会注意到我使用的是Spring-boot-starter-data-Redis,而不是网络上Redis项目导入更多的Spring-data-redis。这里我也仔细的去看了一下,原来在前者的pom.xml文件中也存在Spring-data-redis,也就是说前者是包含后者的。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"...">
	......
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
		</dependency>
	</dependencies>
	......
</project>

    (2).在application.properties文件中进行对Redis的配置,在配置文件中可以配置很多基本Redis的适应性设置。

# Redis数据库索引
spring.redis.database=0  
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379  
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数
spring.redis.pool.max-active=8  
# 连接池最大阻塞等待时间
spring.redis.pool.max-wait=-1  
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8  
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0  
# 连接超时时间(毫秒)
spring.redis.timeout=0  

    (3).创建RedisConfig配置

package com.SpringRedis.api;

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.*;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);

        //由于key值一般直接为字符串,故直接使用StringRedisSerializer进行序列化与反序列化key值
        //1.
        //使用Jackson2JsonRedisSerializer来序列化和反序列化value值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new Jackson2JsonRedisSerializer(Object.class));
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new Jackson2JsonRedisSerializer(Object.class));

        //2.
//        //使用GenericJackson2JsonRedisSerializer来序列化和反序列化value值
//        template.setKeySerializer(new StringRedisSerializer());
//        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
//        template.setHashKeySerializer(new StringRedisSerializer());
//        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        // 初始化RedisTemplate
        template.afterPropertiesSet();

        return template;
    }

    @Bean
    public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }
    @Bean
    public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForValue();
    }
    @Bean
    public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }
    @Bean
    public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }
    @Bean
    public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }
}

    在该配置文件中对RedisTemplate进行了一些设置的初始化,并在序列化的选择上出现了可选项,RedisTemplate默认使用的是JdkSerializationRedisSerializer。使用默认设置则被序列化的对象必须实现Serializable接口,并且在存储时可读性差,内容冗长。

public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
	......
	private RedisSerializer keySerializer = null;
	private RedisSerializer valueSerializer = null;
	private RedisSerializer hashKeySerializer = null;
	private RedisSerializer hashValueSerializer = null;
	private RedisSerializer<String> stringSerializer = new StringRedisSerializer();
	......
	public void afterPropertiesSet() {
		super.afterPropertiesSet();
		boolean defaultUsed = false;
		// 若没有设置统一的默认序列化对象,则使用JdkSerializationRedisSerializer
		if (defaultSerializer == null) {
			defaultSerializer = new JdkSerializationRedisSerializer(
					classLoader != null ? classLoader : this.getClass().getClassLoader());
		}
		if (enableDefaultSerializer) {
			if (keySerializer == null) {
				keySerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (valueSerializer == null) {
				valueSerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (hashKeySerializer == null) {
				hashKeySerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (hashValueSerializer == null) {
				hashValueSerializer = defaultSerializer;
				defaultUsed = true;
			}
		}
	......
	}
	......
}

    所以我们可以根据需要灵活地修改序列化的设置。如使用StringRedisTemplate使用的序列化方式StringRedisSerializer,Jackson2JsonRedisSerializer或者GenericJackson2JsonRedisSerializer等。在使用后两者时,返回值略有不同。上方是使用了Jackson2JsonRedisSerializer下方是使用了GenericJackson2JsonRedisSerializer。前者需要在使用时传入一个序列化对象Class,我们可以统一使用Object.class。而使用GenericJackson2JsonRedisSerializer则会结果信息中存储一个该对象的class信息。但是通过在网上的了解,貌似都在某些场景中存在问题,具体问题点等我测试出来再看哈。

127.0.0.1:6379> get UserInfo
"{\"name\":\"Vic\",\"sex\":\"\xe7\x94\xb7\",\"age\":23}"
127.0.0.1:6379> get UserInfo
"{\"@class\":\"com.SpringRedis.api.UserInfo\",\"name\":\"Vic\",\"sex\":\"\xe7\x94\xb7\",\"age\":23}"

    (4).进行测试。

    在有了上述的配置后一般的场景就可以进行简单的测试了,但是一般都会通过RedisTemplate来进一步实现一些命令的封装以形成工具类,工具类网上还是有很多的,就不在这里贴出来了,这个测试中不会出现工具类内内容,也请放心测试简单场景。同样省略了get/set方法。

package com.SpringRedis.api;

public class UserInfo {
    private String name;
    private String sex;
    private Integer age;
	......
}
package com.SpringRedis.api;

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.*;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestRedis {
    @Resource
    private ValueOperations<String,Object> valueOperations;
    @Test
    public void testValueOption( )throws  Exception{
        UserInfo userInfo = new UserInfo();
        userInfo.setSex("男");
        userInfo.setName("Vic");
        userInfo.setAge(23);
        valueOperations.set("UserInfo",userInfo);

        System.out.println(valueOperations.get("UserInfo"));
    }
}

    9.Redis订阅与发布:

    Redis通过publish/subscribe等命令来实现一种消息通信模式,即发布者发送消息,接受者接收消息。在这种模式中,客户端可以订阅任意数量的频道,并且每次发布者发布新的消息时,所有的订阅者均能获取到该信息。

    Redis订阅与发布相关命令:

    订阅频道

    SUBSCRIBE CHANNEL:订阅给定的一个或多个频道的信息。

    PSUBSCRIBE PATTERN:订阅一个或多个符合给定模式的频道。

    发布频道:

    PUBLISH CHANNEL MESSAGE:将信息发送到指定的频道。

    退订频道:

    UNSUBSCRIBE CHANNEL:退订指定的频道。

    PUNSUBSCRIBE CHANNEL:退订所有给定模式的频道。

    在Redis底层拥有两个结构体,redisClient和redisServer,均拥有一个字典类型的pubsub_channels属性。在服务端用来保存订阅频道的信息。在该字典中,键便为正在被订阅的频道,值为一个链表,链表中保存了所有订阅该频道的客户端。故订阅的操作实质上就是将客户端加入到服务器的该pubsub_channels字典对应频道的链表中,并且在客户端的频道订阅字典pubsub_channels中加入订阅的频道。退订操作正相反。

    10.Redis事务:

    Redis事务实际上就是一组命令的集合。在事务中可以一次执行多个命令,按顺序的串行化执行,事务中所有的命令都会被序列化。并且不会受其他客户端提交的命令的影响,这里的意思是一个事务执行过程中不会被其他命令插入其中。事务的三个阶段就是开始事务——命令入队——执行事务。

    Redis事务命令:

    DISCARD:取消事务,放弃执行事务块中的所有命令。

    EXEC:执行所有事务块内的命令。

    MULTI:标记一个事务块的开始。

    UNWATCH:取消WATCH命令对KEY的监视,若在执行WATCH命令后,EXEC命令或DISCARD命令先被执行的话,就不需要再执行。

    WATCH KEY:监视一个或多个KEY,如果在事务执行之前这个(或这些)KEY被其他命令改动,那么事务被打断。WATCH的声明周期在事务执行后结束。

    Redis事务到底能不能保证原子性?

    有关于这个问题我查了网上的挺多的资料,但是由于毕竟网络我也没有找到一个官方地方给出具体的解释。问题关注的点在于原子性的定义。在百度百科上对于数据库事务的原子性的定义是“原子性(Atomicity):事务中的全部操作在数据库中是不可分割的,要么全部完成,要么全部不执行。”。就这一点看来的话,Redis的事务是具备原子性的,但是从某些其他地方找到的关于原子性的定义则会多两句,如“原子性( “ACID” 特性)声明,对于一系列的数据库操作,要么所有操作一起提交,要么全部回滚;不允许中间状态存在。”,也就是“回滚”两个字。在Redis中是不存在回滚的,并且在Redis事务中任意命令执行失败,其余命令仍会被执行。如下,我给定了一个TEST的key值为k1,很明显不能对该key使用incr命令。而后我书写了一个事务,由结果可知所有的key/value的设定都是执行了的,而实际上“incr TEST”语句是报错的。当然若我将某一个给定key/value的设定放在incr语句之前,也是可以执行的,也就说明Redis事务也没有“回滚”。如果以后看到了更加清晰明确的解释我会再声明,也希望有关于这个问题的有理有据的解释能够回复给我,多谢!

    Redis事务的错误处理:

    1.如果在事务中执行某个命令报错,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

    2.如果在事务队列中的某个命令出现了命令性错误,执行时整个的所有队列都会被取消。实质上这个情况就是在书写事务的时候没有得到正确的命令入队(QUEUED)的反馈,而是出现了“error”字样。

package com.day_8.excercise_1;

import com.alibaba.fastjson.JSONObject;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class TryTransaction {
	public static void main(String args[]) {
		Jedis jedis = new Jedis("127.0.0.1", 6379);
		String result = "test";
		// 开启事务
		Transaction multi = jedis.multi();
		try{
			multi.set("test1".getBytes(), result.getBytes());
			multi.set("test2".getBytes(), result.getBytes());
			// 这里引发了异常,用0作为被除数
			//1.
//			int i = 1 / 0;
			multi.exec();
		}catch(Exception e){
			e.printStackTrace();
			multi.discard();
		}finally{
			jedis.close();
		}
	}
}

    当不使用场景1时,则可以直接设定该key/value。当使用场景1时,则会引发异常,当然key/value的设定也没有执行。

    11.Redis持久化:

    Redis为持久化提供了两种方式,即RDB和AOF。

    RDB:RDB是Redis的默认持久化机制,相当于是快照。指在指定时间间隔内将内存中的数据集全部写入磁盘中,并且是重新记录整个数据集的所有信息。由于RDB能将内存中的数据以快照的方式写入到二进制文件中,所以快照保存数据极快,同样恢复数据也极快,适用于容灾备份。配置文件的默认RDB策略是每隔900秒,在这期间至少变化了一个键值就做快照;每隔300秒,至少变化了10个键值就做快照;每隔60秒,至少变化了10000个键值就做快照。除了配置文件的默认策略,还可以使用手动持久化命令SAVE和BGSAVE,前者会阻塞Redis服务器,直至持久化完成;后者会调用Fork产生一个子进程,则不会影响整个服务器,但是由于子进程在进行数据快照,所以在子进程运行期间的数据不会进行持久化,而是被复制一份等待进入共享内存,而在这期间若发生宕机等情况,则会丢失一部分数据。

    AOF:由于快照方式是在一定间隔时间做一次,而一旦宕机rdb文件内存储的会就是最后一次快照后的所有操作。所以如果系统要求不能丢失任何修改的话,应该使用AOF持久化方式。在使用AOF持久化操作时,Redis会将每一个收到的写命令都通过write函数追加到aof文件中。当Redis服务器重启时则会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。在配置AOF的持久化策略上,Redis默认使用everysec,也就是每一秒持久化一次,也提供always(每次操作都立即写入)和no(不注定进行同步操作,默认30s一次)。也就是说当宕机或者误操作时,只需要重启服务器,或者在aof文件中移除尾部的误操作,便可以重新恢复数据。

    RDB持久化机制不适合小内存机器使用,因为RDB机制符合要求就会照快照,并且非常消耗CPU也无法进行如AOF的秒级别的持久化操作。但是整个数据库只会包含一个文件,方便备份,且RDB在恢复大数据集时速度比AOF快。而AOF除了回复速度较慢外,整个AOF文件的体积较之RDB会大。但是在使用AOF时仍可以保证Redis性能和数据安全,且AOF文件较为容易恢复和更改。

    不过Redis也支持同时开启 RDB 和 AOF两种策略,而在Redis服务器重启后,会优先使用 AOF来恢复数据。

    12.Redis缓存和数据库的一致性

    使用缓存获取数据是提升应用性能的常见手段,但是也会面临需要解决的问题,如数据库数据和缓存中数据的一致性问题。要想保证数据一致性则可以:

    1.实时同步:

    对强一致性要求比较高的,应采用实时同步机制。但是实时同步也存在问题。我们在读取信息时,可以先查询缓存内信息,若缓存查询不到再从数据库中查询,而后保存到缓存中。但是更新数据时则会出现一系列的问题,如:

    (1).数据库/缓存更新成功,缓存/数据库更新失败。

    (2).数据库更新成功,清除缓存失败。

    (3).清除缓存成功,更新数据库失败。

    出现上述问题的主要原因就是,在多线程操作中会存在各种原因导致的顺序问题,从而就会影响数据质量,形成脏数据。推荐使用更新缓存时,先更新数据库,再将缓存的设置过期。也可以使用锁机制保证数据运行过程,但是会相应的增大一些压力。

    在使用SpringBoot进行开发时也可以使用注解方式来保证多线程并发访问,将数据库压力转移给Redis。

    @Cacheable:在执行该注解标注的方法前会先从缓存中查询是否有数据存在,若存在则直接返回缓存内数据,若不存在则执行该方法并将返回值放入内存中。

    @CachePut:无论数据在缓存中是否存在都会执行该注解标注方法,并且将方法返回值记录在缓存中,实现缓存与数据库的同步更新。

    @CacheEvict:方法执行成功后会从缓存中移除满足条件的缓存数据。

    @Caching:组合多个Cache注解使用。

    2.异步队列:对于并发程度高的程序,可以采用异步队列的方式同步,可采用Kafka等消息中间件处理消息的生产和消费。也就是说先更新数据库数据,而后将要删除或要更新的key发送至消息队列中,程序本身先消费该消息,而后再不断重复尝试原操作直至成功。

    3.同步工具:canal是阿里巴巴的一个开源项目,通过监听mysql的binlog日志获取数据,能够高性能的获取mysql数据库的数据变更,而后形成主从复制读写分离。

    4.利用UDF自定义函数的方式:通过书写mysql的UDF函数使mysql操作后主动刷新,回写Redis缓存。    

    13.使用Redis缓存会存在的问题:

    缓存穿透:指查询一个数据库内一定不存在的数据,由于缓存时不命中时需要从数据库中查询,但是每次都查不到数据则也就不会写入缓存,从而导致这个不存在的数据每次请求都要求数据库中查询。

    解决办法:持久层查询不到就缓存空结果(如空""),查询时先判断缓存中是否exists(key),如果有则直接返回空,没有则查询后返回。

    缓存雪崩:缓存集中在一段时间内失效,会发生大量的缓存穿透,而导致所有的查询都落在数据库上,造成了缓存雪崩。

    解决办法:没有完美解决办法,但是可以尽量让失效时间分布均匀。

    缓存击穿:指有一个key被经常访问使用,称为热点key。在高并发的访问下,一旦key失效,所有的并发请求就会直接落在数据库上,形成缓存击穿。

    解决办法:可以设置热点key为永不过期。

    14.Redis高并发问题

    针对Redis的高并发可能会出现的单台Redis可能的单点故障问题,请求负载压力过大问题,内存容量限制问题都可以通过垂直拓展和水平拓展的方式来相应的解决。垂直拓展就是提升单台Redis服务器的性能,但是这种方式毕竟还是有着很容易达到的上限,所以最好的方式就是使用水平拓展,增加Redis服务器的数量,减轻每台Redis服务器的压力。

    主从复制,读写分离:由于在应用场景中,一般写少读多,所以在主从复制上,一般主服务器只负责写的请求,从服务器负责读的请求。这样做不仅可以提高服务器负载能力,也可以根据读请求的规模自由增加或减少从库的数量。此外,由于使用了主从复制,数据也会被复制多份,就算有一台机器出现故障也可以使用其他机器进行数据恢复。

    15.Redis集群

    同一些其他数据库相同,Redis也存在集群配置,在解决高并发时的水平拓展中也提到了。

    1.主从模式:

    通过在从节点的配置文件中设置slaveof+ip指定master节点的ip和端口号即可。主从模式可以备份数据,同时也可以降低某一个节点的压力,达到负载均衡。默认配置中,主从模式就遵循着读写分离,主节点可读可写,从节点只能进行读操作。主节点修改数据后从节点的数据会被覆盖,从节点宕机不会影响其他节点读写操作,并且重启后还会从其对应的master节点同步。主节点宕机后不影响读操作,但是不能提供写操作了。

    2.哨兵模式

    哨兵模式是建立在主从模式的基础上,用来解决master节点宕机后slave节点无法成为master节点的问题,那么便通过设置哨兵模式设置另一个节点成为哨兵,在master节点宕机后重新选举一个master。在master节点宕机后,哨兵模式将在slave中选择一个作为master并修改配置文件。一个sentinel或sentinel集群可以管理多个主从Redis。当使用哨兵模式时最好不要直接连接Redis服务器,而是连接哨兵节点,以防止master节点宕机产生问题。

    3.集群模式

    哨兵模式下虽然保证了高可用和读写分离,但是每台Redis服务器都存储相同的数据浪费空间,故可以使用集群模式。使用集群模式再每台Redis节点上存储着不同的内容。Redis集群同Zookeeper等相同,存在投票机制,所以在集群中要有2n+1个节点,至少要有3个master节点,并且每个节点至少有1个slave节点,才能建立集群,采用无中心结构,每个节点保存数据和整个集群状态,每个节点都有和其他节点的连接。

    集群投票:投票过程是集群中所有master节点参与,如果半数以上master节点与master节点通信超时,则认为当前master节点宕机。如果集群任意master宕机,且当前master没有slave,集群进入fail状态。如果集群超过半数以上的master节点宕机,无论有没有slave节点,集群进入fail状态。

    redis集群节点分配:采用哈希槽(hashslot)的方式来分配16384个插槽(slot),当要存取操作时Redis会根据算法将结果与插槽相对应,而后直接跳转到该节点进行存取操作。并且每增加一个节点,都是在之前创建的各个节点的前面各取一部分插槽作为分配给新节点的插槽。如下:

    节点A:0-5460

    节点B:5461-10922

    节点C:10923-16383

    新增节点D

    节点A:1365-5460

    节点B:6827-10922

    节点C:12288-16383

    节点D:0-1364,5461-6826,10923-12287

    针对Redis的部分知识都了解了一通,比较浅显,很多知识点都是一带而过,但是也算是有了Redis的一个较为完整的知识体系。并且也写了一些代码,以后针对特定问题再进行特定的代码书写和知识学习就清晰多了。感谢各位在网络上的分享,多谢!

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无语梦醒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值