Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的Key-Value数据库,并提供多种语言的API。
Redis 为什么会用单线程?怎么理解单线程+多路 IO 复用技术?
在 Redis 6.0 以前,Redis的核心网络模型选择用单线程来实现。对于一个 DB 来说,CPU 通常不会是瓶颈,因为大多数请求不会是 CPU 密集型的,而是I/O 密集型。
具体到 Redis的话,如果不考虑 RDB/AOF 等持久化方案,Redis是完全的纯内存操作,执行速度是非常快的,因此这部分操作通常不会是性能瓶颈,Redis真正的性能瓶颈在于网络 I/O,也就是客户端和服务端之间的网络传输延迟,因此 Redis选择了单线程的 I/O 多路复用来实现它的核心网络模型。
首先理清一个概念:Redis 是单线程,主要是指 Redis 的网络 IO和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
Redis 6.0为何引入多线程?
就是 Redis的网络 I/O 瓶颈已经越来越明显了。
随着互联网的飞速发展,互联网业务系统所要处理的线上流量越来越大,Redis的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量,要提升 Redis的性能有两个方向:
后者依赖于硬件的发展,暂时无解。所以只能从前者下手,网络 I/O 的优化又可以分为两个方向:
零拷贝技术有其局限性,无法完全适配 Redis这一类复杂的网络 I/O 场景,更多网络 I/O 对 CPU 时间的消耗和 Linux 零拷贝技术。而 DPDK 技术通过旁路网卡 I/O 绕过内核协议栈的方式又太过于复杂以及需要内核甚至是硬件的支持。
因此,利用多核优势成为了优化网络 I/O 性价比最高的方案。
Redis 发展史
本节包括 Linux 和 Windows 两个系统中的 Redis 安装教程。请各位提前准备好如下软件包:
/opt
下需要注意的是:这里提供的压缩包是在 CentOS 7 平台中已经编译且安装过的,所以,解压后,将 bin 目录下所有的文件赋予可执行权力,即可直接启动,不用做任何配置,但为了方便使用,建议按照教程配置后再使用。
[root@c7100 ~]# cd /opt/
[root@c7100 opt]# tar -zxf redis-5.0.10-centos-3.10.0-693.el7.x86_64-release.tar.gz
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# bind 127.0.0.1
# Accept connections on the specified port, default is 6379 (IANA #815344).
# If port 0 is specified Redis will not listen on a TCP socket.
port 26379
# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
daemonize yes
# Creating a pid file is best effort: if Redis is not able to create it
# nothing bad happens, the server will start and run normally.
#pidfile /var/run/redis_6379.pid
pidfile /opt/redis/data/redis_6379.pid
# Specify the log file name. Also the empty string can be used to force
# Redis to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
#logfile /usr/local/redis/log/redis_6379.log
logfile /opt/redis/log/redis_6379.log
# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
#dir /var/lib/redis/6379
dir /opt/redis/data
# Require clients to issue AUTH <PASSWORD> before processing any other
# commands. This might be useful in environments in which you do not trust
# others with access to the host running redis-server.
#
# This should stay commented out for backward compatibility and because most
# people do not need auth (e.g. they run their own servers).
#
# Warning: since Redis is pretty fast an outside user can try up to
# 150k passwords per second against a good box. This means that you should
# use a very strong password otherwise it will be very easy to break.
#
# requirepass foobared
requirepass 123456
/opt/redis
下,故而这一步将需要使用的目录创建好[root@c7100 opt]# mkdir -p /opt/redis/data/
[root@c7100 opt]# mkdir -p /opt/redis/log/
[root@c7100 opt]# chmod +x /opt/redis/bin/*
/etc/profile.d/env-redis.sh
,其内容如下:REDIS_HOME=/opt/redis
PATH=$PATH:$REDIS_HOME/bin
export PATH REDIS_HOME
source
命令使 redis 的环境变量得以生效[root@c7100 opt]# source /etc/profile
[root@c7100 ~]# redis-server /opt/redis/redis.conf
前文提到过,即便是不做2、3、5、6 步骤,也可以通过绝对地址直接启动 redis ,但不推荐,如下所示
[root@c7100 ~]# /opt/redis/bin/redis-server
[root@c7100 ~]# redis-cli -h localhost -p 26379
localhost:26379>
需要注意的是
- 如果没有按照配置来启动 redis 服务,其端口默认是 6379
- 如果需要在 Windows 系统上使用客户端连接Linux 中的 Redis 服务,需要开放防火墙端口
在 Window 上的 Redis 的相关软件的安装步骤非常简单,双击安装程序后,直接傻瓜式下一步安装即可,这里不做讲解。
安装成功后,会自动增加一个名为 Redis 的系统服务,该服务在启动状态情况下,我们可以使用 Redis 的客户端来进行连接,例如: Another Redis Desktop Manager、Redis Desktop Manager、redis-cli 等。
localhost:26379> auth 123456
OK
localhost:26379>
localhost:26379> select 1
OK
localhost:26379[1]>
ex
参数设置过期时间,如果不指定 ex
参数,则默认永不过期。localhost:26379[1]> set name tina
OK
localhost:26379[1]> set age 12
OK
localhost:26379[1]> set nickname Gina ex 60
OK
localhost:26379[1]> get name
"tina"
localhost:26379[1]> keys *
1) "age"
2) "nickname"
3) "name"
localhost:26379[1]> keys a*
1) "age"
localhost:26379[1]> exists name
(integer) 1
localhost:26379[1]> exists name age
(integer) 2
localhost:26379[1]> exists nickname
(integer) 0
localhost:26379[1]> type name
string
localhost:26379[1]> del name age
(integer) 2
localhost:26379[1]> set nickname Gina
OK
localhost:26379[1]> expire nickname 60
(integer) 1
也可以在使用 set 的时候,直接指定其过期时间
localhost:26379[1]> set nickname Gina ex 30
OK
localhost:26379[1]> ttl nickname
(integer) 20
localhost:26379[1]> ttl nickname
(integer) -2
localhost:26379> dbsize
(integer) 0
localhost:26379> flushdb
OK
localhost:26379> flushall
OK
String 类型是 Redis 最基本的数据类型,一个Redis 中字符串 value 最多可以是 512M
localhost:26379[1]> set name tina
OK
localhost:26379[1]> get name
"tina"
localhost:26379[1]> append name " was a leader"
(integer) 17
localhost:26379[1]> get name
"tina was a leader"
localhost:26379[1]> append k1 hello
(integer) 5
localhost:26379[1]> get k1
"hello"
localhost:26379[1]> strlen name
(integer) 17
localhost:26379[1]> get name
"tina was a leader"
localhost:26379[1]> setnx name tina
(integer) 0
localhost:26379[1]> get name
"tina was a leader"
localhost:26379[1]> set nick Gina
OK
localhost:26379[1]> get nick
"Gina"
localhost:26379[1]> set age 18
OK
localhost:26379[1]> incr age
(integer) 19
localhost:26379[1]> decr age
(integer) 18
需要注意的是:如果指定的键不存在,则会创建该键,并将 0 作为初始值,再来进行自增 1 或自减 1
incrby 该指令用于将指定键的整数值和给定的数值相加后覆盖原值(原子操作),
decrby 该指令用于将指定键的整数值和给定的数值相减后覆盖原值(原子操作)
localhost:26379[1]> incrby age 2
(integer) 20
localhost:26379[1]> decrby age 3
(integer) 17
需要注意的是:如果指定的键不存在,则会创建该键,并将 0 作为初始值,再来进行加法或减法操作
localhost:26379[1]> mset k1 v1 k2 v2 k3 v3
OK
localhost:26379[1]> mget k1 k2 k3
1) "v1"
2) "v2"
3) "v3"
localhost:26379[1]> msetnx k3 v3 k4 v4
(integer) 0
localhost:26379[1]> msetnx k4 v4 k5 v5
(integer) 1
localhost:26379[1]> mget k4 k5
1) "v4"
2) "v5"
localhost:26379[1]> get name
"tina"
localhost:26379[1]> getrange name 0 1
"ti"
localhost:26379[1]> getrange name 1 1
"i"
localhost:26379[1]> getrange name 1 2
"in"
localhost:26379[1]> get name
"tina"
localhost:26379[1]> setrange name 1 -
(integer) 4
localhost:26379[1]> get name
"t-na"
localhost:26379[1]> setrange name 1 xyzg
(integer) 5
localhost:26379[1]> get name
"txyzg"
localhost:26379[1]> set name tina
OK
localhost:26379[1]> getset name tom
"tina"
localhost:26379[1]> get name
"tom"
字符串自动扩容特点
|?───── capacity ─────?|
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
|?─── len ───?|
图中内部为当前字符串实际分配的空间, capacity 一般要高于实际字符串长度 len。
当字符串长度小于 1M时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。
需要注意的是字符串最大长度为 512M。
Redis 的 List 是单键多值的类型。
它是简单的字符串列表,按照插入顺序排序。
可以添加一个元素到列表的头部(左边) 或者尾部( 右边 )。
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
localhost:26379[1]> lpush num 1 3 5
(integer) 3
双向链表,采用头插法会改变入表的顺序,最终结果呈现为
5 ? 3 ? 1
localhost:26379[1]> rpush num 4 6 8
(integer) 6
双向链表,采用尾插法不会改变入表的顺序,最终结果呈现为
5 ? 3 ? 1 ? 4 ? 6 ? 8
localhost:26379[1]> lrange num 0 -1
1) "5"
2) "3"
3) "1"
4) "4"
5) "6"
6) "8"
localhost:26379[1]> lpop num
"5"
localhost:26379[1]> lrange num 0 -1
1) "3"
2) "1"
3) "4"
4) "6"
5) "8"
localhost:26379[1]> rpop num
"8"
localhost:26379[1]> lrange num 0 -1
1) "3"
2) "1"
3) "4"
4) "6"
localhost:26379[1]> lpush odd 1 3 5
(integer) 3
localhost:26379[1]> rpush even 4 6 8
(integer) 3
localhost:26379[1]> rpoplpush odd even
"1"
localhost:26379[1]> lrange odd 0 -1
1) "5"
2) "3"
localhost:26379[1]> lrange even 0 -1
1) "1"
2) "4"
3) "6"
4) "8"
localhost:26379[1]> lindex even 1
"4"
localhost:26379[1]> lindex even -1
"8"
localhost:26379[1]> llen even
(integer) 4
向 even 中的 元素 4 前插入元素 2
localhost:26379[1]> linsert even before 4 2
(integer) 5
localhost:26379[1]> lrange even 0 -1
1) "1"
2) "2"
3) "4"
4) "6"
5) "8"
向 even 中的 元素 4 后插入元素 2
localhost:26379[1]> linsert even after 4 2
(integer) 6
localhost:26379[1]> lrange even 0 -1
1) "1"
2) "2"
3) "4"
4) "2"
5) "6"
6) "8"
localhost:26379[1]> lrem even 2 2
(integer) 2
localhost:26379[1]> lrange even 0 -1
1) "1"
2) "4"
3) "6"
4) "8"
localhost:26379[1]> lset even 0 2
OK
localhost:26379[1]> lrange even 0 -1
1) "2"
2) "4"
3) "6"
4) "8"
Redis 的 Set 是自动排重的集合类型。
- 它是一个无序集合,且集合中的元素不允许重复,可以类比 Java 中的 HashSet
- 它的底层是采用的哈希表,其添加、删除和查找的时间复杂度都是 O(1)
localhost:26379[1]> sadd sk Tina Gina Tina Anne
(integer) 3
localhost:26379[1]> smembers sk
1) "Tina"
2) "Gina"
3) "Anne"
localhost:26379[1]> sismember sk Tina
(integer) 1
localhost:26379[1]> sismember sk Tom
(integer) 0
localhost:26379[1]> scard sk
(integer) 3
localhost:26379[1]> spop sk
"Gina"
localhost:26379[1]> srem sk Tina
(integer) 1
localhost:26379[1]> smembers sk
1) "Anne"
localhost:26379[1]> sadd sk Tina Gina
(integer) 2
localhost:26379[1]> srandmember sk 1
1) "Anne"
localhost:26379[1]> sadd girl Tina Gina
(integer) 2
localhost:26379[1]> sadd boy Tom Jack
(integer) 2
localhost:26379[1]> smove girl boy Tina
(integer) 1
localhost:26379[1]> smembers girl
1) "Gina"
localhost:26379[1]> smembers boy
1) "Tina"
2) "Jack"
3) "Tom"
localhost:26379[1]> sadd even 2 4 6
(integer) 3
localhost:26379[1]> sadd prime 2 3 5
(integer) 3
localhost:26379[1]> sinter even prime
1) "2"
localhost:26379[1]> sunion even prime
1) "2"
2) "3"
3) "4"
4) "5"
5) "6"
localhost:26379[1]> sdiff even prime
1) "4"
2) "6"
Redis 的 Hash 是键值对集合类型。
- 它是一个键无序集合,可以类比 Java 中的 Map
- Hash 类型对应的数据结构是两种: ziplist (压缩列表),hashtable (哈希表)。
- 当键值对集合长度较短且个数较少时,使用 ziplist,否则使用 hashtable。
localhost:26379[1]> hset user name Tina age 20
(integer) 2
localhost:26379[1]> hget user name
"Tina"
localhost:26379[1]> hget user age
"20"
localhost:26379[1]> hexists user name
(integer) 1
localhost:26379[1]> hexists user gender
(integer) 0
localhost:26379[1]> hkeys user
1) "name"
2) "age"
3) "address"
4) "weight"
5) "height"
localhost:26379[1]> hvals user
1) "Tina"
2) "20"
3) "Wuhan"
4) "50kg"
5) "178cm"
localhost:26379[1]> hincrby user age 1
(integer) 21
localhost:26379[1]> hincrby user age 3
(integer) 24
localhost:26379[1]> hget user age
"24"
localhost:26379[1]> hsetnx user name Jack
(integer) 0
Redis 的 zset 是一个没有重复元素的字符串集合。
- 有序集合的每个成员都关联了一个 score ,这个 score 被用来按照从最低分到最高分的方式排序集合中的成员。
- 集合的成员是唯一的,但是 score 可以是重复了 。
- 因为元素是有序的,所以可以很快的根据 score 或者 position 来获取一个范围的元素。
localhost:26379[1]> zadd language 100 java 50 C++ 110 python
(integer) 3
localhost:26379[1]> zrange language 0 -1
1) "C++"
2) "java"
3) "python"
localhost:26379[1]> zrange language 0 -1 withscores
1) "C++"
2) "50"
3) "java"
4) "100"
5) "python"
6) "110"
localhost:26379[1]> zrevrange language 0 -1
1) "python"
2) "java"
3) "C++"
localhost:26379[1]> zrevrange language 0 -1 withscores
1) "python"
2) "110"
3) "java"
4) "100"
5) "C++"
6) "50"
localhost:26379[1]> zrangebyscore language 10 105
1) "C++"
2) "java"
localhost:26379[1]> zrevrangebyscore language 105 10
1) "java"
2) "C++"
localhost:26379[1]> zcount language 10 100
(integer) 2
localhost:26379[1]> zrank language java
(integer) 1
localhost:26379[1]> zrank language python
(integer) 2
localhost:26379[1]> zrem language C++
(integer) 1
localhost:26379[1]> zrange language 0 -1
1) "java"
2) "python"
localhost:26379[1]> zrange language 0 -1 withscores
1) "java"
2) "100"
3) "python"
4) "110"
localhost:26379[1]> zincrby language 11 java
"111"
localhost:26379[1]> zrange language 0 -1 withscores
1) "python"
2) "110"
3) "java"
4) "111"
什么是发布和订阅?
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道。
┌─────────────────────┐
│ publisher │
└──────────┬──────────┘
│
▼
┌───────────────────────────┐
│ Channel │
└───────────────────────────┘
▲ ▲ ▲
│ │ │
│ │ │
│ │ │
┌────────────┐ ┌────────────┐ ┌────────────┐
│ subscriber │ │ subscriber │ │ subscriber │
└────────────┘ └────────────┘ └────────────┘
localhost:26379[1]> subscribe c1 c2
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "c1"
3) (integer) 1
1) "subscribe"
2) "c2"
3) (integer) 2
127.0.0.1:26379[1]> publish c1 hello
(integer) 1
127.0.0.1:26379[1]> publish c2 hi
(integer) 1
1) "message"
2) "c1"
3) "hello"
1) "message"
2) "c2"
3) "hi"
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
@RunWith(JUnit4.class)
public class TestRedis {
private final String HOST = "localhost";
private final int PORT = 26379;
private final Jedis jedis = new Jedis(HOST, PORT);
@Before
public void testBefore() {
jedis.auth("123456");
jedis.select(2);
}
@After
public void testAfter() {
jedis.close();
}
@Test
public void testConnection() {
// 输出 PONG 则说明连接成功!
System.out.println(jedis.ping());
}
@Test
public void testStringApi() {
final String key = "test:string-key";
// 测试 set/get 方法
jedis.set(key, "Tina");
String val = jedis.get(key);
System.out.println(key + ":" + val);
// 测试过期时间
jedis.setex(key, 3, "Tina");
System.out.println(jedis.get(key));
Long ttl = jedis.ttl(key);
System.out.println(key + "剩余" + ttl + "s");
boolean exists;
do {
try {
ttl = jedis.ttl(key);
System.out.println("等待" + ttl + "s");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
exists = Boolean.TRUE.equals(jedis.exists(key));
System.out.println(key + "是否存在:" + exists);
} while (exists);
// 测试自增
for (int i = 0; i < 18; i++) {
jedis.incr(key);
}
val = jedis.get(key);
System.out.println(key + ":" + val);
}
@Test
public void testListApi() {
final String k1 = "test:list:even";
final String k2 = "test:list:prime";
// 添加元素 —— 头插法
jedis.lpush(k1, "2", "4", "6");
// 添加元素 —— 尾插法
jedis.rpush(k2, "2", "3", "5");
// 添加元素 —— 将 List 集合中的数据插入到 Redis
List<String> strings = Arrays.asList("6", "8", "10");
jedis.rpush(k1, strings.toArray(new String[3]));
// 查询总数和所有元素
strings = jedis.lrange(k1, 0, -1);
Long len = jedis.llen(k1);
System.out.println("k1 的长度 :" + len + ",k1 的值 :" + strings);
strings = jedis.lrange(k2, 0, -1);
len = jedis.llen(k2);
System.out.println("k2 的长度 :" + len + ",k2 的值 :" + strings);
// 测试往索引位置插入值
long index = 2L;
String val = jedis.lindex(k1, index);
System.out.println("索引" + index + "位置的值是:" + val);
jedis.lset(k1, index, "-6");
System.out.println("替换为 -6 后, k1 的值 :" + jedis.lrange(k1, 0, -1));
jedis.linsert(k1, ListPosition.AFTER, "-6", "-7");
System.out.println("插入 -7 后, k1 的值 :" + jedis.lrange(k1, 0, -1));
// 删除集合中两个元素 6
jedis.lrem(k1, 2, "6");
System.out.println("移除两个 6 后, k1 的值 :" + jedis.lrange(k1, 0, -1));
// 弹出首尾的元素
String pop = jedis.rpop(k1);
System.out.println("pop is:" + pop);
System.out.println("尾部弹出后, k1 的值 :" + jedis.lrange(k1, 0, -1));
pop = jedis.lpop(k1);
System.out.println("pop is:" + pop);
System.out.println("首部弹出后, k1 的值 :" + jedis.lrange(k1, 0, -1));
// 将第一个集合的尾部元素移动到第二个集合的头部
jedis.rpoplpush(k1, k2);
System.out.println("k1 is:" + jedis.lrange(k1, 0, -1));
System.out.println("k2 is:" + jedis.lrange(k2, 0, -1));
}
@Test
public void testSetApi() {
final String k1 = "test:set:even";
final String k2 = "test:set:prime";
// 添加元素
jedis.sadd(k1, "2", "4", "6", "8", "10", "12");
Set<String> members = new HashSet<>(Arrays.asList("2", "3", "5", "7"));
jedis.sadd(k2, members.toArray(new String[0]));
// 查询总数和所有元素
members = jedis.smembers(k1);
Long len = jedis.scard(k1);
System.out.println("k1 的长度 :" + len + ",k1 的值 :" + members);
members = jedis.smembers(k2);
len = jedis.scard(k2);
System.out.println("k2 的长度 :" + len + ",k2 的值 :" + members);
// 判断元素是否存在
Boolean sis = jedis.sismember(k1, "2");
System.out.println("2是否存在:" + sis);
sis = jedis.sismember(k1, "1");
System.out.println("1是否存在:" + sis);
// 移除元素
jedis.srem(k1, "8", "10");
System.out.println("移除 8 和 10 后, k1 的值 :" + jedis.smembers(k1));
// 随机元素
String member = jedis.srandmember(k1);
System.out.println("随机挑选的值为:" + member);
member = jedis.spop(k1);
System.out.println("随机弹出的值为:" + member + "后, k1 的值 :" + jedis.smembers(k1));
// 移动元素
jedis.smove(k1, k2, "12");
System.out.println("移动 12 后, k1 的值 :" + jedis.smembers(k1));
System.out.println("移动 12 后, k2 的值 :" + jedis.smembers(k2));
// 交集、并集和差集
System.out.println("k1, k2 的交集:" + jedis.sinter(k1, k2));
System.out.println("k1, k2 的并集:" + jedis.sunion(k1, k2));
System.out.println("k1, k2 的差集:" + jedis.sdiff(k1, k2));
}
@Test
public void testHashApi() {
final String k1 = "test:hash:user";
Map<String, String> map = new HashMap<>();
// 添加元素
jedis.hset(k1, "name", "tina");
{
map.put("age", "20");
map.put("weight", "50kg");
map.put("height", "170cm");
map.put("address", "武汉");
jedis.hset(k1, map);
}
// 查询总数和所有元素
map = jedis.hgetAll(k1);
Long len = jedis.hlen(k1);
System.out.println("k1 的长度 :" + len + ",k1 的值 :" + map);
// 判断元素是否存在
Boolean sis = jedis.hexists(k1, "name");
System.out.println("name 是否存在:" + sis);
sis = jedis.hexists(k1, "gender");
System.out.println("gender 是否存在:" + sis);
// 移除元素
jedis.hdel(k1, "weight", "height");
System.out.println("移除 weight 和 height 后, k1 的值 :" + jedis.hgetAll(k1));
}
@Test
public void testZSetApi() {
final String k1 = "test:zset:language";
Map<String, Double> map = new HashMap<>();
Set<String> set;
{
map.put("java", 120D);
map.put("c++", 110D);
map.put("python", 105D);
map.put("c#", 85D);
map.put("vb", 34D);
map.put("php", 80D);
map.put("javascript", 140D);
}
// 添加元素
jedis.zadd(k1, 50D, "scala");
jedis.zadd(k1, map);
// 查询总数和所有元素
set = jedis.zrange(k1, 0, -1);
Long len = jedis.zcard(k1);
Long count = jedis.zcount(k1, 100D, 130D);
System.out.println("k1 的长度 :" + len + ", [100, 130]的个数 :" + count);
System.out.println("k1 的值 :" + set);
// 带 score 的查询
Set<Tuple> tuples = jedis.zrangeByScoreWithScores(k1, 90D, 120D);
System.out.println("顺[90, 120]:" + tuples);
tuples = jedis.zrevrangeByScoreWithScores(k1, 120D, 90D);
System.out.println("逆[90, 120]:" + tuples);
// 删除元素
jedis.zrem(k1, "vb", "scala");
System.out.println("移除 vb 和scala 后, k1 的值 :" + jedis.zrange(k1, 0, -1));
// 增量
jedis.zincrby(k1, 20D, "c++");
System.out.println("调整 score 后, k1 的值 :" + jedis.zrange(k1, 0, -1));
}
}
spring-session-data-redis
是一个Java Spring框架的库,它用于将Spring Session的数据存储在Redis中。Spring Session 是Spring生态系统中的一部分,它用于管理用户会话的状态。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.8.RELEASE</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
spring:
redis:
port: 26379
host: localhost
password: 123456
database: 3
public class SessionInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
if (!(session != null && session.getAttribute("UNAME") != null)) {
// 当前请求没有会话数据,则视作未登录,跳转到登录页
response.sendRedirect("/login");
return false;
}
return true;
}
}
@Configuration
@EnableRedisHttpSession // 启用 Redis 会话管理
public class SessionConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 增加拦截器及其拦截地址
registry.addInterceptor(new SessionInterceptor())
.addPathPatterns("/dashboard", "/dashboard/*");
}
}
@GetMapping("/login")
public String index(HttpSession session, String username, String password) {
...... 此处省略验证账号密码 ......
// 设置会话数据信息
session.setAttribute("UNAME", username);
return "redirect:/dashboard";
}
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!--
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.4.5</version>
</dependency>
-->
<!--
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>5.7.12</version>
</dependency>
-->
配置中的邮箱账号和密码需要开启 POP3/SMTP 服务
spring:
redis:
port: 26379
host: localhost
password: 123456
database: 3
mail:
port: 465
protocol: smtp
host: smtp.yeah.net
properties:
mail:
smtp:
auth: true
ssl:
trust: smtp.yeah.net
starttls:
enable: true
required: true
socketFactory:
port: 465
class: javax.net.ssl.SSLSocketFactory
debug: true
username: timor2020@yeah.net
password: **********************
default-encoding: UTF-8
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
private static final String KEY_CAPTCHA = "CAPTCHA_CODE";
@Value("${spring.mail.username}")
private String mailUsername;
@Resource
private StringRedisTemplate redis;
@Resource
private JavaMailSender sender;
/**
* 验证邮箱验证码
*
* @param mail 邮箱
* @param code 验证码
*/
@RequestMapping("verify/email")
public String verifyMail(String mail, String code) {
ValueOperations<String, String> ops = redis.opsForValue();
String key = "captcha:" + mail;
String captcha = ops.get(key);
boolean matched = code != null && code.equals(captcha);
if (matched) {
redis.delete(key);
}
return matched ? "OK" : "NO";
}
/**
* 发送邮箱验证码
*
* @param mail 邮箱
*/
@RequestMapping("send/email")
public String sendMail(String mail) {
String code = RandomStringUtils.randomAlphanumeric(6);
int minutes = 5;
String content = "您的验证码是:[<b>" + code + "</b>]," + minutes + "分钟内有效";
// 将验证码保存到 redis 中并
ValueOperations<String, String> ops = redis.opsForValue();
ops.set("captcha:" + mail, code, minutes, TimeUnit.MINUTES);
try {
MimeMessage message = sender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
try {
helper.setFrom(mailUsername, "武汉晴川学院");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
helper.setTo(mail);
helper.setSubject("登录验证码");
helper.setText(content, true);
helper.setValidateAddresses(true);
sender.send(message);
} catch (MessagingException e) {
e.printStackTrace();
return "邮件发送出错:" + e.getMessage();
}
return "邮件已发送,请注意查收";
}
}
本案例主要涉及到的是 redis 的键会自动过期这个特点,参考代码如下:
@Component
public class CaptchaAccessLimitInterceptor implements HandlerInterceptor {
// 设置访问限制时间,单位:分钟
private static final int LIMIT_TIME = 1;
@Resource
private StringRedisTemplate redis;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
ValueOperations<String, String> ops = redis.opsForValue();
String id = session.getId();
String key = "CaptchaAccessLimit:" + id;
boolean exists = Boolean.TRUE.equals(redis.hasKey(key));
Long val = ops.increment(key);
val = val == null ? 0 : val;
// 【键不存在】 或者 【访问次数在 1 分钟内 5 次以下】,允许操作
if (exists) {
System.out.println("在" + LIMIT_TIME + "分钟内第" + val + "次访问");
if (val > 5) {
response.setContentType("text/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("访问次数过多,访问受限");
writer.close();
return false;
}
} else {
redis.expire(key, LIMIT_TIME, TimeUnit.MINUTES);
System.out.println("首次访问,设置访问次数:" + val);
}
return true;
}
}
抽奖案例采用的是 redis 的 set 的 spop,参考代码如下:
@RestController
@RequestMapping("/raffle")
public class RaffleController {
private static final String KEY_RAFFLE = "RAFFLE";
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.database}")
private int database;
private Jedis jedis;
@PostConstruct
public void init() {
jedis = new Jedis(host, port);
jedis.auth(password);
jedis.select(database);
jedis.sadd(KEY_RAFFLE, "XIAOMI MIX Fold 3", "HUAWEI Mate60 Pro");
jedis.sadd(KEY_RAFFLE, "OPPO Find N3 Flip", "VIVO s17");
jedis.expire(KEY_RAFFLE, 1 * 60);
}
@GetMapping(path = "", produces = {"text/html;charset=utf-8"})
public String index() {
Long ex = jedis.ttl(KEY_RAFFLE);
ex = ex == null ? 0L : ex;
Set<String> strings = jedis.smembers(KEY_RAFFLE);
if (ex > -1) {
return "<meta http-equiv='refresh' content='1'/>" +
"<h1>随机抽奖(还剩" + ex + "秒)</h1>" +
"<hr /><ol><li>" + String.join("</li><li>", strings) + "</li></ol>" +
"<hr /><a href='/raffle/start'>立即抢购</a>";
}
return "<h1>随机抽奖(活动已经结束)</h1><hr />";
}
@GetMapping(path = "start", produces = {"text/html;charset=utf-8"})
public String start() {
String member = jedis.spop(KEY_RAFFLE);
return "<h1>成功抢到" + member + "</h1><hr /><a href='/raffle'>返回</a>";
}
}
这三个案例均可以采用了 redis 的 zset 来处理,参考代码如下:
@RestController
@RequestMapping("counter")
public class CounterController {
private static final String KEY_COUNTER = "COUNTER";
@Resource
private StringRedisTemplate redis;
private static final ObjectMapper mapper = new ObjectMapper();
@PostConstruct
public void init() throws JsonProcessingException {
ValueOperations<String, String> opsForValue = redis.opsForValue();
ZSetOperations<String, String> opsForZSet = redis.opsForZSet();
opsForZSet.removeRange(KEY_COUNTER, 0, -1);
for (int i = 0; i < 30; i++) {
Article article = new Article();
article.setId(i);
article.setTitle("文章[" + RandomStringUtils.randomAlphabetic(10) + "]");
article.setContent(RandomStringUtils.randomAlphabetic(100, 20000));
String string = mapper.writeValueAsString(article);
String key = KEY_COUNTER + ":" + article.getId();
opsForValue.set(key, string);
opsForZSet.add(KEY_COUNTER, key, 0.0);
}
}
@GetMapping(path = "", produces = {"text/html;charset=utf-8"})
public String index() throws JsonProcessingException {
ZSetOperations<String, String> opsForZSet = redis.opsForZSet();
ValueOperations<String, String> opsForValue = redis.opsForValue();
List<String> strings = new ArrayList<>();
Set<TypedTuple<String>> tuples = opsForZSet.rangeWithScores(KEY_COUNTER, 0, -1);
if (tuples != null) {
for (TypedTuple<String> tuple : tuples) {
Double score = tuple.getScore();
String key = tuple.getValue();
if (key == null) {
continue;
}
String s = opsForValue.get(key);
Article article = mapper.readValue(s, Article.class);
strings.add("<li><b>" + article.getTitle() + "("
+ article.getId() + ")</b>(<i>"
+ score + "</i>)<a href='/counter/incr?id="
+ article.getId() + "'>加赞</a>" +
"<a href='/counter/incr?step=1&id="
+ article.getId() + "'>减赞</a></li>");
}
}
return "<h1>文章列表</h1><hr /><ol>" + String.join("", strings) + "</ol>";
}
@GetMapping(path = "incr", produces = {"text/html;charset=utf-8"})
public String incr(Integer id, Double step) {
step = step == null || step == 0 ? 1D : -1D;
ZSetOperations<String, String> ops = redis.opsForZSet();
String key = KEY_COUNTER + ":" + id;
ops.incrementScore(KEY_COUNTER, key, step);
return "<meta http-equiv='refresh' content='1;url=/counter'>";
}
}
原子操作指的是一个事务包含多个操作,这些操作要么全部执行,要么全都不执行
@RunWith(JUnit4.class)
public class TestAtomicity {
private static int number = 0;
public void increase() {
String name = Thread.currentThread().getName();
System.out.println(name + "`s 开始循环前,number = " + number);
for (int i = 0; i < 10000; i++) {
number++;
}
}
@Test
public void testIncreasingOnMultiThreading() throws InterruptedException {
int count = 10;
Thread[] ths = new Thread[count];
for (int i = 0; i < count; i++) {
ths[i] = new Thread(this::increase, "T" + i);
ths[i].start();
}
for (int i = 0; i < count; i++) {
ths[i].join();
}
System.out.println("main:" + number);
}
}
从该单元测试的结果不难发现有两个结论:
++
操作也应该是执行了 10 ? 10000 次,但是从 main 函数中的输出结果来看,可以确定的是 Java 语言中的 ++
操作本身并非是原子操作。解决思路:
++
操作,这样能规避 ++
造成的问题synchronized
关键字,这样能规避多线程调用引发的问题。