开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
本文主要讲述Redis String的用法及其应用场景。下面要讲的应用场景属于实战,用的是lettuce。
Redis String命令
基础操作:
Redis-cli -h 127.0.0.1 -p 6379
auth [username]password
String命令
读写命令
- SET和GET
SETNX和SETXX可以用到锁上面
set key value
get key
TTL name 查看过期时间
SET name kouzhaoxuejie NX
参数:
EX 参数是给这个 Key 设置一个过期时间,单位是秒。
PX 参数的话,和 EX 参数类似,唯一的区别就是时间单位变成了毫秒
NX是在目标 Key 不存在时候,才写入这个 Key;要是 Key 已存在的话,这个 Key 就写不进去。
XX参数的功能正好是和 NX 反着的,XX 参数表示的是 Key 不存在的时候,写不进去。
EXAT和PXAT是指定 Key 在某个时间点过期,后面跟的是一个明确的时间戳,意思是到了这个点 Key 就没了。
-
MSET和MGET批量操作,用法和set、get一样
-
字符串批量命令是
MSETNX,它是 SETNX 的批量版本,就是多个 Key 都不存在的时候,可以一把写入多个 Key。递增操作
SET age 25 INCR age ->26 INCRBY age 100 ->126 -
INCR 命令和DECR 命令对这个 age 进行 加一和减一操作。 -
INCRBY和DECRBY两个命令,这两条命令后面可以指定加多少、减多少。
操作部分字符串
set name kouzhao
append name xuejie
get name
->"kouzhaoxuejie"
setrange name 0 kz
get name
-> "kzuzhaoxuejie"
- APPEND,它是往一个 Key 里面追加一个字符串,和 Java 字符串的 append() 方法一个意思。
SETRANGE 命令,功能是指定一个下标,然后用传进去的字符串,替换这个下标后的内容。
复合操作
复合操作其实就是上面几个命令的组合,常用的有三个命令:GETDEL、GETEX 和 GETSET。
get name
->"kouzhaoxuejie"
getset name zhangsan
->"kouzhaoxuejie"
get name
->"zhangsan"
GETSET 命令的意思是,返回当前这个 name 的值,也就是返回 kouzhaoxuejie,然后同时用 zhangsan 来覆盖 name 的值,这个时候再查 name 这个 Key,拿到的就是 zhangsan 了。
getex name ex 10
->"zhangsan"
TTL name
->8
TTL name
->7
GETEX 命令的意思是,获取 Key 的值,同时给 Key 设置一个过期时间。来看下图的例子,我们这里获取 name 的同时,把 name 这个 Key 的过期时间设置成 10 秒。我们用 TTL 命令可以看到 name 剩余的过期时间不断在减少,最后返回一个 -2,就表示 name 过期了。
set name kouzhaoxuejie
getdel name
->"kouzhaoxuejie"
get name
(nil)
DEL 命令是删除一个 Key,GETDEL 命令 就是先获取一个 Key 的值,同时删除这个 Key 的值。如下图所示,这里我们用 GETDEL 命令读取 name 值的时候,就把它一并删除了,再用 GET 命令查,就查询不到了。
使用场景
这部分主要介绍在什么场景下可以考虑使用 Redis 字符串,同时结合 Lettuce 客户端,看看如何在 Java 里面执行 Redis 字符串命令。
三个场景: 用 Redis 的 String 结构缓存一个 Java 对象、实现分布式的效果以及实现限流的效果。
项目环境搭建(lettuce)
先创建一个 Maven 项目 lettuce-demo,然后 pom 文件里面加上 lettuce 的依赖。
<!--lettuce依赖-->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.1.8.RELEASE</version>
</dependency>
<!--json依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
<!--juint依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
缓存对象
这部分主要就是要掌握Lettuce的基本使用。
对象序列化与反序列化。
这边就简单的创建一个商品实体类
public class Product {
private String name;
private double price;
private String desc;
// 省略getter/setter方法
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Product{");
sb.append("name='").append(name).append(''');
sb.append(", price=").append(price);
sb.append(", desc='").append(desc).append(''');
sb.append('}');
return sb.toString();
}
}
测试单元,这部分是重点。
-
password@host:port/0
RedisClient redisClient = RedisClient.create("redis://admin1234@127.0.0.1:6379/0"); -
对象序列化
ObjectMapper这个是为了将Product对象序列化,然后存入redis。如何序列化呢,就是使用writeValueAsString这个方法,即
objectMapper.writeValueAsString(product); -
创建redis客户端实例并创建连接
Lettuce用RedisClient的create方法来创建redis客户端,返回的是RedisClient对象。
使用connect方法连接客户端,返回的是StatefulRedisConnection对象。
连接完之后通过 async() 方法创建一个异步的 Command 对象,即RedisAsyncCommands<String, String> asyncCommands=
connection.async();返回的是RedisAsyncCommands对象。当然redis还提供了一个同步的方法,如RedisCommands<String, String> syncCommands = connection.sync();, -
java执行Redis命令
以字符串的set命令举例,
asyncCommands.set("product", json),方法返回的是Future 对象。可以用 get() 方法设置一个等待的超时时间,等待 SET 命令执行完成。我们这里设置 1 秒,TimeUnit 用来指定时间的单位。如asyncCommands.set("product", json).get(1, TimeUnit.SECONDS);设置一个合理的超时时间,可以防止 Redis 长时间不返回把上游服务拖垮了。当然,在用同步的对象执行命令的时候,就不用再调 get() 方法,设置超时时间了,它会一直阻塞等待命令执行结束。
当业务与 Redis 的交互结束之后,我们就把连接和 RedisClient 都关闭掉,用来释放比较珍贵的网络资源。
connection.close();redisClient.shutdown();
public class RedisTest {
@Test
public void testCacheProduct() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
Product product = new Product(); // 创建Product对象
product.setName("杯子");
product.setPrice(100d);
product.setDesc("这是一个杯子");
String json = objectMapper.writeValueAsString(product);
RedisClient redisClient = RedisClient.create("redis://127.0.0.1:6379/0");
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisAsyncCommands<String, String> asyncCommands = connection.async();
asyncCommands.set("product", json).get(1, TimeUnit.SECONDS);
// RedisCommands<String, String> syncCommands = connection.sync();
// syncCommands.set("product", json);
connection.close();
redisClient.shutdown();
}
}
上述代码执行结果:
127.0.0.1:6379> GET product
"{"name":"\xe6\x9d\xaf\xe5\xad\x90","price":100.0,"desc":"\xe8\xbf\x99\xe6\x98\xaf\xe4\xb8\x80\xe4\xb8\xaa\xe6\x9d\xaf\xe5\xad\x90"}"
我们再加一个 GET 命令的单测,同时把单测类的结构调整一下。我们把建连和创建 Command 对象的逻辑放到 before() 方法里面,把释放连接的逻辑放到 after() 方法里面,然后加上 @Before 和 @After 注解,这样的话,每次跑单测的时候,就会先执行 before() 进行建连,再执行单测逻辑,最后执行 after() 关闭连接。
-
上述代码redis连接相关的都先拿出来,需要RedisClient对象(也就是redis客户端实例)、StatefulRedisConnection对象(由redis客户端创建连接所得)、RedisAsyncCommands(异步的命令执行对象)。
-
将redis客户端的创建与连接等配置相关的放到Before注解下的函数中,程序运行前会先执行该步。
-
连接释放相关的放入After注解的函数中执行。
-
像上述程序需要写的测试代码就可以改成,先对product对象做序列化,然后直接用RedisAsyncCommands对象执行redis命令即可。
-
对于get命令,我们需要先get拿到product这个键对应的东西,然后再反序列化拿到实际结果。
代码中第二个get与前面set里面的是一样的,都是设置等待超时时间。
String json = asyncCommands.get("product").get(1, TimeUnit.SECONDS); Product product = objectMapper.readValue(json, new TypeReference<Product>() { });
public class RedisTest {
private static RedisClient redisClient;
private static StatefulRedisConnection<String, String> connection;
private static RedisAsyncCommands<String, String> asyncCommands;
@Before
public void before(){
redisClient = RedisClient.create("redis://127.0.0.1:6379/0");
connection = redisClient.connect();
asyncCommands = connection.async();
// RedisCommands<String, String> syncCommands = connection.sync();
}
@After
public void after(){
connection.close();
redisClient.shutdown();
}
@Test
public void testCacheProduct() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
Product product = new Product();
product.setName("杯子");
product.setPrice(100d);
product.setDesc("这是一个杯子");
String json = objectMapper.writeValueAsString(product);
asyncCommands.set("product", json).get(1, TimeUnit.SECONDS);
}
@Test
public void testGetProduct() throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
String json = asyncCommands.get("product").get(1, TimeUnit.SECONDS);
Product product = objectMapper.readValue(json, new TypeReference<Product>() {
});
System.out.println(product);
}
}
分布式锁
SET name kouzhaoxuejie NX,设置name这个key,value是kouzhaoxuejie,若该key不存在,才能写入,若存在则无法写入。
利用 Redis 命令的原子性和 SET...NX 命令(或者是 SETNX 命令)实现锁的效果,Redis 字符串的 Key 是锁的名称,Value 存的是当前锁的拥有者。
下面这个代码就是模拟三个客户抢资源的场景。
业务逻辑注意点:
- new CountDownLatch(threadNum);创建这个对象的目的就是让三个客户对应的线程在开始时都拥堵在countDownLatch.await();处,然后三个线程一起开始执行下面的业务逻辑。业务逻辑第一步就是抢锁。
- SetArgs.Builder.ex() 方法,对标的就是 SET 命令的 EX 参数,SetArgs.Builder.nx() 方法就是 SET 命令的 NX 参数。这里我们用 ex(5) 方法告诉 Redis 需要这个 Key 五秒自动过期,用 nx() 方法告诉 Redis 这个 Key 不存在的时候,才能创建。
- 执行 asyncCommands.set() 方法发送 SET 命令,其中第一个参数是 SET 命令的 Key,第二个是 Value,第三个的话就是 SetArgs 这些扩展参数。
- 如果返回值不是 “OK”,那就是当前线程加锁失败了,当前线程会休眠一会,再继续这个 while 循环去抢锁。如果返回是 “OK” 的话,那证明这个 Key 写入成功了。
代码注意点:
- new CountDownLatch(threadNum)、SetArgs.Builder.ex(5).nx();
- Runnable线程相关:多线程其中的一个方式就是实现Runnable接口,这里面的话是用了函数式表达式写了,然后new Thread创建线程,thread.start()启动线程。
@Test
public void testLock() throws Exception {
int threadNum = 1;
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
Runnable runnable = () -> {
try {
countDownLatch.await();
while (true) {
// 获取锁
SetArgs setArgs = SetArgs.Builder.ex(5).nx();
String succ = asyncCommands.set("update-product",
Thread.currentThread().getName(), setArgs).get(1, TimeUnit.SECONDS);
// 加锁失败
if (!"OK".equals(succ)) {
System.out.println(Thread.currentThread().getName() + "加锁失败,自选等待锁");
Thread.sleep(100);
} else {
System.out.println(Thread.currentThread().getName() + "加锁成功");
break;
}
}
// 加锁成功
System.out.println(Thread.currentThread().getName() + "开始执行业务逻辑");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "完成业务逻辑");
// 释放锁
asyncCommands.del("update-product").get(1, TimeUnit.SECONDS);
System.out.println(Thread.currentThread().getName() + "释放锁");
} catch (Exception e) {
e.printStackTrace();
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
countDownLatch.countDown();
Thread.sleep(TimeUnit.DAYS.toMillis(1));
}
限流
这里限流的思想就是用到字符串中的incr累加,超过maxQps的部分将会被限流。
@Test
public void testLimit() throws Exception {
String prefix = "order-service";
long maxQps = 10;
long nowSeconds = System.currentTimeMillis() / 1000;
for (int i = 0; i < 15; i++) {
Long result = asyncCommands.incr(prefix + nowSeconds).get(1, TimeUnit.SECONDS);
if (result > maxQps) {
System.out.println("请求被限流");
}else{
System.out.println("请求正常被处理");
}
}
}
以上就是本文要讲的所有内容了。