redis从入门到踩坑

背景

Redis在互联网项目的使用也是非常普遍的,作为最常用的NO-SQL数据库,对Redis的了解已经成为了后端开发的必备技能。小编对Redis的使用时间不长,但是项目中确两次踩中了Redis的坑,今天特意从基础知识层面到实战层面对Redis知识进行梳理,能够达到对Redis的知识体系有更全面和深入的理解。

Redis的特点

优点:
  1. Key-Value类型的内存数据库,是加强版的Memcached。
  2. 整个数据库都是在内存中进行操作的,并且定期异步持久化数据到硬盘上进行保存。
  3. 在内存中进行操作,不存在磁盘IO,性能方面是非常出色的,读取操作处理速度可以超过10万次每秒,是已知性能最快的Key-Value 数据库。
  4. Redis还提供丰富的数据结构类型。
  5. Redis利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。
缺点:
  1. 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。

Redis和Memcached的比较

Redis的常用数据结构及使用场景

String

String是Redis最基本的类型,一个key对应一个value,也是最常用的数据结构,在定义每个String的key的时候,记得加上前缀。一个Key最大能存储512MB,一个Value最大能存储1G。

Set

Redis的Set是string类型的无序集合,集合是通过哈希表实现的,所以添加、删除和查找的复杂度都是O(1)。Set集合取交集、差集和并集可以完成两组数据的比较,所以Redis借用Set数据结构常用于两组数据的比较。

ZSet

Redis ZSet和Set一样也是String类型元素的结合,并且不允许重复的成员。不同的是ZSet中每个元素都会关联一个double类型的分数,Redis通过分数(score)为集合中的所有元素进行大小排序。注意ZSet的成员是唯一的,但分数(score)却可以重复。常用语排行榜、分页查询和获取指定范围数据等应用场景。

Hash

Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。

List

Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。常用于构建异步队列。

Redis实现分布式锁

场景:

用户在使用APP的时候,页面非常的卡顿,就会随便狂点,由于接口没有做重复提交,会出现好几个相同的请求,在service层,一个线程没有insert完成,另一个线程一查,空的。于是也插入一条进来。原本每个人一条的,某个业务员出现了三条,导致业务逻辑错误。在业务逻辑中经常会有先查询判空再插入的场景,但是在高并发的时候,很容易出现插入记录重复的情况。

为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。但是随着业务发展的需要,原单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

分布式锁机制常用的有三种方式,redis分布式锁、zookeeper和数据库表。

在这里简单介绍基于redis的分布式锁。

  1. setnx

    setnx key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

  2. expire

    expire key timeout : 设置key为一个超时时间,单位为second,超过这个时间锁就会自动释放,避免出现由于客户端crash,不释放锁,导致死锁的现象。

  3. delete

    delete key : 删除key

  4. 实现思想

    (1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

    (2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

    (3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

此处需要补上相关的代码

上图所示为采用redis缓存实现分布式系统下,分布式锁的效果图。

序列化和反序列化

实体对象等存入到Redis数据库中,并不是直接存储的,是以byte数组的形式存储的,所以存储到Redis中的时候,需要序列化成byte数据,从Redis读取数据的时候,需要进行反序列化操作。

spring-data-redis包中存在

public interface RedisSerializer<T> {
    byte[] serialize(T var1) throws SerializationException;
    T deserialize(byte[] var1) throws SerializationException;
}

实现此接口的类有如下:

  1. GenericToStringSerializer

    可以将任何对象泛化为字符串并序列化

  2. StringRedisSerializer

    简单的字符串序列化

  3. JdkSerializationRedisSerializer

    JDK提供的序列化功能,被序列化的对象必须实现Serializable接口。

    优点: 优点是反序列化时不需要提供类型信息(class),并且速度最快。

    缺点: 序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗redis服务器的大量内存,且通过redis客户端也不容易阅读。

  4. JacksonJsonRedisSerializer、Jackson2JsonRedisSerializer 和GenericJackson2JsonRedisSerializer

    使用Jackson库将对象序列化为JSON字符串。

    优点: 速度快,序列化后的字符串短小精悍,并且易于阅读。

    缺点: 但缺点也非常致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。

项目中Redis的踩坑记

下面分享两个项目中使用Redis时候踩到坑。

坑1

【问题背景】

在生产环境的Redis经常会报出RedisConnectionFailureException: java.net.SocketException: Broken pipe

【异常打印】

11:28:29 INFO  - get data from redis, key = c15aad89-4a1a-4cb0-82a5-2027b990c1ca
11:28:29 WARN  - /market/info/eForum/getIndexList
org.springframework.data.redis.RedisConnectionFailureException: java.net.SocketException: Broken pipe; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketException: Broken pipe
at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:67) ~[spring-data-redis-1.7.3.RELEASE.jar:?]
at org.springframework.data.redis.connection.jedis.JedisExceptionConverter.convert(JedisExceptionConverter.java:41) ~[spring-data-redis-1.7.3.RELEASE.jar:?]
at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:37) ~[spring-data-redis-1.7.3.RELEASE.jar:?]
at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:37) ~[spring-data-redis-1.7.3.RELEASE.jar:?]
at org.springframework.data.redis.connection.jedis.JedisConnection.convertJedisAccessException(JedisConnection.java:212) ~[spring-data-redis-1.7.3.RELEASE.jar:?]
at org.springframework.data.redis.connection.jedis.JedisConnection.get(JedisConnection.java:1117) ~[spring-data-redis-1.7.3.RELEASE.jar:?]
at org.springframework.data.redis.core.DefaultValueOperations$1.inRedis(DefaultValueOperation

【问题原因】

Redis底层也创建了连接池,获取到了失效的连接,并且Redis客户端尝试通过此连接池跟服务端进行通信, 导致抛出上面的异常。

【解决办法】

Redis配置的连接池使用jar包commons-pool-2.4.2.jar方式,其中BaseObjectPoolConfig类为基础配置类。

private boolean testOnCreate = false;
private boolean testOnBorrow = false;
private boolean testOnReturn = false;
private boolean testWhileIdle = false;

如上述四个属性参数默认都是false,可以通过修改 testOnBorrow = true 和 testWhileIdle = true 来解决获取无效链接的问题。其中 testOnBorrow = true 是获取链接的时候对链接的有效性进行检查,会影响效率,在高并发的前提下。所以一般只是配置 testWhileIdle = true , 这个是在闲暇的时候进行检查,去除无效的链接。

坑2

【问题背景】

版本日那天提交了代码闲来无事,看到用户信息类UserInfoExt,存储在common的util目录下,有强迫症的我,硬是把它移到了entity包下。以为完美的重构了,没想到挖出了一个巨大的坑。打预发版的包到测试环境,立马所有的已登录用户,都不能进行其他操作。只要切换页面就会抛出“网络服务异常情况”,整个预发版的测试环境被我搞瘫痪了,大家都没法测试。 预发版测试不完成,就没法正常发版,说实话那时候压力还挺大的,全项目的人都盯着你。以后要重构代码,千万别发版前重构,最好是版本迭代开始的前几天就重构好,这样即使重构带来的bug,也有足够的时候去发现和解决。

【异常打印】

19:32:47 INFO  - Started Application in 10.932 seconds (JVM running for 12.296)
19:32:50 INFO  - get data from redis, key = 10d044f9-0e94-420b-9631-b83f5ca2ed30
19:32:50 WARN  - /market/renewal/homePage/index
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Could not resolve type id ‘com.pa.market.common.util.UserInfoExt‘ into a subtype of [simple type, class java.lang.Object]: no such class found
 at [Source: [[email protected]; line: 1, column: 11]; nested exception is com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id ‘com.pa.market.common.util.UserInfoExt‘ into a subtype of [simple type, class java.lang.Object]: no such class found at [Source: [[email protected]; line: 1, column: 11]
 

【问题原因】

项目中使用了拦截器,对每个http请求进行拦截。通过前端传递过来的token,去redis缓存中获取用户信息UserInfoExt,用户信息是在用户登录的时候存入到redis缓存中的。根据获取到的用户信息来判断是否存是登录状态。

所以除白名单外的url,其他请求都需要进行这个操作。通过日志打印,很明显出现在UserInfoExt对象存储到redis中序列化和反序列化的操作步骤。

【解决办法】

@Bean
public RedisTemplate<K, V> redisTemplate() {
    RedisTemplate<K, V> redisTemplate = new RedisTemplate<K, V>();
    redisTemplate.setConnectionFactory(jedisConnectionFactory());
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    return redisTemplate;
 }

查看Redis的Bean定义发现,对key的序列化使用的是StringRedisSerializer系列化,value值的序列化是GenericJackson2JsonRedisSerializer的序列化方法。

其中GenericJackson2JsonRedisSerializer序列化方法会在redis中记录类的class信息,如下所示:

{
"@class": "com.pa.market.common.util.UserInfoExt",
"url": "www.baidu.com",
"name": "baidu"
}

"@class": "com.pa.market.common.util.UserInfoExt",每个对象都会有这个id存在(可以通过源码看出为嘛有这个@class),如果用户一直处在登录状态,是以com.pa.market.common.util.UserInfoExt这个路径进行的序列化操作。但是移动了UserInfoExt的类路径后,包全名变了。所以会抛出no such class found的异常。这样在判断用户是否存在的地方就抛出了异常,故而所有的请求都失败了,已经登录的用户没法进行任何操作。

【总结】

对于上面的序列化的坑,貌似没有很好的解决方案。从比较常用的序列化和反序列化类,可以发现每个都有各自的优点和缺点。如果在redis层面把对象转成json,那么每条记录中都会有@class这个标记,如果以后代码重构,移动类路径,肯定是不行的,是个巨坑。如果在入redis之前,就把对象直接转成json,然后用StringRedisSerializer的方式对value进行序列化和反序列化,这样可读性好,也不会跟对象的类路径有强关联。但是需要中间做一道处理,写的时候需要对象转json,读的时候又需要json转对象,会降低效率。

Redis的高级特性

1、集群

2、发布订购

3、持久化

4、Redis服务器如何容灾,如何预防单点故障等

5、读写分离操作

6、异步队列

7、Redis的雪崩和穿透

以上特性有待后续的解锁,敬请期待!

原文地址:https://www.cnblogs.com/1024Community/p/8922631.html

时间: 2024-10-03 03:02:02

redis从入门到踩坑的相关文章

记录redis的一次踩坑(提醒诸位)

#背景:来公司之前redis跑的是单主,无备份,rdb和aof都没有,于是我就决定做一个主从,在从上做rdb备份,本着资源充分利用的心理,在一台memcache上做了redis从,memcache这台内存富余50G左右. 周末陆续收到报警redis从机这一台内存富余不足,怕影响到memcache服务,于是决定把redis从的rdb备份关掉,因为在bgsave的时候内存占用会变成双倍,修改配置文件注释掉save那几条重启. 故障就发生了:因为主从重启的时候,主会做一次bgsave操作生成rdb文件

redis集群搭建踩坑笔记

推荐参考教程:https://blog.csdn.net/pucao_cug/article/details/69250101 错误: from /usr/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require' from /usr/local/redis-3.0.6/src/redis-trib.rb:25:in `<main>' 解决: gem install redis (最新) sudo gem install

iOS开发-OpenGLES 入门踩坑

Flat coloring(单色) 是通知OpenGL使用单一的颜色来渲染,OpenGL将一直使用指定的颜色来渲染直到你指定其它的颜色. 指定颜色的方法为 public abstract void glColor4f(float red, float green, float blue, float alpha). 缺省的red,green,blue为1,代表白色. Smooth coloring (平滑颜色过渡) 当给每个顶点定义一个颜色时,OpenGL自动为不同顶点颜色之间生成中间过渡颜色(

&lt;&lt;Python编程:从入门到实践&gt;&gt;踩坑记 Django

<<Python编程:从入门到实践>>踩坑记 Django Django Python 19.1.1.5 模板new_topic 做完书上的步骤后,对主题添加页面经行测试,但是浏览器显示 服务器异常. 个人采用的开发环境是virtual studio code , 测试起来很是难受,因为我配置的debug环境,断点操作没有作用. 经过我不断的测试,才发现我失败的原因是由于之前的误操作,先建立new_pizzas.py后改为new_pizzas.html的,错误就在这里.在我之后新建

阿里分布式事务seata入门(采坑)

1. 阿里分布式事务seata入门(采坑) 1.1. 前言 seata是feascar改名而来,这是阿里在19年年初开源出来的分布式事务框架,当初刚出来的时候就想研究下了,一直拖到了现在,目前是0.8.0版本,看版本就知道这还是个比较新的项目,但现在已经有上万个Star了,可见阿里的影响力.但是虽然有阿里背书,该挖坑还得挖,它宣称集成它比较简单,导致的是现在它的文档优点残缺不全,好几个文档标题点进去都没内容,不知道为什么删了,可能是更新比较快,文档跟不上节奏索性删了[手动滑稽] 1.2. 快速开

阿里云磁盘扩容踩坑总结

公司半年前上线一个新的项目,采购了一批阿里云主机,磁盘组成是40G系统盘+100G的数据盘,数据库采用MariaDB Galera Cluster集群部署,由于业务数据量快速增长,导致磁盘存储空间剩余量很少,急需要扩容,先总结整个项目规划中埋下的坑: 1.没有DBA对数据库的容量规划,而前期的运维人员采购时选用100G的SSD云盘: 2.数据库默认使用共享表空间,缺点是删除数据后不释放空间,当数据快速增长后,我们采取了先删除临时表数据的方式来尽量避免暴力扩容,争取在春节期间稳定,删除部分数据后,

AI相关 TensorFlow -卷积神经网络 踩坑日记之一

上次写完粗浅的BP算法 介绍 本来应该继续把 卷积神经网络算法写一下的 但是最近一直在踩 TensorFlow的坑.所以就先跳过算法介绍直接来应用场景,原谅我吧. TensorFlow 介绍 TF是google开源出来的人工智能库,由python语言写的 官网地址:http://www.tensorflow.org/   请用科学上网访问 中文地址:http://www.tensorfly.cn/ 当然还有其他AI库,不过大多数都是由python 写的 .net 的AI库叫 Accord.net

之后要接触更多代码管理的知识——2015踩坑有感

前言 学习是没有止境的,管理代码的能力也永远需要提高. 前几个月还觉得R语言,业务上要用的都学得七七八八了呢,这几个月在自家部门吭哧吭哧搞报表自动化时,各个坑一踩一个准,才明白写代码,懂得一点语言特性固然重要,弄一套科学地管理代码的方法,却是势在必行. 因此在这里总结一下这几个月来我踩过的种种坑,以及之后在查阅种种大神的经验,以及学软件工程这门课时看到的一些比较妥当的方法,算是这几个月的一个总结.2016年的时候,真的要多学学如何科学地管理代码,科学开发 请注意,因为我属于跨专业半路出家写代码,

Java踩坑之路

陆陆续续学Java也快一年多了,从开始的一窍不通到现在的初窥门径,我努力过,迷茫过,痛过,乐过,反思过,沉淀过.趁着新年,我希望能把这些东西记下来,就当是我一路走来的脚印. 一.初识网站应用 记得第一次接触Java,是写一个小网站,当时用servlet+tomcat做服务端,数据库是mysql.那时我对于网站应用的概念一片空白,之前接触的都是C++和MFC写桌面程序.我花了一周时间看完了<java servlet programming>这本书,然后我就开始写代码了.但是,真的当我写代码的时候