背景前端
Redis在互联网项目的使用也是很是广泛的,做为最经常使用的NO-SQL数据库,对Redis的了解已经成为了后端开发的必备技能。小编对Redis的使用时间不长,可是项目中确两次踩中了Redis的坑,今天特地从基础知识层面到实战层面对Redis知识进行梳理,可以达到对Redis的知识体系有更全面和深刻的理解。java
Redis的特色redis
Redis和Memcached的比较spring
Redis的经常使用数据结构及使用场景数据库
String是Redis最基本的类型,一个key对应一个value,也是最经常使用的数据结构,在定义每一个String的key的时候,记得加上前缀。一个Key最大能存储512MB,一个Value最大能存储1G。json
Redis的Set是string类型的无序集合,集合是经过哈希表实现的,因此添加、删除和查找的复杂度都是O(1)。Set集合取交集、差集和并集能够完成两组数据的比较,因此Redis借用Set数据结构经常使用于两组数据的比较。后端
Redis ZSet和Set同样也是String类型元素的结合,而且不容许重复的成员。不一样的是ZSet中每一个元素都会关联一个double类型的分数,Redis经过分数(score)为集合中的全部元素进行大小排序。注意ZSet的成员是惟一的,但分数(score)却能够重复。经常使用语排行榜、分页查询和获取指定范围数据等应用场景。数组
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。缓存
Redis 列表是简单的字符串列表,按照插入顺序排序。你能够添加一个元素到列表的头部(左边)或者尾部(右边)。经常使用于构建异步队列。服务器
场景:
用户在使用APP的时候,页面很是的卡顿,就会随便狂点,因为接口没有作重复提交,会出现好几个相同的请求,在service层,一个线程没有insert完成,另外一个线程一查,空的。因而也插入一条进来。本来每一个人一条的,某个业务员出现了三条,致使业务逻辑错误。在业务逻辑中常常会有先查询判空再插入的场景,可是在高并发的时候,很容易出现插入记录重复的状况。
为了保证一个方法或属性在高并发状况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的状况下,可使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。可是随着业务发展的须要,原单机部署的系统被演化成分布式集群系统后,因为分布式系统多线程、多进程而且分布在不一样机器上,这将使原单机部署状况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就须要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
分布式锁机制经常使用的有三种方式,redis分布式锁、zookeeper和数据库表。
在这里简单介绍基于redis的分布式锁。
setnx
setnx key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不作,返回0。
expire
expire key timeout : 设置key为一个超时时间,单位为second,超过这个时间锁就会自动释放,避免出现因为客户端crash,不释放锁,致使死锁的现象。
delete
delete key : 删除key
实现思想
(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; }
实现此接口的类有以下:
GenericToStringSerializer
能够将任何对象泛化为字符串并序列化
StringRedisSerializer
简单的字符串序列化
JacksonJsonRedisSerializer、Jackson2JsonRedisSerializer 和GenericJackson2JsonRedisSerializer
使用Jackson库将对象序列化为JSON字符串。
优势: 速度快,序列化后的字符串短小精悍,而且易于阅读。
缺点: 但缺点也很是致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。
项目中Redis的踩坑记
下面分享两个项目中使用Redis时候踩到坑。
【问题背景】
在生产环境的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 , 这个是在闲暇的时候进行检查,去除无效的连接。
【问题背景】
版本日那天提交了代码闲来无事,看到用户信息类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: [B@641a684c; 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: [B@641a684c; 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的高级特性
一、集群
二、发布订购
三、持久化
四、Redis服务器如何容灾,如何预防单点故障等
五、读写分离操做
六、异步队列
七、Redis的雪崩和穿透
以上特性有待后续的解锁,敬请期待!