Java客户端Jedis
1、搭建开发Redis的Java开发环境

1.1、在Windows下安装Java 8
Java是由Sun Microsystems公司(已被Orade公司收 购)于1995年5月推出的高级程序设计语言。Java可运行于多个平台,例如Windows、macOS,及其他多种UNIX版本的操作系统。
JDK是Java的软件开发工具包,主要用于移动设备、嵌入式设备上的Java应用程序开发。JDK是整个Java开发的核心,它包含Java的运行环境、Java工具和Java基础的类库。使用Java操作Redis,需要先按照以下步骤安装Java。
1.下载JDK 8
在Oracle官网,找到JDK 8的安装包,单击“AcceptLicense Agreement”,根据自己的操作系统下载对应的文件。本书的实例使用的操作系统为64位Windows 10,故下载Windows x64对应的安装包jdk-8u151-windows-x64.exe,如图所示。

双击安装包进行安装,选择自定义安装JDK 8,它的安装路径可以由读者自定义,笔者的安装路径放在了D:\installed_software\jdk1_8目录下。
安装时根据提示一步步操作就可以了,注意安装路径不要使用带有中文或空格的目录,避免在之后的使用过程中出现一些莫名的错误。
2.配置JDK 8
(1)右击“此电脑”,选择“属性”。在“系统”窗口左侧单击“高级系统设置”,打开“系统属性”对话框,如图7-2所示。在“系统属性”对话框的“高级”选项卡里单击“环境变量”按钮,弹出“环境变量”对话框,如图所示。

(2)新建名为JAVA_HOME的变量,变量的值为之前安装的JDK 8的路径:D:\installed_software\jdk1_8。
(3)新建名为CLASS_PATH的变量,变量的值可以设置为“%JAVA_HOME%/lib;.;”。
(4)在已有的系统变量Path的变量值前加上“%JAVA_HOME%/bin;.;”。
至此JDK 8配置完成,如图所示。

下面检验JDK 8是否配置成功。按“Win+R”组合键,在运行框中输入cmd命令,如图所示。

在命令提示符窗口中执行java-version命令,如果出现图所示的结果,则表明配置JDK 8成功了。

1.2、安装Tomcat 9
Tomcat服务器是一个免费的开放源代码的Web应用服务器,属于轻量级应用服务器,在中小型操作系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP程序的首选。
可以根据自己的操作系统,去Apache官网选择合适的版本下载,笔者下载的是Windows 64位版本的apache-tomcat-9.0.24.zip。将压缩包解压缩后放到指定目录就可以使用,无须安装。Tomcat 9(以下简称Tomcat)解压缩后的目录如图所示。

Tomcat关键目录及文件作用如下。
bin:用于存放各种平台下启动和关闭Tomcat的脚本文件。在该目录中有两个非常关键的文件startup.bat和shutdown.bat,前者是Windows下启动Tomcat的文件,后者是对应的关闭文件。conf:Tomcat的各种配置文件,其中server.xml为服务器的主配置文件,web.xml为所有Web应用的配置文件,tomcat-users.xml用于定义Tomcat的用户信息、配置用户的权限与安全。lib:此目录存放Tomcat和所有Web应用都能访问的JAR包。logs:用于存放Tomcat的日志文件,Tomcat的所有日志文件都存放在此目录中。temp:临时文件夹,Tomcat运行时如果有临时文件将保存于此目录中。webapps:Web应用的发布目录,把Java Web站点或WAR文件放入这个目录中,就可以通过Tomcat访问了。work:Tomcat解析JSP所生成的Servlet文件放在这个目录中。
启动Tomcat,在Tomcat的bin目录下找到startup.bat文件并双击运行。此时会弹出一个黑色窗口的控制台,如图所示。黑色窗口的控制台不要关闭,如果关闭就相当于把Tomcat关闭了。

在浏览器地址栏中输入http://localhost:8080或者http://127.0.0.1:8080,如果看到图所示界面,则证明Tomcat启动成功了。Tomcat默认的访问端口是8080。

1.3、搭建IntelliJ IDEA开发环境
俗话说,“工欲善其事,必先利其器”。在做Java开发之前,一个好的IDEA会使编程效率得到大幅度提高。IntelliJ IDEA是目前最流行的Java开发环境之一。本小节主要介绍IntelliJ IDEA开发环境的安装和配置。IntelliJIDEA不但可以开发Java应用,还可以作为Java源代码的 阅读器。
1.安装IntelliJ IDEA
IntelliJ IDEA是用于Java开发的集成环境(也可用于其他语言)。IntelliJ IDEA在业界被公认为最好的Java开发环境之一,尤其在智能代码助手、代码自动提示、重构、Java EE支持、Ant、JUnit、CVS整合、代码审查、创新的GUI设计等方面的功能可以说是超常的。
IntelliJ IDEA官网上对不同的操作系统(Windows、macOS、Linux)都有两个版本可供下载。Ultimate为旗舰版,功能全面,插件丰富,但是按年收费,功能上类似于MyEclipse。
Community为社区版,免费使用,功能相对而言不是很丰富,但是不影响开发使用,功能上类似于Eclipse。本书选用IntelliJ IDEA社区版作为Java开发环境。IntelliJ IDEA社区版的安装很简单,按照提示一步步在Windows上操作就可以安装成功。
IntelliJ IDEA下载页面如图所示。

2、使用Java操作Redis
2.1、连接Redis的两种方式
使用IDEA新建Maven项目,需要在Maven项目中导入pom.xml文件。
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
导入的pom.xml文件实际引入了如下JAR包,如图所示。

1.Jedis直连
Jedis直连相当于一个TCP连接,数据传输完成后关闭连接,如图所示。

package com.dxtd.demo;
import redis.clients.jedis.Jedis;
public class RedisDemo {
public static void main(String[] args){
//生成一个Jedis对象,这个对象负责和指定的Redis节点进行通信
Jedis jedis = new Jedis("127.0.0.1", 6379);
//jedis执行set命令
jedis.set("name","xinping");
//jedis执行get命令
String name = jedis.get("name");
System.out.println(name);
jedis.close();
}
}
该程序的输出如下。
xinping
打开一个终端,并使用redis-cli命令连接到Redis服务器。
127.0.0.1:6379> KEYS *
1) "name"
127.0.0.1:6379> GET name
"xinping"
可以看到数据已经保存到Redis服务器上了。
2.Jedis连接池连接
使用Jedis连接池可以不用创建新的Jedis对象连接Redis服务器,这大大减少创建和回收Redis连接的开销,如图所示。

下表所示是使用Jedis直连和Jedis连接池连接这两种方案的对比。

package com.dxtd.demo;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisPoolDemo {
public static void main(String[] args){
//创建连接池配置对象
JedisPoolConfig jpc = new JedisPoolConfig();
//设置最大闲置个数
jpc.setMaxIdle(30);
//设置最小闲置个数
jpc.setMinIdle(10);
//设置最大连接数
jpc.setMaxTotal(50);
//创建连接池对象。host为连接Redis主机IP地址;port为Redis的默认端口
JedisPool jedisPool = new JedisPool(jpc, "127.0.0.1", 6379);
Jedis resource = null;
try{
//获取连接资源
resource = jedisPool.getResource();
//放值
resource.set("name", "hello redis");
//取值
String name = resource.get("name");
System.out.println("name=" + name);
}catch(Exception e){
e.printStackTrace();
}finally {
if (resource != null) {
//这里使用的close不代表关闭连接,指的是归还资源
jedisPool.close();
}
}
}
}
该程序输出如下。
name=hello redis
2.2、操作String
在本例中使用Jedis的set()方法添加数据,使用append()方法拼接数据,使用del()方法删除某个key,使用mset()方法设置多个key-value键值对,使用incr()方法对key进行加1操作。
public class StringDemo {
public static void main(String[] args) {
// 连接Redis服务器,127.0.0.1:6379
Jedis jedis = new Jedis("127.0.0.1", 6379);
// -----添加数据----------
jedis.set("name", "wang");
System.out.println(jedis.get("name"));// 执行结果:wang
jedis.append("name", " is my friend"); // 拼接
System.out.println(jedis.get("name"));
jedis.del("name"); // 删除某个key
System.out.println(jedis.get("name"));
// 设置多个key-value键值对
jedis.mset("name", "xinping", "age", "35", "qq", "759949947");
jedis.incr("age"); // 进行加1操作
System.out.println(jedis.get("name") + "-" + jedis.get("age") + "-" +
jedis.get("qq"));
jedis.close();
}
}
该程序输出如下。
wang
wang is my friend
null
xinping-36-759949947
2.3、操作Map
在本例中使用Jedis的hmset()方法添加多个数据,使用hmget()方法得到多个key的值,使用hdel()方法删除某个key,使用hkeys()方法返回Map中的所有key,使用hvals()方法返回Map中的所有value。
public static void testMap() {
// -----添加数据----------
Map<String, String> map = new HashMap<String, String>();
map.put("name", "wang");
map.put("age", "22");
map.put("qq", "123456");
// 连接Redis服务器,127.0.0.1:6379
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.hmset("user", map);
// 取出user中的name,执行结果:[minxr]。注意,结果是一个泛型的List
// 第一个参数是存入Redis中Map的key,后面跟的是放入Map中的key,后面的key可以
跟多个,是可变参数
List<String> rsmap = jedis.hmget("user", "name", "age", "qq");
// 删除Map中的某个key
jedis.hdel("user", "age");
System.out.println(jedis.hmget("user", "age")); // 因为删除了,所以返回的是null
System.out.println(jedis.hlen("user")); // 返回key为user的键中存放的值的个数2
System.out.println(jedis.exists("user"));// 是否存在key为user的记录,存在
则返回true
System.out.println(jedis.hkeys("user"));// 返回Map中的所有key
System.out.println(jedis.hvals("user"));// 返回Map中的所有value
Iterator<String> iter = jedis.hkeys("user").iterator();
while (iter.hasNext()) {
String key = iter.next();
System.out.println(key + ":" + jedis.hmget("user", key));
}
jedis.close();
}
该程序输出如下。
[null]
2
true
[name, qq]
[wang, 123456]
name:[wang]
qq:[123456]
2.4、操作List
在本例中使用Jedis的lpush()方法向key添加列表数据,使用lrange()方法遍历列表中的数据。
public static void testList() {
// 连接Redis服务器,127.0.0.1:6379
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 开始操作前,先删除key为“java framework”的所有内容
jedis.del("java framework");
System.out.println(jedis.lrange("java framework", 0, -1));
// 先向key为“java framework”的列表按照从左到右的顺序存放3条数据
jedis.lpush("java framework", "spring");
jedis.lpush("java framework", "struts");
jedis.lpush("java framework", "hibernate");
// jedis.lrange()返回列表中指定区间内的元素,其中0表示列表的第一个元素,-1表示列
表的最后一个元素
System.out.println(jedis.lrange("java framework", 0, -1));
// 开始操作前,先移除key为“java framework”的所有内容
jedis.del("java framework");
// 再向key为"java framework"的列表尾部(最右边)存放三条数据
jedis.rpush("java framework", "spring");
jedis.rpush("java framework", "struts");
jedis.rpush("java framework", "hibernate");
System.out.println(jedis.lrange("java framework", 0, -1));
jedis.close();
}
该程序输出如下。
[]
[hibernate, struts, spring]
[spring, struts, hibernate]
2.5、操作Set
在本例中使用Jedis的sadd()方法添加元素,使用smembers()方法获取所有加入的value,使用sismember()方法判断集合中的元素是否存在。
public static void testSet() {
// 连接Redis服务器,127.0.0.1:6379
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 添加元素
jedis.sadd("usernames", "lisi","wangwu","xinping","zhangsan");
// 获取所有加入的value
System.out.println(jedis.smembers("usernames"));
// 判断who是不是user集合的元素
System.out.println(jedis.sismember("usernames", "who"));
System.out.println(jedis.srandmember("usernames"));
// 返回集合的元素个数
System.out.println(jedis.scard("usernames"));
jedis.close();
}
该程序输出如下。
[zhangsan, lisi, wangwu, xinping]
false
xinping
4
2.6、排序
在本例中使用Jedis的rpush()方法、lpush方法向列表添加数据,然后使用sort()方法进行排序,最后使用lrange()方法显示排序后的列表。
public static void testSort() {
// 连接Redis服务器,127.0.0.1:6379
Jedis jedis = new Jedis("127.0.0.1", 6379);
// Jedis排序
// 注意,此处的rpush()和lpush()是对List的操作,是一个双向链表
jedis.del("a");// 先清除数据,再加入数据进行测试
jedis.rpush("a", "1");
jedis.lpush("a", "6");
jedis.lpush("a", "3");
jedis.lpush("a", "9");
System.out.println(jedis.lrange("a", 0, -1));// [9, 3, 6, 1]
System.out.println(jedis.sort("a")); //输出排序后的结果
System.out.println(jedis.lrange("a", 0, -1));
}
该程序输出如下。
[9, 3, 6, 1]
[1, 3, 6, 9]
[9, 3, 6, 1]
2.7、Redis存储图片
本小节使用Redis来存储一张图片。首先从网络下载一张图片,然后使用Base64对图片进行编码,再使用Base64对图片进行解码,最后将图片保存到本地E盘根目录下。
在百度图片中搜索熊猫,在搜索结果中可以看到一张可爱的大熊猫图片,如图所示。我们要把这张图片存储在Redis中。

1.使用Base64对图片进行编码和解码
package com.dxtd.demo;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import javax.imageio.ImageIO;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
public class Base64ImageUtils {
/**
* 对网络图片进行Base64编码
*
* @param imageUrl
* 图片的URL路径,例如http://www.aaa.com/b.jpg
* @return
*/
public static String encodeImgageToBase64(URL imageUrl) {// 将图片文件转化为
字节数组字符串,并对其进行Base64编码处理
ByteArrayOutputStream outputStream = null;
try {
BufferedImage bufferedImage = ImageIO.read(imageUrl);
outputStream = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "jpg", outputStream);
} catch (MalformedURLException e1) {
e1.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 对字节数组字符串进行Base64编码
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(outputStream.toByteArray());// 返回Base64编码过的
字节数组字符串
}
/**
* 将本地图片进行Base64编码
*
* @param imageFile
* 图片的URL路径,例如F:/.....xx.jpg
* @return
*/
public static String encodeImgageToBase64(File imageFile) {// 将图片文件转化
为字节数组字符串,并对其进行Base64编码处理
ByteArrayOutputStream outputStream = null;
try {
BufferedImage bufferedImage = ImageIO.read(imageFile);
outputStream = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "jpg", outputStream);
} catch (MalformedURLException e1) {
e1.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// 对字节数组字符串进行Base64编码
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(outputStream.toByteArray());// 返回Base64编码过的
字节数组字符串
}
/**
* 对Base64编码过的图片进行解码,并保存到指定目录
*
* @param base64
* Base64编码的图片信息
* @return
*/
public static void decodeBase64ToImage(String base64, String path,
String imgName) {
BASE64Decoder decoder = new BASE64Decoder();
try {
FileOutputStream write = new FileOutputStream(new File(path
+ imgName));
byte[] decoderBytes = decoder.decodeBuffer(base64);
write.write(decoderBytes);
write.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String [] args){
URL url = null;
try {
url = new URL("https://graph.baidu.com/thumb/691874602,911901597.jpg");
} catch (MalformedURLException e) {
e.printStackTrace();
}
String encoderStr = Base64ImageUtils.encodeImgageToBase64(url);
System.out.println(encoderStr);
Base64ImageUtils.decodeBase64ToImage(encoderStr, "E:/", "pandas.jpg");
}
}
在控制台查看Base64编码后的图片信息,如图所示。

本程序将网络上的大熊猫图片保存为本地磁盘E下的pandas.jpg。下载的图片如图所示,和网络上的图片是一样的。

图片转化成String需要注意以下两点。
- 我们可以在Redis中存储图片的Base64编码或者解码,以key-value键值对格式存储,key为普通字符串,value为图片的Base64编码,获取key后再使用Base64解码就可以了。
- 我们也可以在Redis中存储图片的网络URL地址或者本地的Path路径。图片可以存储到磁盘中,然后在Redis中以图片的网络URL地址或者本地的Path路径作为value进行存储。
2.图片转化成Object
要将图片转化成Object,可直接在Redis中存储图片,使用Java的序列化/反序列化机制进行处理。本实例使用“ImageRedisTest.java”,内容如下。
package com.dxtd.demo;
import java.net.MalformedURLException;
import java.net.URL;
import redis.clients.jedis.Jedis;
public class ImageRedisTest {
private static Jedis jedis = null;
/**
* 初始化Jedis对象
*
* @throws Exception
*/
public ImageRedisTest() {
jedis = new Jedis("127.0.0.1", 6379);
}
/**
* 将图片写入Redis中
*
*/
public void saveImage() {
URL url = null;
try {
url = new URL("https://graph.baidu.com/thumb/691874602,911901597.jpg");
} catch (MalformedURLException e) {
e.printStackTrace();
}
String encoderStr = Base64ImageUtils.encodeImgageToBase64(url);
System.out.println(encoderStr);
jedis.set("image:e:/pandas.jpg".getBytes(), encoderStr.getBytes());
}
/**
* 从 Redis中获得图片,保存到本地
*
*/
public void getImage() {
System.out.println(jedis.get(("image:e:/pandas.jpg")));
String encoderStr = jedis.get("image:e:/pandas.jpg");
Base64ImageUtils.decodeBase64ToImage(encoderStr, "E:/", "pandas2.jpg");
}
public static void main(String[] args) {
ImageRedisTest test = new ImageRedisTest();
test.saveImage();
//test.getImage();
}
}
运行程序,使用redis-cli命令连接Redis服务器,可以看到在Redis数据库里已经存在key为image:e:/pandas.jpg了。获取这个key的值,如图所示,可以看到已经成功把图片存储到Redis了。

2.8、Redis存储Object
Redis存储Object(对象)的时候,要进行对象的序列化与反序列化操作。本实例使用“Person.java”,保存用户数据,内容如下。
package com.dxtd.demo;
import java.io.Serializable;
public class Person implements Serializable {
private int id;
private String name;
public Person() {
}
public Person(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.dxtd.demo;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SerializeUtil {
/**
* 序列化
*
* @param object
* @return
*/
public static byte[] serialize(Object object) {
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(object);
byte[] bytes = baos.toByteArray();
return bytes;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 反序列化
*
* @param bytes
* @return
*/
public static Object unserialize(byte[] bytes) {
ByteArrayInputStream bais = null;
try {
bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
package com.dxtd.demo;
import redis.clients.jedis.Jedis;
public class PersonRedisTest {
private static Jedis jedis = null;
/**
* 初始化Jedis对象
*
* @throws Exception
*/
public PersonRedisTest() {
jedis = new Jedis("127.0.0.1", 6379);
}
/**
* 序列化写对象,将Person对象写入Redis中
*
*/
public void setObject() {
jedis.set("person:100".getBytes(),
SerializeUtil.serialize(new Person(100, "zhangsan")));
jedis.set("person:101".getBytes(),
SerializeUtil.serialize(new Person(101, "xinping")));
}
/**
* 反序列化取对象,用Jedis获取对象
*
*/
public void getObject() {
byte[] data100 = jedis.get(("person:100").getBytes());
Person person100 = (Person) SerializeUtil.unserialize(data100);
System.out.println(String.format("person:100->id=%s,name=%s",
person100.getId(), person100.getName()));
byte[] data101 = jedis.get(("person:101").getBytes());
Person person101 = (Person) SerializeUtil.unserialize(data101);
System.out.println(String.format("person:101->id=%s,name=%s",
person101.getId(), person101.getName()));
}
public static void main(String[] args) {
PersonRedisTest rt = new PersonRedisTest();
rt.setObject();
rt.getObject();
}
}
该程序输出如下。
person:100->id=100,name=zhangsan
person:101->id=101,name=xinping
查看Redis存储的对象,如图所示,可以看出已经把Java对象存储到Redis了。

2.9、Redis存储和计算用户访问量
Redis存储和计算用户访问量有这样一个场景,我们需要统计在2020年8月6日22点到23点之间系统某个页面的访客数,指的是某个用户不管访问这个页面几次都算作访问页面一次是不重复计数的。key设置为user:login:2020080622。这种场景下适合使用HyperLogLog来存储和计算用户访问量,HyperLogLog使用统计概率的算法,牺牲数据的精准性来节省内存的占用空间。
public static void testHyperLogLog(){
Jedis jedis = new Jedis("127.0.0.1",6379);
// 模拟在2020年8月6日22点,存储100万条用户访问数据
for (int i = 0; i < 100 * 10000; i++) {
jedis.pfadd("user:login:2020080622", "user-"+ i );
}
long total = jedis.pfcount("user:login:2020080622");
System.out.printf("key=user:login:202008062200 count=%d", total);
jedis.close();
}
运行程序,输出如下。
key=user:login:2020080622 count=1003993
从返回结果可以看出,使用HyperLogLog存储100万条用户访问数据,计算用户访问量多出了3993条,存在一定的误差。
然后再通过redis-rdb-tools统计user:login:2020080622这个key的信息,发现它只占用14400Byte的硬盘空间,也就是14KB。
$ rdb -c memory /usr/local/redis/bin/dump.rdb
database,type,key,size_in_bytes,encoding,num_elements,len_largest_element,expiry
0,string,user:login:2020080622,14400,string,12304,12304,
所以HyperLogLog适合统计用户访问量、日活跃用户量、月活跃用户量此类对精确度要求不高的场景。
3、Redis调用方式
3.1、普通同步
普通同步是一种最简单和最基础的调用方式,对于简单的数据存取需求,可以通过这种方式调用。
public static void jedisNormal() {
Jedis jedis = new Jedis("localhost");
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
String result = jedis.set("n:" + i, ""+i);
}
long end = System.currentTimeMillis();
System.out.println("Simple SET: " + ((end - start)/1000.0) + " seconds");
jedis.disconnect();
}
该程序输出如下。
Simple SET: 0.031 seconds
3.2、事务
Redis事务可以一次执行多个命令,有以下两个特性。
隔离性:事务的所有命令都会被序列化,按顺序执行,事务执行完后才会执行其他客户端的命令。原子性:事务中的命令要么全部被执行,要么全部不执行。
public static void jedisTrans() {
Jedis jedis = new Jedis("localhost");
long start = System.currentTimeMillis();
// 开启事务
Transaction tx = jedis.multi();
for (int i = 0; i < 100; i++) {
tx.set("t:" + i, i + "");
}
// 提交事务
List<Object> results = tx.exec();
long end = System.currentTimeMillis();
System.out.println("Transaction SET: " + ((end - start)/1000.0) + " seconds");
jedis.disconnect();
}
该程序输出如下。
Transaction SET: 0.047 seconds
3.3、管道
管道是一种两个进程之间进行单向通信的机制。在Redis中有时候我们需要采用异步的方式,一次发送多个命令,并且不同步等待其返回结果,这样可以取得非常好的执行效率。
public static void jedisPipelined() {
Jedis jedis = new Jedis("localhost");
Pipeline pipeline = jedis.pipelined();
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
pipeline.set("p:" + i, "" + i);
}
List<Object> results = pipeline.syncAndReturnAll();
long end = System.currentTimeMillis();
System.out.println("Pipelined SET: " + ((end - start)/1000.0) + " seconds");
jedis.disconnect();
}
该程序输出如下。
Pipelined SET: 0.051 seconds
3.4、管道中调用事务
有时候我们需要异步执行命令,但是又希望多个命令是连续的,所以我们就采用管道中调用事务的方式。Jedis支持在管道中调用事务。
public static void jedisCombPipelineTrans() {
Jedis jedis = new Jedis("localhost");
long start = System.currentTimeMillis();
Pipeline pipeline = jedis.pipelined();
pipeline.mul ti();
for (int i = 0; i < 100000; i++) {
pipeline.set("" + i, "" + i);
}
pipeline.exec();
List<Object> results = pipeline.syncAndReturnAll();
long end = System.currentTimeMillis();
System.out.println("Pipelined transaction: " + ((end - start) / 1000.0) +
" seconds");
jedis.disconnect();
}
该程序输出如下。
Pipelined transaction: 0.463 seconds
4、Redis集群与Java
本节使用Jedis连接一个Redis集群,一个Redis集群环境由6个节点组成,每个节点有不同的端口,有3个主节点和3个从节点。
public static void testJedisCluster() {
JedisPoolConfig config = new JedisPoolConfig();
config = new JedisPoolConfig();
config.setMaxTotal(60000); // 设置最大连接数
config.setMaxIdle(1000); // 设置最大空闲数
config.setMaxWaitMillis(3000); // 设置超时时间
config.setTestOnBorrow(true);
HashSet<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.11.15", 8001));
nodes.add(new HostAndPort("192.168.11.15", 8002));
nodes.add(new HostAndPort("192.168.11.15", 8003));
nodes.add(new HostAndPort("192.168.11.15", 8004));
nodes.add(new HostAndPort("192.168.11.15", 8005));
nodes.add(new HostAndPort("192.168.11.15", 8006));
JedisCluster cluster = new JedisCluster(nodes, config);
cluster.set("book", "redis");
System.out.println("集群测试 key=book,value=" + cluster.get("book"));
cluster.close();
}
程序运行返回以下结果。
集群测试 key=book , value=redis
使用redis-cli-c-h 192.168.11.15-p 8001连接Redis集群,可以看到数据已经成功插入,如图所示。

5、实例1:使用Redis获取用户的共同好友
可以使用Redis来存储社交关系,例如有这样一个需求:在微博中zhangsan有一批好友,lisi有另外一批好友,现在需要查询zhangsan和lisi的共同好友。为了实现这个需求,可以使用Redis的Set数据类型。Set类型是String类型的无序集合,集合中的元素是唯一的,这表示集合中不能出现重复的元素。可以使用Redis的SINTER key命令获得给定集合的交集。
5.1、初始化数据
首先,使用redis-cli命令连接Redis服务器,执行以下命令。
# 清空当前数据库
127.0.0.1:6379> FLUSHDB
OK
# 添加用户zhangsan 拥有的好友集合
127.0.0.1:6379> SADD zhangsan "friend_1"
(integer) 1
127.0.0.1:6379> SADD zhangsan "friend_2"
(integer) 1
127.0.0.1:6379> SADD zhangsan "friend_3"
(integer) 1
# 添加用户lisi拥有的好友集合
127.0.0.1:6379> SADD lisi "friend_1"
(integer) 1
127.0.0.1:6379> SADD lisi "friend_3"
(integer) 1
127.0.0.1:6379> SADD lisi "friend_5"
(integer) 1
# 显示用户zhangsan的好友集合
127.0.0.1:6379> SMEMBERS zhangsan
1) "friend_2"
2) "friend_1"
3) "friend_3"
# 显示用户lisi的好友集合
127.0.0.1:6379> SMEMBERS lisi
1) "friend_5"
2) "friend_1"
3) "friend_3"
# 获取zhangsan和lisi的共同好友
127.0.0.1:6379> SINTER zhangsan lisi
1) "friend_1"
2) "friend_3"
5.2、使用Jedis获取用户的共同好友
使用Jedis完成同样的操作,即获取zhangsan和lisi的共同好友。本实例使用“TestSNS.java”,内容如下。
package com.redis;
import redis.clients.jedis.Jedis;
public class TestSNS {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
//清空当前数据库
jedis.flushDB();
//zhangsan的好友
jedis.sadd("zhangsan", "friend_1");
jedis.sadd("zhangsan", "friend_2");
jedis.sadd("zhangsan", "friend_3");
System.out.println("zhangsan的好友 =>"+ jedis.smembers("zhangsan"));
//lisi的好友
jedis.sadd("lisi", "friend_1");
jedis.sadd("lisi", "friend_3");
jedis.sadd("lisi", "friend_5");
System.out.println("lisi的好友 =>"+ jedis.smembers("lisi"));
//获取zhangsan和lisi的共同好友
System.out.println("zhangsan和lisi的共同好友 =>"+ jedis.sinter("zhangsan","lisi"));
//释放资源
jedis.close();
}
}
运行程序得到结果,和在Redis中的统计结果是一样的。
zhangsan的好友 =>[friend_3, friend_2, friend_1]
lisi的好友 =>[friend_3, friend_1, friend_5]
zhangsan和lisi的共同好友 =>[friend_3, friend_1]
6、实例2:在Tomcat上使用Redis保存Session
当用户访问量大和应用服务器使用集群来部署时,使用Tomcat默认自带的Session就不能满足需求了。当然解决方法有很多,本节提供了一个解决方法,就是使用Redis来保存Session。好处就是使用Session的代码没有任何变化,Tomcat默认把Session保存到Redis中,把Redis缓存作为一个Session的存储系统。我们先看一下分布式Session的概念。
6.1、分布式Session
单服务器Web应用中,Session信息只存在该服务器中,这是前几年的流行方式。但是近几年随着分布式系统的流行,单系统已经不能满足日益增长的百万级用户的需求了,集群方式部署服务器已在很多公司运用起来。当高并发量的请求到达服务器的时候通过负载均衡的方式发送到集群中的某个服务器,这样就可能导致同一个用户的多次请求被发送到集群的不同服务器上,就会出现取不到Session数据的情况,于是Session的共享就成了一个问题。
如图所示,假设用户包含登录信息的Session都记录在Web-Server1上,反向代理如果将请求路由到Web-Server2上,就找不到相关登录信息,从而导致用户需要重新登录。

分布式系统Session一致性的问题,可以采用Session复制(同步)来解决,持久化Session到Redis中,这样每个Web-Server都包含全部的Session。Web-Server可以支持Web应用原有的功能,不需要修改代码,如图所示。

6.2、持久化Tomcat Session到Redis
HTTP是无状态协议,这意味着每次客户端访问网页时,都要单独打开一个服务器,因此服务器不会记录先前客户端请求的任何信息。
Session表示客户端与服务器的一次会话。从客户端打开浏览器并连接到服务器开始,到客户端关闭浏览器离开这个服务器结束,被称为一个会话。当一个客户端访问一个服务器时,可能会在服务器的几个页面之间切换,服务器可以通过Session来保存客户端的用户信息,如果用户关闭了浏览器,那么Session就会丢失。改进方法是把Redis作为Tomcat服务器的缓存,保存Session。
实验环境基于CentOS 7,实验环境需要的Nginx、Tomcat和JDK的安装请参考以前内容。实验环境如表所示。

Redis作为缓存服务器的实验拓扑如图所示。

这里搭建一个有两台Tomcat服务器的小集群。在图7-21中,Nginx作为反向代理,实现动静分离的功能,将客户端动态请求根据权重(Weight)随机分配给两台Tomcat服务器,Redis作为两台Tomcat服务器的共享Session数据服务器。为了保证网络的连通性,把两台Linux主机的防火墙全部关闭。
6.3、安装服务器Tomcat和反向代理服务器Nginx
1.安装服务器Tomcat
首先在两台计算机上安装JDK 13,并设置系统变量。具体操作如下。
(1)在Linux上安装Java。
首先下载JDK 13。在Oracle官网找到JDK 13的下载地址。在下载页面中单击“Accept License Agreement”,并根据自己的操作系统选择对应的版本。本小节以CentOS 7 64位操作系统为例,下载Linux版本的Java压缩包jdk-13_linux-x64_bin.tar.gz,如图所示。

在/usr/local目录下创建java文件夹。
$ mkdir -p /usr/local/java
把jdk-13_linux-x64_bin.tar.gz上传到CentOS 7服务器。手动解压缩JDK 13的压缩包jdk-13_linux-x64_bin.tar.gz,然后设置系统变量。
$ tar -zxvf jdk-13_linux-x64_bin.tar.gz
复制解压缩的JDK 13到/usr/local/java目录下。
$ mv jdk-13 /usr/local/java
使用vi命令修改/etc/profile配置文件,设置系统变量。
$ vi /etc/profile
在/etc/profile配置文件中添加如下内容。
export JAVA_HOME=/usr/local/java
export PATH=$JAVA_HOME/bin:.:$PATH:
export CLASS_PATH=$JAVA_HOME/lib:.:
让修改的/etc/profile配置文件生效。
$ source /etc/profile
/usr/local/java是笔者在本机上安装JDK 13的位置,在Linux上安装的所有软件都推荐部署在/usr/local目录下。在Linux下路径之间使用冒号(:)分隔。
验证JDK 13有效性。
[root@localhost upload]# java -version
java version "13" 2019-09-17
Java(TM) SE Runtime Environment (build 13+33)
Java HotSpot(TM) 64-Bit Server VM (build 13+33, mixed mode, sharing)
(2)安装Tomcat 7。
在Apache官网下载Tomcat 7的Linux版本压缩包apache-tomcat-7.0.96.tar.gz,如图所示。

然后解压缩Tomcat 7的压缩包并授予执行权限。
$ tar -zxvf apache-tomcat-7.0.96.tar.gz
$ chmod -R 777 apache-tomcat-7.0.96
启动Tomcat 7,进入解压缩后的Tomcat 7目录。
$ cd apache-tomcat-7.0.96/bin
$ ./startup.sh
访问http://127.0.0.1:8080,如果看到图所示页面就说明启动Tomcat 7成功了。

停止Tomcat 7可以使用如下命令。
$ ./shutdown.sh
2.安装反向代理服务器Nginx
Nginx是一个高性能的HTTP和反向代理Web服务器,同时也提供了IMAP/POP3/SMTP服务。在Nginx官网下载最新的Nginx的Linux版本压缩包nginx-1.17.4.tar.gz,如图所示。

要做到负载均衡,就需要使用代理服务器将接收的请求均衡地分发到各服务器中。这里的代理服务器就是Nginx,分发请求的服务器使用的是Tomcat。
在主机192.168.11.14上安装Nginx,具体步骤如下。
(1)解压缩Nginx压缩包。
$ tar –zxvf nginx-1.17.4.tar.gz
在安装Nginx之前先检查是否已安装Nginx的一些模块依赖的lib库,例如g++、gcc、pcre-devel、openssl-devel和zlib-devel。
$ yum -y install gcc-c++ pcre pcre-devel zlib zlib-devel openssl openssl-
devel --setopt=protected_multilib=false
(2)编译Nginx源代码。
把Nginx安装到/usr/local/nginx目录下。
$ cd nginx-1.17.4
$ ./configure --prefix==/usr/local/nginx
编译Nginx。
$ make
安装Nginx。
$ make install
安装Nginx成功后,查看Nginx的安装目录,即/usr/local/nginx目录,如图所示。

安装Nginx后,查看Nginx版本。
[root@localhost local]# /usr/local/nginx/sbin/nginx -v
nginx version: nginx/1.17.4
(3)启动Nginx。
$ /usr/local/nginx/sbin/nginx
然后在浏览器中访问http://127.0.0.1查看Nginx是否启动成功。Nginx默认占用的端口是80,HTTP默认使用的端口也是80,所以访问http://127.0.0.1:80和http://127.0.0.1的效果是一样的,如图所示。

(4)配置Nginx服务器。
关闭Nginx服务器。
$ /usr/local/nginx/sbin/nginx -s stop
其中参数-s表示强制关闭Nginx服务器。
测试Nginx的配置文件是否正确。
$ /usr/local/nginx/sbin/nginx -t
重启Nginx,修改配置文件后,使用以下命令重新加载,使配置文件生效。
$ /usr/local/nginx/sbin/nginx -s reload
(5)启用Nginx状态信息。
可以通过ngx_http_stub_status_module模块来监控Nginx的一些状态信息。在编译Nginx的时候,此模块默认是不编译的。从源码编译安装Nginx时,需要在编译的时候使用如下命令添加ngx_http_stub_status_module模块,然后再编译安装Nginx。
$ ./configure --prefix=/usr/local/nginx --with-http_stub_status_module
然后使用nginx-V命令来查看是否有ngx_http_stub_status_module模块。
$ /usr/local/nginx/sbin/nginx -V
nginx version: nginx/1.17.4
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)
configure arguments: --prefix=/usr/local/nginx --with-http_stub_status_module
然后修改nginx的配置文件nginx.conf的内容。
$ vi /usr/local/nginx/conf/nginx.conf
添加以下内容到server节点里,然后保存文件,退出到命令行。
location ~ ^/nginx-status {
allow 127.0.0.1;
deny all;
stub_status on;
access_log off;
}
修改配置后,使用如下命令重新加载Nginx的配置文件,使配置文件生效。
$ /usr/local/nginx/sbin/nginx -s reload
重启Nginx后,在浏览器中访问http://127.0.0.1/nginx-status进入Nginx状态信息页面,如图所示。

页面中参数含义如下。
Active connections:表示Nginx的当前的活跃连接数。server:表示Nginx共处理的连接数。accepts:表示Nginx已接受的连接数。handled requests:表示Nginx从启动到现在处理的客户端请求总数。Reading:表示Nginx读取到客户端的Header信息数。Writing:表示Nginx返回给客户端的Header信息数。Waiting:表示Nginx已经处理完,正在等候下一次请求指令的驻留连接(开启keep-alive设置的情况下,这个值等于Active connections-(Reading+Writing))。
6.4、配置Tomcat集群
1.部署Web项目
Session保存在服务器中,Cookie保存在用户浏览器本地或内存中。用户在发起一个HTTP请求时,在请求头中会有Cookie,在Cookie中有一个Session ID值(Tomcat中称为JSESSIONID),通过这个值获取到服务器对应的Session,如图所示。

用户在给Session赋值时,我们可以为用户生成一个唯一的Cookie值作为Session ID存储在用户的客户端,将该Cookie值作为缓存的key和Session值一起存入Redis缓存中。
使用IntelliJ IDEA新建Java EE Web项目,本实例文件名为“Redis\Chapter 07\web1”,如图所示。

修改index.jsp文件,具体内容如下。
<%@ page language="java" contentType="text/
html;charset=UTF-8"%>
<html>
<head><title>TomcatA</title></head>
<body>
<table align="centre" border="1">
<tr>
<td>Session ID</td>
<td><%= session.getId() %></td>
</tr>
<tr>
<td>Created on</td>
<td><%= session.getCreationTime() %></td>
</tr>
</table>
</body>
</html>
sessionID:<%=session.getId()%>
<br>
SessionIP:<%=request.getServerName()%>
<br>
SessionPort:<%=request.getServerPort()%>
<br>
<%
//为了区分,第二个页面显示的内容可以是This is Tomcat Server 222
out.println("This is Tomcat Server 111");
Object obj = session.getAttribute("counter");
if( null == obj){
session.setAttribute("counter" , new Integer(1));
out.println("该页面被访问了1次<br/>");
}else{
int counterVal = Integer.parseInt( obj.toString() );
out.println("该页面被访问了 " + counterVal +" 次");
counterVal ++;
session.setAttribute("counter", counterVal);
}
%>
把web1项目部署到主机192.168.11.15的Tomcat的%/tomcat7/webapps目录下,然后稍微修改index.jsp文件上的内容以示区别,以便区分请求被分发到了哪个Tomcat上。修改index.jsp文件的内容如下。
<%@ page language="java" contentType="text/html;charset=UTF-8"%>
<html>
<head><title>TomcatA</title></head>
<body>
<table align="centre" border="1">
<tr>
<td>Session ID</td>
<td><%= session.getId() %></td>
</tr>
<tr>
<td>Created on</td>
<td><%= session.getCreationTime() %></td>
</tr>
</table>
</body>
</html>
sessionID:<%=session.getId()%>
<br>
SessionIP:<%=request.getServerName()%>
<br>
SessionPort:<%=request.getServerPort()%>
<br>
<%
out.println("This is Tomcat Server 222");
Object obj = session.getAttribute("counter");
if( null == obj){
session.setAttribute("counter" , new Integer(1));
out.println("该页面被访问了1次<br/>");
}else{
int counterVal = Integer.parseInt( obj.toString() );
out.println("该页面被访问了 " + counterVal +" 次");
counterVal ++;
session.setAttribute("counter", counterVal);
}
%>
把web1项目分别部署到两台主机的Tomcat服务器上,然后访问http://localhost/web1会发现Session ID不一致,如果要保持Session ID一致,需要配置Tomcat集群。
把web1项目部署在主机192.168.11.14的%/tomcat7/webapps/目录下。访问http://192.168.11.14:8080/web1,查看页面中的SessionID,如图所示。

把web1项目部署在主机192.168.11.15的%/tomcat7/webapps/目录下。访问http://192.168.11.15:8080/web1,查看另一个页面中的Session ID,如图所示。

为了配置Tomcat集群,需要配置Nginx服务器。
2.配置Nginx服务器
修改主机192.168.11.14上的Nginx配置文件nginx.conf。
vi /usr/local/nginx/conf/nginx.conf
修改Nginx配置文件nginx.conf后,对nginx.conf配置文件内容进行精简,精简后的配置文件内容如图所示。

在nginx.conf配置文件里配置Nginx反向代理服务器使用的主要命令如图所示。
- 使用upstream命令配置后端服务器集群。
- 使用proxy_pass命令配置需要转发的路径地址。
在本例中将web1项目部署在两台Tomcat服务器上,IP地址分别为192.168.11.14和192.168.11.15,反向代理服务器Nginx部署在IP地址为192.168.11.14的计算机上,proxy_pass的URL配置为http://tomcat。如果在IP地址为192.168.11.14的计算机上访问http://127.0.0.1,Nginx会把请求地址转向http://tomcat。Tomcat是Nginx配置的服务器集群名称,Nginx会根据权重把请求分配给名称为Tomcat的服务器集群上对应的http://192.168.11.14:8080和http://192.168.11.15:8080的Web应用上,权重越大,Web应用获得分配请求的概率越大。
然后重新启动Nginx。
$ /usr/local/nginx/sbin/nginx -s reload
在主机192.168.11.14上访问http://localhost/web1,会发现一个问题:每次刷新页面都会得到不同的Session ID值,如图所示。我们还需要修改Tomcat的配置文件,使用Redis保存Session ID值。

6.5、配置Tomcat使用Redis管理Session
首先,在两台主机上修改conf/context.xml文件的内容,重新启动Tomcat。
- 在两台主机的Tomcat的conf/context.xml文件里都增加如下内容:在实验环境中第一台测试服务器的IP地址为192.168.11.14,也就是Redis服务器所在主机的IP地址。
<?xml version='1.0' encoding='utf-8'?>
<Context>
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
host="192.168.11.14"
port="6379"
database="0"
/>
</Context>
- host:对外提供服务的主机。
- port:对外提供服务的端口。
下载最新的Jedis模块、Tomcat Redis SessionManager模块和Apache Commons Pool模块,把三个模块的jedis-3.1.0.jar、tomcat-redis-session-manager1.2.jar、commons-pool2-2.0.jar复制到%/Tomcat7/lib目录下。
- 修改redis.conf配置文件,修改bind对应的值为如下内容。
bind 127.0.0.1 192.168.11.14
然后重新启动Redis服务器。
./redis-server redis.conf
客户端连接Redis服务器。
redis-cli –h 192.168.11.14 –p 6379
在IP地址为192.168.11.14的Linux主机上再次访问http://localhost:8080/web1,会发现Session ID值不变,如图所示。

可以看出,分别访问了不同的Tomcat,但是得到的Session ID值却是相同的,说明达到了集群的目的,这样就实现了负载均衡。假设服务器在运行过程中,其中一个Tomcat崩溃了,仍然还有另一个Tomcat可以正常访问服务。
访问Redis会发现多了一个key,说明在Tomcat上使用Redis保存了Session,如图所示。


11万+

被折叠的 条评论
为什么被折叠?



