此再见非彼再见,在之前的一篇文章中,学习了 Redis 的安装和基本的使用,在边实习边学习的过程中,加深了对其的理解,所以在这里总结一哈。
文章目录
回顾 Redis
Redis 支持五种的数据类型:
String(字符串)、Hash(哈希)、List(列表)、Set(集合)及 ZSet(sorted set,有序集合)。
键 Key 的管理
命令
命令 | 作用 |
---|---|
DEL key | key 存在时删除 key |
DUMP key | 序列化给定 key 并返回序列化的值 |
EXISTS key | 检查 key 是否存在 |
EXPIRE key seconds | 为给定 key 设置过期时间 |
PEXPIRE key milliseconds | 同上 |
TTL key | 以秒为单位返回给定 key 的剩余生存时间 |
PTTL key | 以毫秒为单位,同上 |
PERSIST key | 移除 key 的过期时间,key 将持久保持 |
KEYS pattern | 查找所有符合给定模式的 key (* ?) |
RENAME key newkey | 修改 key 的名称 |
MOVE key db | 将当前数据库的 key 移动到给定的数据库 db 中 |
TYPE key | 返回 key 所存储的值的类型 |
最常用的就是 EXPIRE key seconds,应用场景:
- 限时优惠活动信息
- 需要定时更新的数据,积分榜
- 手机验证码
key 的命名建议:
- 不要太长,redis 单个 key 存入 512M 大小,且会降低查找效率
- 使用统一的命名模式提高 key 的可读性,如 studen,userId
五种数据类型
String
最基本的 Redis 存储类型,一个 key 对应一个 value。
二进制安全是指在传输数据时,编码、解码发生在客户端,保证二进制数据的信息安全,不会被篡改或者破译。
而 String 类型是二进制安全的,也就是说可以保存任何的数据,如图片或者序列化的对象等。
命令
命令 | 作用 |
---|---|
SET key value | 设置给定 key 的值,如果已经存在则覆盖 |
SETNX key value | 只在 key 不存在时设置 key 的值 |
MSET key value [key value] | 同时设置多个 key-value |
GET key | 获取指定 key 的值,不存在返回 nil |
GETRANGE key start end | 获取获取指定 key 中字符串的子字符串 |
GETBIT key offset | 对 key 所储存的字符串值,获取指定偏移量上的位 |
MGET key1 [key2] | 获取多个给定 key 的值 |
GETSET | 设置指定 key 的值,返回旧值 |
STRLEN key | 返回 key 所储存的字符串值的长度 |
DEL key | 删除指定的 key |
INCR key | 将 key 中储存的数字加1,如果不存在则初始化为0再操作 |
INCR key 增量 | 指定 key 的自增量 |
DECR key | DECR key 减量 |
APPEND key value | 在指定 key 值的末尾追加 |
虽然 String 可以保存任何数据,但我们还是需要选择合适的储存类型,这样才便于我们操作数据。
String 类型的应用场景:
- 单个字符串或 JSON 字符串数据
- 图片文件
- 计数器(自增自减命令都具有原子特性)
Hash
相比于 String 类型,Hash 类型更适合储存一个 JavaBean 对象。
User(id,name,age,sex)
命令
命令 | 作用 |
---|---|
HSET key field/value | 为指定 key 指定field value |
HMSET key field value [field value] | 同上(多个) |
HGET key field | 获取储存在 hash 中的值,根据 field 得到 value |
HMGET key field[field1] | 同上(多个) |
HGETALL key | 返回 hash 表中所有的字段和值 |
HKEYS key | 获取所有哈希表中的字段 |
HLEN key | 获取哈希表中字段的数量 |
HDEL key field[field1] | 删除一个或多个 hash 表字段 |
HSETNX key field value | 只有在字段 field 不存在时,设置哈希表字段的值 |
HINCRBY key field increment | 为 hash 表 key 中的指定整数字段加上增量 |
HINCRBYFLOAT key field increment | 同上(浮点字段) |
HEXISTS key field | 查看 hash 表 key 中,指定的字段是否存在 |
为什么不用 String 存储一个对象?
首先我们来看下,用 String 类型来储存一个用户对象的方式:
- 将用户 ID 作为查找 key,而其他信息以序列化的方式存储,在操作数据时,需要进行序列化或反序列化,同时需要考虑并发问题。
- 将对象所有的成员都以 key-value 的形式存储,用 ID+属性名来作为 key,这样虽然没有了上一种方法的缺点,但造成大量内存的浪费。
反观hash 类型,它是最接近关系数据库结构的数据类型,可以将数据库一条记录转换成一个 hashmap 存放到 redis 中。
Java 操作 Redis
常用的两种:
- Jedis是Redis官方推荐的面向Java的操作Redis的客户端
- RedisTemplate 是 SpringDataRedis中对JedisApi 的高度封装
Jedis
1.引入 JAR
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.4.2</version>
</dependency>
2.连接 Redis
//直接 new Jedis 对象
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.auth("123456");
//通过 redis pool
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(5);//最大连接数
poolConfig.setMaxIdle(1);//最大空闲数
String host = "127.0.0.1";
int port = 6379;
JedisPool pool = new JedisPool(poolConfig,host,port);
Jedis jedis = pool.getResource();
jedis.auth("123456");
3.使用 Jedis 客户端
String key = "moke";
if(jedis.exists(key)){
String res = jedis.get(key);
System.out.println("Redis数据:"+res);
}else{
//查询数据库,获取到 res
String res = "数据库数据";
jedis.set(key,result);
System.out.println("MySQL数据:"+res);
}
jedis.close();
这一步就是我们平时在项目中的应用,为了减轻数据库的访问压力,首先判断 Key 是否存在,如果存在则直接返回;如果不存在,才查询数据库,然后将结果存到 redis 中,下次调用就不需要访问数据库。
当然我们可以将获取 Jedis 客户端的操作封装成一个工具类:
public class JedisUtils{
private static final JedisPool JEDIS_POOL;
static{
JedisPoolConfig config = new JedisPoolConfig();
poolConfig.setMaxTotal(5);
poolConfig.setMaxIdle(1);
JEDIS_POOL = new JedisPool(poolConfig,"127.0.0.1",6379);
}
public static Jedis getJedis(){
Jedis jedis = JEDIS_POOL.getResource();
jedis.auth("123");
return jedis;
}
}
我们用上面的工具类,来看下如何使用 Jedis 来操作 hash 类型:
//user = selectById(id)
Jedis jedis = JedisUtils.getJedis();
String key = user.getName() + user.getId();
if(jedis.exist(key)){
Map<String,String> hash = jedis.hgetAll(key);
User u = new User();
u.setId(Integer.parseInt(hash.get("id")));
u.setName(hash.get("name"));
u.setAge(hash.get("age"));
u.setSex(hash.get("sex"));
System.out.println("Redis中数据"+u);
}else{
User user = userService.getBy(id);
Map<String,String> hash = new HashMap<String,String>();
hash.put("id",u.getId()+"");
hash.put("name",u.getName());
hash.put("age",u.getAge());
hash.put("sex",u.getSex());
jedis.hmset(key,hash);
System.out.println("MySQL中数据"+user);
}
从上面我们可以发现,jedis 操作 Redis 的方法名其实就是我们之前学的那些 Redis 的命令。
也可以发现用 Jedis 操作比较繁琐,所以就有了 RedisTemplate 模板,封装了 redis 连接池管理的逻辑,无需关心连接的获取与释放。
RedisTemplate
使用过程可以在之前的文章中看:地址
这里主要说下在 Spring 中配置 redistemplate 时,配置 key 或者 value 的序列化,不然可能会乱码:
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="hashKeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="valueSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
<property name="hashValueSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
</bean>
List
类似于 LinkedList,主要用于存储一组数据,可以很方便实现分页,也可以用于实现任务(消息)队列。
命令
命令 | 作用 |
---|---|
LPUSH key value [value1] | 将一个或多个值插入到列表头部 |
RPUSH key value [value1] | 将一个或多个值插入到列表尾部 |
LPUSHX key value | 将一个值插入到已存在的列表头部,如果不存在则无效 |
RPUSHX key value | 将一个值插入到已存在的列表尾部,如果不存在则无效 |
LLEN key | 获取列表长度 |
LINDEX key index | 通过所以获取列表中的元素 |
LRANGE key start stop | 获取列表指定范围内的元素 |
LPOP key | 移除并获取列表的第一个元素 |
RPOP key | 移除并获取列表的最后一个元素 |
BLPOP key1 [key2] timeout | 移除并获取列表第一个元素,如果列表没有元素会阻塞到有或者超时 |
LTRIM key start stop | 对一个列表进行修剪,不在区间的元素删去 |
LSET key index value | 通过索引设置列表元素的值 |
LINSERT key BEFORE | AFTER wordl value |
RPOPLPUSH source detination | 移除列表最后一个元素并添加到另一个列表返回 |
BRPOPLPUSH source destination timeout | 从列表中弹出一个值并插入另一个列表,如果前一个列表没有元素则会阻塞到有或超时 |
任务队列
list 一般会用来实现一个消息队列
任务队列:
在处理请求时,某些操作的执行时间可能会比较长,为了避免用户一直等待,通过将其放入队列,并在之后对队列进行处理,这种将工作交给任务处理器来执行的做法被称为任务队列。
而任务队列则可以使用 BRPOPLPUSH 命令来实现。
Set
类似 Java 中的 HashTable 集合,是 String 类型的无序集合,集合成员是唯一的。
Set 底层存储结构使用了 intset 和 hashtable 两种,前者为数组而后者则是哈希表:
- intset 只有保存整数元素时才使用,使得它可以通过二分查找元素。
- hashtable 的value永远为null,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。
命令
命令 | 作用 |
---|---|
SADD key member1 [member2] | 向集合添加一个或多个成员 |
SCARD key | 获取集合的成员数 |
SMEMBERS key | 返回集合中的所有成员 |
SISMEMBER key member | 判断 member 元素是否是集合 key 的成员 |
SRANDMEMBER key[count] | 返回集合中一个或多个随机数 |
SREM key member1 [member2] | 移除集合中一个或多个成员 |
SPOP key[count] | 移除并返回集合中的一个随机元素 |
SMOVE source destination member | 将 member 元素移到 destination 集合 |
SDIFF key1 [key2] | 返回给定所有集合的差集(左侧) |
SDIFFSTORE destination key1 [key2] | 结果存储在 destination 中 |
SINTER key1 [key2] | 返回给定所有集合的交集(共有) |
SINTERSTORE destination key1 [key2] | 结果存储在 destination 中 |
SUNION key1 [key2] | 返回所有给定集合的并集 |
SUNION STORE destination key1 [key2] | 结果存储在 destination 中 |
应用场景:
- 集合运算:共同好友、共同关注等
- 唯一性:统计访问网站的IP
ZSet
ZSet,即 sorted set,因为 redis 中有序集合的操作命令都以 z 开头。
ZSet 和 Set 不同的是每个元素都会关联一个 double 类型的分数,通过分数对集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)可以重复。
命令
命令 | 作用 |
---|---|
ZADD key score1 member1 [score2 member2] | 向有序集合添加一个或多个成员,或更新已存在成员的分数 |
ZCARD key | 获取有序集合的成员数 |
ZCOUNT key min max | 计算指定区间的成员数 |
ZRANK key start stop | 从低到高返回指定索引区间的成员 |
ZREVRANGE key start stop | 从高到低返回指定索引区间的成员 |
del key | 移除集合 |
ZREMRANGEBYRANK key start stop | 移除有序集合中给定的排名区间的所有成员 |
ZREMRANGEBYSCORE key min max | 移除有序集合中给定的分数区间的所有成员 |
应用场景:排行榜、定时任务
Redis 发布订阅
发布/订阅模式(Pub/Sub)是一种消息模式,它有 两个参与者 : 发布者和订阅者 。发布者向 某个信道发布一条消息,订阅者绑定这个信道,当有消息发布至信道时就会接收到一个通知。最重要的一点是, 发布者和订阅者是完全解耦的,彼此并不知晓对方 的存在。两者仅仅共享一个信道名称。
从定义上可以看出,发布订阅模式里双方是完全解耦的,而在观察者模式里,目标对象管理这观察者,双方是耦合的,这是最主要的区别,而在发布订阅模式中多了一个中间层信道(频道)。
命令
命令 | 作用 |
---|---|
SUBSCRIBE chaannel […] | 订阅给定的一个或多个频道的信息 |
PSUBSCRIBE pattern […] | 订阅一个或多个符合给定模式的频道 |
PUBLISH channel message | 将消息发送到指定频道 |
UNSUBSCRIBE channel […] | 退订给定的频道 |
PUNSUBSCRIBE pattern […] | 退订给定模式的频道 |
应用场景:构建实时消息系统,如即时聊天。
Redis 多数据库
Redis 下,数据库是由一个整数索引标识,默认情况下,客户端连接到数据库 0。
在 Redis 配置文件中:
database 16
表示从 0 开始有16个数据库
命令
命令 | 作用 |
---|---|
select 索引 | 切换数据库 |
move key 索引 | 移动数据 |
flushdb | 清除当前数据库的所有 key |
flushall | 清除整个 Redis 的数据库所有的 key |
Redis 事务
事务执行的阶段:
- 开始事务
- 命令入队
- 执行事务
Redis 会将一个事务中的所有命令序列化,然后按顺序执行,执行中不会被其他命令插入。
命令
命令 | 作用 |
---|---|
DISCARD | 取消事务 |
EXEC | 执行所有事务块内的命令 |
MULTI | 标记一个事务块的开始 |
UNWATCH | 取消 WATCH 命令对所有 key 的监视 |
WATCH key […] | 监视 key,如果事务执行前这个 key 被其他命令改动,那么事务将被打断 |
事务的错误处理
- 语法(报告)错误,整个事务不会执行
- 如果执行的某个命令报出错误,则只有报错的命令不会被执行,而其他命令依然执行,不会回滚。
Redis 数据淘汰
当内存不足时,Redis 会根据配置的缓存策略淘汰部分 Keys,以保证写入成功。如果无淘汰策略或没找到合适淘汰的 Key 时,会返回 out of memory 错误。
最大缓存配置
在配置文件中:
maxmemory 1G
6 种淘汰策略
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
- volatile-lfu:从已设置过期的 Keys 中,删除一段时间内使用次数最少的。
- volatile-ttl:从已设置过期时间的数据集中挑选最近要过期的数据淘汰。
- volatile-random:从已设置过期时间的数据集中随机选择数据淘汰。
- allkeys-lru:从数据集中挑选最近最少使用的
- allkeys-lfu:从所有 Keys 中删除一段时间内使用次数最少的。
- allkeys-random:从数据集中随机选择数据淘汰。
- no-enviction:禁止淘汰数据,返回错误信息
注: 平时使用 Redis 时尽量主动设置 key 的 expire 时间,有助于提高查询性能。
Redis 持久化
两种持久化机制:
- RDB
- AOF
RDB
redis 默认的持久化机制,以快照的方式,将内存中的数据写入二进制文件中(dump.rdb)
优点: 保存数据和恢复数据极快,适用于灾难备份。
缺点:小内存机器不适合,且 RDB 只有符合要求才会执行快照。
快照条件:
- 服务器正常关闭
- 配置文件中设置:
save 900 1 //每900秒至少一个key发送变化,则产生快照
AOF
RDB 有一个致命缺陷,就是如果 Redis 意外 down 掉的话,就会丢失最后一次快照之后的所有修改。
而 AOF(Append-only file)是在 Redis 的每次写操作都通过 write 方法将数据追加到文件中,当 Redis 重启时就会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
三种方式:
- appendonly yes:启用 aof 持久化方式
- appendfsync always:收到写命令就立即写入磁盘,保存完全的持久化,最慢
- appendfsync everysec:每秒写入磁盘一次,折中
- appendfsync no:最快,完全依赖系统
缺点:持久化的文件会越来越大,且对于自增命令,出现多少次自增就会有多少条命令。
Redis 缓存与数据库一致性
四种解决方案:
- 实时同步
- 异步队列
- 第三方同步工具(阿里)
- UDF 自定义函数
实时同步
原理
查询不到缓存时才会从数据库查询,并保存到缓存。而在更新缓存时,先更新数据库,再将缓存的设置过期时间。
使用注解:
@Cacheable @CachePut @CacheEvict @Caching
缓存穿透
在查询一个一定不存在的数据,由于缓存在不命中时需要查询数据库,查不到数据则不写入缓存,这就会导致每次请求查询这个不存在的资源时,每次都要查询数据库,造成缓存穿透。
解决:查询不到不是不缓存,而是缓存空结果(注意insert)。
异步队列
对于并发程度较高的,可采用异步队列的方式同步,比如 kafka、ActiveMQ等消息中间件处理消息生产和消费。
第三方同步工具
使用阿里的 canal 实现方式是模拟 mysql slave 和 master 的同步机制(主从复制),通过监控 DB bitlog 的日志更新来触发缓存的更新:
热点 key(缓存雪崩)
对于某个 key 访问平凡,即使设置了失效时间,在失效时有大量线程来构建缓存,导致负载增加,解决办法:
- 构建缓存的地方使用锁,单机用 synchronized,lock等,分布式用分布式锁。
- 缓存过期时间不设置,将时间设置在value中,如果检查到value中的时间过期则异步更新缓存。
- 设置标签缓存以及它的过期时间,该标签过期后会异步更新实际缓存。
Redis 并发
一般不会使用一台 Redis 服务器,原因:
- 单个 Redis 服务器容易发生单点故障
- 单个服务器的性能与系统资源比较有限
高可用
“高可用性”通常来描述一个系统经过专门的设计,从而减少停工时间,而保证其服务的高度可用性。
高并发
高并发是指通过设计保证系统能够同时并行处理很多请求。
关键字
- 响应时间:系统对请求做出响应的时间
- 吞吐量:单位时间内处理的请求数
- 每秒查询率:每秒响应请求数
- 并发用户数:同时承载正常使用系统功能的用户数量
提高并发能力
主要有两种类型:
- 垂直扩展
- 水平扩展
垂直扩展
垂直扩展就是提升单机处理能力,例如:
- 增加单机硬件性能:CPU、内存、网卡…
- 提升单机架构性能:缓存减少 IO 次数,使用异步来增加吞吐量,使用无锁数据结构减少响应时间
优点:
是最快最简单的方式。
缺点:
提升是有限的
水平扩展
水平扩展则是只要增加服务器的数量,就能线性扩充系统性能,主要难点是在架构各层进行可水平扩展的设计。
redis 是一个非关系型数据库,其常见的水平扩展也和 mysql 一样,可以实现主从复制,如下图:
将一台 Redis 服务器作为主库,其他三台作为从库,主库只负责写数据,每次有数据更新将更新的数据同步到它所有的从库,而从库只负责数据。
注: 一台主库可以有多个从库,而一个从库只能附属一台主库。
主从复制配置
1.主数据库不需要配置,只需在传讲从数据库时指定主服务器就行了
port 6380 //从服务器的端口号
slaveof 127.0.0.1 6379 //指定主服务器
也可以在启动时指定:
./usr/local/redis/redis.server ./redis.conf --port 6380 --slaveof 127.0.0.1
2.主从服务器客户端切换
slaveof on one //变回主
slaveof ip port //变回从
Redis 集群
常见的 Redis 集群搭建方案有三种:
- Twitter 的 twemproxy
- 豌豆荚的 codis
- redis 官方的 redis-cluster
(至少3master+3slave)
这里我们主要学习的 redis 官方的 redis-cluster,其主要特点:
- 所有节点彼此互联,内部使用二进制协议优化传输速度和带宽。
- 节点的 fail 状态是检测到集群中超过半数的节点无法连接才会生效。
- 客户端与 节点直连,不需要中间代理层,只需连接其中一个节点。
- redis-cluster 把所有物理节点映射到[0-16383]slot上,由它负责维护。
16384个哈希槽,对 key 使用 crc16 算法,再对16384求余数,通过这样的方式将内容放到对应的槽中
容错性
redis-cluster 投票机制,投票过程是集群中所有 master 参与,如果半数以上 master 与某个 master 节点通信超时,则认为当前 master 节点挂掉。
如果集群超过半数以上 master 挂掉,或者任意master挂掉且其没有slave,则集群进入 fail 状态。
集群搭建
集群中至少有奇数个节点,所以搭建集群最少需要 3 台主机,同时每个节点至少有一个备份节点,所以最少需要创建使用 6 台机器才能完成 Redis Cluster 集群(主节点、备份节点由 redis-cluster 集群确定)。
1.对每个机器中的 redis 的配置文件进行修改:
2.创建集群
可以通过官方提供的 redis-trib.rb 来创建集群,安装后直接:
redis-trib.rb create --replicas 1 197.168.1.101:7001 197.168.1.102:7002 197.168.1.103:7003 197.168.1.104:7004 197.168.1.105:7005 197.168.1.106:7006
3.连接集群
redis-cli -h 197.168.1.103 -c -p 7003
只需要指定连接集群上的其中一个节点即可。
4.查看集群信息
Cluster Nodes
Cluster Info
每个 Redis 的节点都有一个 ID 值,被称为节点 ID。此 ID 被特定的 redis 实例永久使用,以便实例再集群上下文中具有唯一的名称。