Redis总结上

这几天看了Redis,现在写个总结记录一下:

  1. Redis vs 数据库,为什么要使用Redis
  2. Java API vs Spring redis
  3. Redis中6中数据类型及常用命令
  4. Redis常用技术:事务、watch、流水线、Lua
  5. Redis相关配置:备份、内存回收策略、复制、哨兵模式
  6. Spring缓存机制和Redis

一、Redis VS 数据库
首先Redis和数据库都能存储数据,数据库持久化数据是面向磁盘,弊端是介于磁盘读写慢,因此性能差,适用于一般的管理系统;Redis作为NoSQL内存数据库的一种,实用ANSIC语言编写,数据主要存储在内存中(部分可以持久化到磁盘),只有6种数据类型,因此性能很高,适用于高并发读写的网络系统;弊端是安全稳定性差,如断电或机器故障就容易丢失数据,持久化能力有限,且内存开销大,性价比不是很高;因此,一般我们只会缓存一些常用的相对较小的数据;
综上所述:在需要缓存常用数据和高速读写的场景下,我们会使用Redis来提高系统性能;

二、Java API VS Spring
(1)Java API:jedis.jar
测试代码如下,使用了连接池:

JedisPoolConfig poolCfg=new JedisPoolConfig();
poolCfg.setMaxIdle(50);
poolCfg.setMaxTotal(100);
poolCfg.setMaxWaitMillis(20000);
JedisPool pool=new JedisPool(poolCfg,"localhost"); //端口号默认6379
Jedis jedis=pool.getResource();
long start=System.currentTimeMillis();
int i=0;
try {
	while(true){
		long end=System.currentTimeMillis();
		if(end-start>=1000){
			break;
		}
		i++;
		jedis.set("test"+i, i+"");
	}
} finally{
	jedis.close();
	pool.close();
}
System.out.println("Redis每秒操作:"+i+"次");

(2)Spring Redis:jedis.jar,spring-data-redis.jar(注意两个包的兼容性)
Redis只能提供基于字符串型的操作,在需要转换Redis字符串和Java对象时,就需要编写大量转换规则代码,而Spring刚好提供了这些的封装,它提供了序列化的设计框架和一些序列化的列,通过(反)序列化把Java对象与字符串互相转换;
–配置文件applicationContext.xml

<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
	<!--最大空闲数 -->
	<property name="maxIdle" value="50" />
	<!--最大连接数 -->
	<property name="maxTotal" value="100" />
	<!--最大等待时间 -->
	<property name="maxWaitMillis" value="20000" />
</bean>

<!-- 工厂类:有4种工厂模型,推荐使用Jedis或Lettuce,Jredis和Srp已过期,不推荐 -->
<bean id="connectionFactory"
	class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
	<property name="hostName" value="localhost" /><!-- 服务器,默认是localhost,是本机可以不配置 -->
	<property name="port" value="6379" /><!-- 接口端口,默认是6379,可以不配置 -->
	<!--<property name="password" value="paasword"/> 密码,需要密码连接Redis的场合 -->
	<property name="poolConfig" ref="poolConfig" /><!-- 连接池配置对象 -->
</bean>

<!-- RedisTemplate有键值序列化器连个属性,如下键是StringRedisSerializer,值是JdkSerializationRedisSerializer -->
<!-- 序列化器:一共7种,另可以自定义,实现RedisSerializer接口 -->
<bean id="jdkSerializationRedisSerializer"
	class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
	
<bean id="stringRedisSerializer"
	class="org.springframework.data.redis.serializer.StringRedisSerializer" />
	
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
	<property name="connectionFactory" ref="connectionFactory" />
	<property name="keySerializer" ref="stringRedisSerializer" />
	<property name="valueSerializer" ref="jdkSerializationRedisSerializer" />
</bean>

–POJO类

public class Role implements Serializable{
	//代表序列化的版本编号
	private static final long serialVersionUID = -8280473669523684829L;
	
	private long id;
	private String roleName;
	private String note;
	/*-------getter、setter--------*/
}

–使用RedisTemplate保存Role对象

//方式一:不能保证操作都来自于同一个Redis连接
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
Role role = new Role();
role.setId(1L);
role.setRoleName("role_name_1");
role.setNote("note_1");
redisTemplate.opsForValue().set("role_1", role);
Role role1 = (Role) redisTemplate.opsForValue().get("role_1");
System.out.println(role1.getRoleName());
//方式二:使用SessionCallback把多个命令放入同一个Redis连接中,这样资源损耗比较小
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
SessionCallback callBack = new SessionCallback<Role>() {
	public Role execute(RedisOperations ops) throws DataAccessException {
		Role role = new Role();
		role.setId(1L);
		role.setRoleName("role_name_1");
		role.setNote("role_note_1");
		ops.boundValueOps("role_1").set(role);
		return (Role) ops.boundValueOps("role_1").get();
	}
};
Role savedRole = (Role) redisTemplate.execute(callBack);
System.out.println(savedRole.getId());

三、Redis6种数据类型
Redis是一种键值(key-value)数据库,这6种数据类型既可以存储数据也可以简单计算,从而简化编程,这也是性能提升的地方;
注意,根据数据类型的不同,applicationContext.xml中redisTemplate的配置也会不同;
(1)STRING字符串:value是string类型
可以保存字符串、整数和浮点数,字符串可以求子串等,整数浮点数可以自增等;
Redis最基础的数据结构,以键值对存储,类似Java的Map;
下表为常用命令与spring redis代码:
在这里插入图片描述
(2)哈希:value为Object类型
hash结构可存储2^32-1(40多亿)键值对;在Redis中,hash是一个String类型的filed和value的映射表,所以实际存储的还是字符串;键值对无序;
与String类型不同,命令以h打头,通过key索引到对应的hash,通过field索引到hash结构中的键值对;
Hash结构:
在这里插入图片描述
下表为常用命令与spring redis代码:
在这里插入图片描述
(3)Linked-list链表
有序双向(可从左到右或从右到左遍历)的结构,可存储2^32-1(40多亿)节点;链表结构只能从一个方向去遍历所有节点,因此读数据性能相对不佳;但链表的数据节点是分配在不同的内存区域,并不连续,是根据上下节点的索引保存,因此插入删除数据十分便利;
下表为常用命令与spring redis代码:
注意如下命令都是进程不安全的,因为可能同时会有多个Redis客户端操作同一个链表,会造成数据安全与一致性问题,为了克服这个问题,也有几个加锁的命令,通过阻塞解决,但实际中用的不多,因为会造成其他线程挂起、恢复,性能太低;具体解决方案后续会再说明;
在这里插入图片描述
(4)集合:value是Set类型
一个哈希表结构,可存储2^32-1(40多亿)个元素,集合中元素不可重复,插入相同记录会失败,无序,每一个元素都是String数据结构;
在这里插入图片描述
(5)有序集合
比无序集合多了一个分数,分数是一个双精度的浮点数,集合根据分数排序,元素唯一,可存储2^32-1(40多亿)个元素,在满足一定条件下还可以对值进行排序;spring-data-redis对有序集合进行了封装,提供了接口TypedTuple,包含getValue(),getScore()方法,默认实现类是DefaultTypedTuple;

// Spring提供接口TypedTuple操作有序集合
Set<TypedTuple> set1 = new HashSet<TypedTuple>();
Set<TypedTuple> set2 = new HashSet<TypedTuple>();
int j = 9;
for (int i = 1; i <= 9; i++) {
	j--;
	// 计算分数和值
	Double score1 = Double.valueOf(i);
	String value1 = "x" + i;
	Double score2 = Double.valueOf(j);
	String value2 = j % 2 == 1 ? "y" + j : "x" + j;
	// 使用Spring提供的默认TypedTuple——DefaultTypedTuple
	TypedTuple typedTuple1 = new DefaultTypedTuple(value1, score1);
	set1.add(typedTuple1);
	TypedTuple typedTuple2 = new DefaultTypedTuple(value2, score2);
	set2.add(typedTuple2);
}
...
//集合范围
Range range = Range.range();
range.lt("x8");// 小于
range.gt("x1");// 大于
// 限制返回个数
Limit limit = Limit.limit();
// 限制返回个数
limit.count(4);
// 限制从第五个开始截取
limit.offset(5);

在这里插入图片描述
(6)基数HyperLogLog
基数是一种算法,作用是评估大约需要准备多少个存储单元去存储数据(不重复),但算法一般会存在一定的误差(一般是可控的),Redis从2.8.9版本开始支持基数数据结构。不存储数据,只是评估;
在这里插入图片描述

四、Redis的一些常用技术
1、Redis事务
与其他NoSQL不同,Redis支持事务,使用MUTI-EXEC命令组合保证:
(1)事务是一个被隔离的操作,事务中的方法会被Redis进行序列化并按顺序执行,事务在执行过程中不会被其他客户端发生的命令打断;
(2)事务具有原子性,即要么全部成功要么全部失败;

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
SessionCallback callBack = (SessionCallback) (RedisOperations ops) -> {
        //开启事务
	ops.multi();
	ops.boundValueOps("key1").set("value1");
	// 注意由于命令只是进入队列,而没有被执行,所以此处采用get命令,而value却返回为null
	String value = (String) ops.boundValueOps("key1").get();
	System.out.println("事务执行过程中,命令入队列,而没有被执行,所以value为空:value=" + value);
	// 此时list会保存之前进入队列的所有命令的结果
	List list = ops.exec();// 执行事务
	// 事务结束后,获取value1
	value = (String) redisTemplate.opsForValue().get("key1");
	return value;
};
// 执行Redis的命令
String value = (String) redisTemplate.execute(callBack);
System.out.println(value);

事务回滚:与数据库不太一致,对于命令格式正确但数据类型不对的情况,命令会正常进入事务队列,执行时会报错,但之前和之后的命令会正常被执行,事务不会回滚;对于命令格式错误的情况,Redis会立即检测并产生错误,事务回滚;由上可知,Redis是在命令进入到事务时就检测是否正确来决定是否回滚事务;之所以如此简单是为了保证性能;

2.流水线pipelined
在事务中Redis提供了一个可以批量执行的队列,保证了高性能,但multi-exec事务有系统开销,因为需要检测对应的锁和序列化命令,所以,如果需要在没有任何附加条件下去执行一系列的命令,从而提高系统性能,就有了流水线技术;
实际应用中,系统的瓶颈往往是网络通信时的延时;而流水线技术刚好可以解决这个问题,Redis的流水线是一种通信协议,Spring测试代码如下:

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
// 使用Java8的Lambda表达式
SessionCallback callBack = (SessionCallback) (RedisOperations ops) -> {
	for (int i = 0; i < 100000; i++) {
		int j = i + 1;
		ops.boundValueOps("pipeline_key_" + j).set("pipeline_value_" + j);
		ops.boundValueOps("pipeline_key_" + j).get();
	}
	return null;
};
long start = System.currentTimeMillis();
// 执行Redis的流水线命令,这里需要考虑到List大小,因为更大的返回值需要更大的内存空间,可能会引发JVM内存溢出
List resultList = redisTemplate.executePipelined(callBack);
long end = System.currentTimeMillis();
System.err.println(end - start);

3.发布订阅
Redis支持发布订阅模式,只需要:
(1)有发送的消息渠道,让发送系统能够发送消息;
(2)要有订阅者订阅这个消息;

监听容器——监听器——监听指定渠道
在spring中配置发布订阅模式时会提供接收消息的类(实现MessageListener接口),并实现接口定义的方法onMessage,示例如下:

public class RedisMessageListener implements MessageListener {
	private RedisTemplate redisTemplate;
	@Override
	public void onMessage(Message message, byte[] bytes) {
		// 获取消息
		byte[] body = message.getBody();
		// 使用值序列化器转换
		String msgBody = (String) getRedisTemplate().getValueSerializer().deserialize(body);
		System.err.println(msgBody);
		// 获取channel
		byte[] channel = message.getChannel();
		// 使用字符串序列化器转换
		String channelStr = (String) getRedisTemplate().getStringSerializer().deserialize(channel);
		System.err.println(channelStr);
		// 渠道名称转换
		String bytesStr = new String(bytes);
		System.err.println(bytesStr);
	}
	/*--get、set转换器--*/
}

还需要在配置文件中配置该监听类和监听容器,用来监听指定渠道:

<bean id="redisMsgListener" class="com.ssm.chapter19.redis.listener.RedisMessageListener">
	<property name="redisTemplate" ref="redisTemplate" />
</bean>
<bean id="topicContainer" class="org.springframework.data.redis.listener.RedisMessageListenerContainer"
	destroy-method="destroy">
	<!--Redis连接工厂 -->
	<property name="connectionFactory" ref="connectionFactory" />
	<!--连接池,这里只要线程池生存,才能继续监听 -->
	<property name="taskExecutor">
		<bean class="org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler">
			<property name="poolSize" value="2" />
		</bean>
	</property>
	<!--消息监听Map -->
	<property name="messageListeners">
		<map>
			<!--配置监听者,key-ref和bean id定义一致 -->
			<entry key-ref="redisMsgListener">
				<!--监听类:监听chat渠道 -->
				<bean class="org.springframework.data.redis.listener.ChannelTopic">
					<constructor-arg value="chat" />
				</bean>
			</entry>
		</map>
	</property>
</bean>

测试Redis发布订阅:向渠道chat发送消息;

redisTemplate.convertAndSend("chat","I am lazy!");

4.超时命令
Redis基于内存,所以对于Redis键值对的内存回收就十分重要;除了通过System.gc()去建议JVM回收内存垃圾,但也可能导致JVM在回收大量内存空间时引发性能低下的问题;因此,Redis提供了del命令来删除一些键值对和选择适当的回收机制及时间;
一般情况下,我们想要回收超时的键值对,Redis可以给对应的键值设置超时,命令如下:
在这里插入图片描述
注意:当key超时时,Redis并不会自动回收key的存储空间,只会标识;这是因为对于很大的键值对超时,马上自动回收可能会引起停顿,但这也有坏处就是超时的键值对会浪费很多空间;
Redis提供两种回收方式:
-定时回收:在确定的某个时间触发一段代码,回收超时键值对;
-惰性回收:当一个超时的键,被再次用get命令访问时,将触发Redis将其从内存中清空;

5.使用Lua语言
在Redis2.6以上版本,可以使用Lua语言操作;Lua语言可以弥补Redis在计算能力上的不足,Lua语言具备原子性,可以保证对于并发数据的一致性支持;
(1)执行输入Lua程序代码
命令格式:

eval lua-script key-num [key1 key2 key3 ...] [value1 value2 value3 ...]

eval 代表执行Lua语言;
lua-script代表Lua语言脚本
key-num证书代表参数中有多少个key,从1开始,没有参数写0;

eg:
command是命令:set、get、del等;
key是被操作的键;
param1,param2…代表给key的参数;

eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-value

如果需要多次执行同一段脚本,就可以使用Redis缓存脚本的功能,在Redis中脚本会通过SHA-1签名算法加密脚本,返回一个标识字符串,之后就可以通过这个字符串执行加密后的脚本,这样对于很长的脚本,就只需传递32位字符串即可,能提高传输效率;
-存储字符串:

//返回SHA-1签名过后的标识字符串,记为shastring
//对应API:jedis.scriptLoad("redis.call('set',KEYS[1],ARGV[1])")
script load script 
//执行脚本 
//对应API:jedis.evalsha(sha1,1,new String[]{"sha-key","sha-val"})
evalsha shastring keynum [key1 key2 key3 ...] [param1 param2 param3 ...]

-存储对象:spring提供了RedisScript接口及默认实现类DefaultRedisScript

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
// 定义默认脚本封装类
DefaultRedisScript<Role> redisScript = new DefaultRedisScript<Role>();
// 设置脚本
redisScript.setScriptText("redis.call('set', KEYS[1], ARGV[1])  return redis.call('get', KEYS[1])");
// 定义操作的key列表
List<String> keyList = new ArrayList<String>();
keyList.add("role1");
// 需要序列化保存和读取的对象
Role role = new Role();
role.setId(1L);
role.setRoleName("role_name_1");
role.setNote("note_1");
// 获得标识字符串
String sha1 = redisScript.getSha1();
System.out.println(sha1);
// 设置返回结果类型,如果没有这句话,结果返回为空
redisScript.setResultType(Role.class);
// 定义序列化器
JdkSerializationRedisSerializer serializer = new JdkSerializationRedisSerializer();
// 执行脚本
// 第一个是RedisScript接口对象,第二个是参数序列化器
// 第三个是结果序列化器,第四个是Reids的key列表,最后是参数列表
Role obj = (Role) redisTemplate.execute(redisScript, serializer, serializer, keyList, role);
// 打印结果
System.out.println(obj);

(2)执行lua文件

redis-cli --eval test.lua key1 key2 , 2 4
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
RedisTemplate redisTemplate = applicationContext.getBean(RedisTemplate.class);
// 读入文件流
File file = new File("G:\\dev\\redis\\test.lua");
byte[] bytes = getFileToByte(file);
Jedis jedis = (Jedis) redisTemplate.getConnectionFactory().getConnection().getNativeConnection();
// 发送文件二进制给Redis,这样REdis就会返回sha1标识
byte[] sha1 = jedis.scriptLoad(bytes);
// 使用返回的标识执行,其中第二个参数2,表示使用2个键
// 而后面的字符串都转化为了二进制字节进行传输
Object obj = jedis.evalsha(sha1, 2, "key1".getBytes(), "key2".getBytes(), "2".getBytes(), "4".getBytes());
System.out.println(obj);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值