5. Redis Java API操作
Redis不仅可以通过命令行进行操作,也可以通过JavaAPI操作,通过使用Java API来对Redis数据库中的各种数据类型操作。
离线架构和实时架构流程:
5.1 创建maven工程并导入依赖
5.1.2 导入POM依赖
<dependencies> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>6.14.3</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.0</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> <!-- <verbal>true</verbal>--> </configuration> </plugin> </plugins> </build>
5.2 创建包结构和类
在test目录创建 cn.itcast.redis.api_test 包结构
创建RedisTest类
5.3 连接以及关闭redis客户端
因为后续测试都需要用到Redis连接,所以,我们先创建一个JedisPool用于获取Redis连接。此处,我们基于TestNG来测试各类的API。使用@BeforeTest在执行测试用例前,创建Redis连接池。使用@AfterTest在执行测试用例后,关闭连接池。
实现步骤:
创建JedisPoolConfig配置对象,指定最大空闲连接为10个、最大等待时间为3000毫秒、最大连接数为50、最小空闲连接5个
创建JedisPool
使用@Test注解,编写测试用例,查看Redis中所有的key
a) 从Redis连接池获取Redis连接
b) 调用keys方法获取所有的key
c) 遍历打印所有key
package cn.itcast.redis.api_test; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import java.util.List; import java.util.Set; /** * @author :caizhengjie * @description: * 1. 创建JedisPoolConfig配置对象,指定最大空闲连接为10个、最大等待时间为3000毫秒、最大连接数为50、最小空闲连接5个 * 2. 创建JedisPool * 3. 使用@Test注解,编写测试用例,查看Redis中所有的key * a) 从Redis连接池获取Redis连接 * b) 调用keys方法获取所有的key * c) 遍历打印所有key * @date :2021/1/26 11:28 上午 */ public class RedisTest { private JedisPool jedisPool; @BeforeTest public void redisConnectionPool(){ // 创建JedisPoolConfig配置对象 JedisPoolConfig config = new JedisPoolConfig(); // 指定最大空闲连接为10个 config.setMaxIdle(10); // 最小空闲连接5个 config.setMinIdle(5); // 最大等待时间为3000毫秒 config.setMaxWaitMillis(3000); // 最大连接数为50 config.setMaxTotal(50); jedisPool = new JedisPool(config,"bigdata-pro-m07"); } @Test public void keysTest(){ // 从redis连接池获取redis连接 Jedis jedis = jedisPool.getResource(); // 调用keys方法获取所有的key Set<String> keySet = jedis.keys("*"); for (String key : keySet){ System.out.println(key); } } @AfterTest public void afterTest(){ // 关闭连接池 jedisPool.close(); } }
注意:
操作Redis一般要使用Jedis的连接池,这样可以有效的复用连接资源
在IDEA中,有时候提示可能不完整,其实Jedis连接池,可以指定端口号
5.4 操作string类型数据
添加一个string类型数据,key为pv,用于保存pv的值,初始值为0
查询该key对应的数据
修改pv为1000
实现整形数据原子自增操作 +1
实现整形该数据原子自增操作 +1000
/** * 操作string类型数据 */ @Test public void stringTest(){ // 从redis连接池获取redis连接 Jedis jedis = jedisPool.getResource(); // 1.添加一个string数据类型,key为pv,用于保存pv的值,初始值为0 jedis.set("pv","0"); // 2.查询key对应的数据 System.out.println("pv:"+jedis.get("pv")); // 3.修改pv为1000 jedis.set("pv","1000"); // 4.实现整形数据原子自增操作+1 jedis.incr("pv"); // 5.实现整形数据原子自增操作+1000 jedis.incrBy("pv",1000); System.out.println(jedis.get("pv")); }
注意:
Redis操作string其实和SHELL命令是一样
将来在编写Flink程序/Spark Streaming程序操作Redis的时候,注意操作完Redis之后,执行close,将连接返回到连接池。
5.5 操作hash列表类型数据
往Hash结构中添加以下商品库存
a) iphone11 => 10000
b) macbookpro => 9000
获取Hash中所有的商品
新增3000个macbookpro库存
删除整个Hash的数据
/** * 操作hash列表类型数据 */ @Test public void hashTest(){ // 从redis连接池获取redis连接 Jedis jedis = jedisPool.getResource(); // 1.往Hash结构中添加以下商品库存 // (a)iPhone11 => 10000 // (b)MacBookPro => 9000 jedis.hset("goods","iPhone11","10000"); jedis.hset("goods","MacBookPro","9000"); // 2.获取Hash中所有的商品 Set<String> goodSet = jedis.hkeys("goods"); System.out.println("所有商品:"); for (String good : goodSet) { System.out.println(good); } // 3.新增3000个MacBookPro库存 // String storeMacBook = jedis.hget("goods","MacBookPro"); // long longStore = Long.parseLong(storeMacBook); // long addStore = longStore + 3000; // jedis.hset("goods","MacBookPro",addStore + ""); jedis.hincrBy("goods","MacBookPro",3000); // 4.删除整个Hash的数据 jedis.del("goods"); jedis.close(); }
注意:
当我们后续在编写Flink、Spark Streaming流处理程序使用Java操作Redis时候,涉及到一些数字的累加
一定要使用incr、hincrBy
5.6 操作list类型数据
向list的左边插入以下三个手机号码:18511310001、18912301231、18123123312
从右边移除一个手机号码
获取list所有的值
/** * 操作list类型数据 */ @Test public void listTest(){ // 从redis连接池获取redis连接 Jedis jedis = jedisPool.getResource(); // 1.向list的左边插入以下三个手机号:18511310001、18511310002、18511310003 jedis.lpush("tel_list","18511310001","18511310002","18511310003"); // 2.从右面移除一个手机号码 jedis.rpop("tel_list"); // 3.获取list所有的值 List<String> telList = jedis.lrange("tel_list",0,-1); for (String tel : telList) { System.out.println(tel); } }
注意:
List可以用来存储重复的元素,而且是有序的
获取所有的元素,lrange(key, 0, -1)
5.7 操作set类型的数据
使用set来保存uv值,为了方便计算,将用户名保存到uv中。
往一个set中添加页面 page1 的uv,用户user1访问一次该页面
user2访问一次该页面
user1再次访问一次该页面
最后获取 page1的uv值
/** * 操作set类型的数据 */ @Test public void setTest(){ // 从redis连接池获取redis连接 Jedis jedis = jedisPool.getResource(); // 求UV就是求独立有多少个不重复 // 1.往一个set中添加页面page1的uv,用户user1访问一次该页面 jedis.sadd("uv","user1"); // 2.user2访问一次该页面 jedis.sadd("uv","user2"); // 3.user1访问一次该页面 jedis.sadd("uv","user1"); // 最后获取page1的uv值 System.out.println("uv:" + jedis.scard("uv")); jedis.close(); }
注意:
计算UV主要是去重
将来所有的一些要求高效率去重的业务场景,都可以使用Set操作
6. Redis的持久化
由于redis是一个内存数据库,所有的数据都是保存在内存当中的,内存当中的数据极易丢失,所以redis的数据持久化就显得尤为重要,在redis当中,提供了两种数据持久化的方式,分别为RDB以及AOF,且Redis默认开启的数据持久化方式为RDB方式。
6.1 RDB持久化方案
6.1.1 介绍
Redis会定期保存数据快照至一个rdb文件中,并在启动时自动加载rdb文件,恢复之前保存的数据。可以在配置文件中配置Redis进行快照保存的时机:
save [seconds] [changes]
意为在seconds秒内如果发生了changes次数据修改,则进行一次RDB快照保存,例如
save 60 100
会让Redis每60秒检查一次数据变更情况,如果发生了100次或以上的数据变更,则进行RDB快照保存。可以配置多条save指令,让Redis执行多级的快照保存策略。Redis默认开启RDB快照。也可以通过SAVE或者BGSAVE命令手动触发RDB快照保存。 SAVE 和 BGSAVE 两个命令都会调用 rdbSave 函数,但它们调用的方式各有不同:
SAVE 直接调用 rdbSave ,阻塞 Redis 主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。
BGSAVE 则 fork 出一个子进程,子进程负责调用 rdbSave ,并在保存完成之后向主进程发送信号,通知保存已完成。 Redis 服务器在BGSAVE 执行期间仍然可以继续处理客户端的请求。
6.1.2 RDB方案优点
对性能影响最小。如前文所述,Redis在保存RDB快照时会fork出子进程进行,几乎不影响Redis处理客户端请求的效率。
每次快照会生成一个完整的数据快照文件,所以可以辅以其他手段保存多个时间点的快照(例如把每天0点的快照备份至其他存储媒介中),作为非常可靠的灾难恢复手段。
使用RDB文件进行数据恢复比使用AOF要快很多
6.1.3 RDB方案缺点
快照是定期生成的,所以在Redis crash时或多或少会丢失一部分数据
如果数据集非常大且CPU不够强(比如单核CPU),Redis在fork子进程时可能会消耗相对较长的时间,影响Redis对外提供服务的能力
6.1.4 RDB配置
修改redis的配置文件
cd /opt/modules/redis-3.2.8 vim redis.conf # 第202行 save 900 1 save 300 10 save 60 10000 save 5 1
这三个选项是redis的配置文件默认自带的存储机制。表示每隔多少秒,有多少个key发生变化就生成一份dump.rdb文件,作为redis的快照文件
例如:save 60 10000 表示在60秒内,有10000个key发生变化,就会生成一份redis的快照
重新启动redis服务
每次生成新的dump.rdb都会覆盖掉之前的老的快照
ps -ef | grep redis bin/redis-cli -h bigdata-pro-m07 shutdown bin/redis-server redis.conf
6.2 AOF持久化方案
采用AOF持久方式时,Redis会把每一个写请求都记录在一个日志文件里。在Redis重启时,会把AOF文件中记录的所有写操作顺序执行一遍,确保数据恢复到最新。
AOF默认是关闭的,如要开启,进行如下配置:
# 第594行 appendonly yes
6.2.3 配置AOF
AOF提供了三种fsync配置:always/everysec/no,通过配置项[appendfsync]指定:
appendfsync no:不进行fsync,将flush文件的时机交给OS决定,速度最快
appendfsync always:每写入一条日志就进行一次fsync操作,数据安全性最高,但速度最慢
appendfsync everysec:折中的做法,交由后台线程每秒fsync一次
6.2.4 AOF rewrite
随着AOF不断地记录写操作日志,因为所有的写操作都会记录,所以必定会出现一些无用的日志。大量无用的日志会让AOF文件过大,也会让数据恢复的时间过长。不过Redis提供了AOF rewrite功能,可以重写AOF文件,只保留能够把数据恢复到最新状态的最小写操作集。
AOF rewrite可以通过BGREWRITEAOF命令触发,也可以配置Redis定期自动进行:
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb
Redis在每次AOF rewrite时,会记录完成rewrite后的AOF日志大小,当AOF日志大小在该基础上增长了100%后,自动进行AOF rewrite
auto-aof-rewrite-min-size最开始的AOF文件必须要触发这个文件才触发,后面的每次重写就不会根据这个变量了。该变量仅初始化启动Redis有效。
6.2.5 AOF优点
最安全,在启用appendfsync为always时,任何已写入的数据都不会丢失,使用在启用appendfsync everysec也至多只会丢失1秒的数据
AOF文件在发生断电等问题时也不会损坏,即使出现了某条日志只写入了一半的情况,也可以使用redis-check-aof工具轻松修复
AOF文件易读,可修改,在进行某些错误的数据清除操作后,只要AOF文件没有rewrite,就可以把AOF文件备份出来,把错误的命令删除,然后恢复数据。
6.2.6 AOF的缺点
AOF文件通常比RDB文件更大
性能消耗比RDB高
数据恢复速度比RDB慢
Redis的数据持久化工作本身就会带来延迟,需要根据数据的安全级别和性能要求制定合理的持久化策略:
AOF + fsync always的设置虽然能够绝对确保数据安全,但每个操作都会触发一次fsync,会对Redis的性能有比较明显的影响
AOF + fsync every second是比较好的折中方案,每秒fsync一次
AOF + fsync never会提供AOF持久化方案下的最优性能
使用RDB持久化通常会提供比使用AOF更高的性能,但需要注意RDB的策略配置
6.3 RDB or AOF
每一次RDB快照和AOF Rewrite都需要Redis主进程进行fork操作。fork操作本身可能会产生较高的耗时,与CPU和Redis占用的内存大小有关。根据具体的情况合理配置RDB快照和AOF Rewrite时机,避免过于频繁的fork带来的延迟
Redis在fork子进程时需要将内存分页表拷贝至子进程,以占用了24GB内存的Redis实例为例,共需要拷贝48MB的数据。在使用单Xeon 2.27Ghz的物理机上,这一fork操作耗时216ms。
7. Redis 高级使用
7.1 Redis 事务
7.1.1 Redis事务简介
Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
总结说:Redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
Redis事务没有隔离级别的概念:
批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。
Redis不保证原子性:
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
一个事务从开始到执行会经历以下三个阶段:
第一阶段:开始事务
第二阶段:命令入队
第三阶段、执行事务
Redis事务相关命令:
MULTI:开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令队列
EXEC:执行事务中的所有操作命令
DISCARD:取消事务,放弃执行事务块中的所有命令
WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令
UNWATCH:取消WATCH对所有key的监视
7.1.2 Redis事务演示
MULTI开始一个事务:给k1、k2分别赋值,在事务中修改k1、k2,执行事务后,查看k1、k2值都被修改。
bigdata-pro-m07:6379> set k1 v1 OK bigdata-pro-m07:6379> set k2 v2 OK bigdata-pro-m07:6379> multi OK bigdata-pro-m07:6379> set k1 11 QUEUED bigdata-pro-m07:6379> set k2 22 QUEUED bigdata-pro-m07:6379> exec 1) OK 2) OK bigdata-pro-m07:6379> get k1 "11" bigdata-pro-m07:6379> get k2 "22"
- 事务失败处理:语法错误(编译器错误),在开启事务后,修改k1值为11,k2值为22,但k2语法错误,最终导致事务提交失败,k1、k2保留原值。
bigdata-pro-m07:6379> flushdb OK bigdata-pro-m07:6379> keys * (empty list or set) bigdata-pro-m07:6379> set k1 v1 OK bigdata-pro-m07:6379> set k2 v2 OK bigdata-pro-m07:6379> multi OK bigdata-pro-m07:6379> set k1 11 QUEUED bigdata-pro-m07:6379> sets k2 22 (error) ERR unknown command 'sets' bigdata-pro-m07:6379> exec (error) EXECABORT Transaction discarded because of previous errors. bigdata-pro-m07:6379> get k1 "v1" bigdata-pro-m07:6379> get k2 "v2"
Redis类型错误(运行时错误),在开启事务后,修改k1值为11,k2值为22,但将k2的类型作为List,在运行时检测类型错误,最终导致事务提交失败,此时事务并没有回滚,而是跳过错误命令继续执行, 结果k1值改变、k2保留原值。
bigdata-pro-m07:6379> flushdb OK bigdata-pro-m07:6379> keys * (empty list or set) bigdata-pro-m07:6379> set k1 v1 OK bigdata-pro-m07:6379> set k2 v2 OK bigdata-pro-m07:6379> multi OK bigdata-pro-m07:6379> set k1 11 QUEUED bigdata-pro-m07:6379> lpush k2 22 QUEUED bigdata-pro-m07:6379> exec 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value bigdata-pro-m07:6379> get k1 "11" bigdata-pro-m07:6379> get k2 "v2"
DISCARD取消事务
bigdata-pro-m07:6379> multi OK bigdata-pro-m07:6379> set k6 v6 QUEUED bigdata-pro-m07:6379> set k7 v7 QUEUED bigdata-pro-m07:6379> discard OK bigdata-pro-m07:6379> get k6 (nil) bigdata-pro-m07:6379> get k7 (nil)
7.1.3 为什么Redis不支持事务回滚?
多数事务失败是由语法错误或者数据结构类型错误导致的,语法错误说明在命令入队前就进行检测的,而类型错误是在执行时检测的,Redis为提升性能而采用这种简单的事务,这是不同于关系型数据库的,特别要注意区分。Redis之所以保持这样简易的事务,完全是为了保证高并发下的核心问题——性能。
7.2 Redis 过期策略
Redis是key-value数据库,可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
过期策略通常有以下三种:
定时过期
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。 极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期
每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
7.3 内存淘汰策略
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据,在Redis的配置文件中描述如下:
# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory # is reached. You can select among five behaviors: #最大内存策略:当到达最大使用内存时,你可以在下面5种行为中选择,Redis如何选择淘汰数据库键 #当内存不足以容纳新写入数据时 # volatile-lru -> remove the key with an expire set using an LRU algorithm # volatile-lru :在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把 redis 既当缓存,又做持久化存储的时候才用。 # allkeys-lru -> remove any key according to the LRU algorithm # allkeys-lru : 移除最近最少使用的key (推荐) # volatile-random -> remove a random key with an expire set # volatile-random : 在设置了过期时间的键空间中,随机移除一个键,不推荐 # allkeys-random -> remove a random key, any key # allkeys-random : 直接在键空间中随机移除一个键,弄啥叻 # volatile-ttl -> remove the key with the nearest expire time (minor TTL) # volatile-ttl : 在设置了过期时间的键空间中,有更早过期时间的key优先移除 不推荐 # noeviction -> don't expire at all, just return an error on write operations # noeviction : 不做过键处理,只返回一个写操作错误。 不推荐 # Note: with any of the above policies, Redis will return an error on write # operations, when there are no suitable keys for eviction. # 上面所有的策略下,在没有合适的淘汰删除的键时,执行写操作时,Redis 会返回一个错误。下面是写入命令: # At the date of writing these commands are: set setnx setex append # incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd # sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby # zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby # getset mset msetnx exec sort # 过期策略默认是: # The default is: # maxmemory-policy noeviction
实际项目中设置内存淘汰策略:maxmemory-policy allkeys-lru,移除最近最少使用的key。