RedisTemplate 常用方法,序列化方式,分布式锁,工具类封装、商品秒杀、禁用Keys推荐Scan
目录RedisTemplate 常用方法RedisTemplate 序列化方式RedisTemplate 常用方法org.springframework.data.redis.core.RedisTemplate 常用方法(本文环境 Spring Boot 2.1.3):方法描述Boolean expire(K key, final long timeout, f...
目录
客户端 Jedis、Redisson、Lettuce 对比
1、三个都提供了基于 Redis 操作的 Java API,只是封装程度,具体实现稍有不同。
Jedis | Java 实现的客户端。支持基本的数据类型如:String、Hash、List、Set、Sorted Set。 使用阻塞的 I/O,方法调用同步,程序流需要等到 socket 处理完 I/O 才能执行,不支持异步操作。Jedis 客户端实例不是线程安全的,需要通过连接池来使用 Jedis。 |
Redisson | 分布式锁,分布式集合,可通过 Redis 支持延迟队列。 |
Lettuce | 用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。 基于 Netty 框架的事件驱动的通信层,其方法调用是异步的。Lettuce 的 API 是线程安全的,所以可以操作单个 Lettuce 连接来完成各种操作。 |
RedisTemplate 常用方法
org.springframework.data.redis.core.RedisTemplate 常用方法(本文环境 Spring Boot 2.1.3):
方法 | 描述 |
---|---|
Boolean expire(K key, final long timeout, final TimeUnit unit) | 为指定的 key 指定缓存失效时间。时间一到 key 会被移除。key 不存在时,不影响。 注意:如果 key 后续被重新设置值,比如 set key value,则 key 过期时间失效,需要重新设置。 |
Boolean expireAt(K key, final Date date) | 设置 key 失效日期。注意:如果 key 后续被重新设置值,比如 set key value,则 key 过期时间失效,需要重新设置。 |
Long getExpire(K key) | 获取 key 的剩余过期时间。 -1 表示永久有效。-2 表示 key 不存在。 |
Long getExpire(K key, final TimeUnit timeUnit) | 获取 key 的剩余过期时间,并换算成指定的时间单位 |
Boolean hasKey(K key) | 判断 key 是否存在 |
Boolean delete(K key) | 删除指定的 key |
Long delete(Collection keys) | 删除多个 key |
RedisSerializer<?> getDefaultSerializer() | 获取默认的序列化方式。RedisTemplate 是 JdkSerializationRedisSerializer;StringRedisTemplate 是 StringRedisSerializer |
Set keys(K pattern) | 获取整个库下符合指定正则的所有 key,如 keys(*) 获取所有 key |
Boolean move(K key, final int dbIndex) | 将 key 从当前库移动目标库 dbIndex |
ClusterOperations<K, V> opsForCluster() | 获取 ClusterOperations 用于操作集群 |
GeoOperations<K, V> opsForGeo() | 获取 GeoOperations 用于操作地图 |
<HK, HV> HashOperations<K, HK, HV> opsForHash() | 获取 HashOperations 用于操作 hsha 数据类型 |
ListOperations<K, V> opsForList() | 获取 ListOperations 用于操作 list 类型 |
SetOperations<K, V> opsForSet() | 获取 SetOperations 用于操作无序集合 |
ValueOperations<K, V> opsForValue() | 获取 ValueOperations 用于操作 String类型 |
ZSetOperations<K, V> opsForZSet() | 获取 ValueOperations 用于操作有序集合 |
rename(K oldKey, K newKey) | 为 oldKey 进行重命名 |
DataType type(K key) | 查询缓存 key 的类型,DataType 是一个枚举,code(name) 可选值如下:none, string, list, set, zset hash none 表示 key 不存在,或者类型不确定 |
T execute(RedisScript<T> script, List<K> keys, Object... args) | 执行给定的 lau 脚本。 1、多个操作使用 lau 脚本统一执行是事务安全的,具有原子性。 2、脚本中 KEYS[x] 是对 keys 进去取值,ARGV[x] 是对 args 进行取值,索引从1开始。 3、返回脚本执行的结果,类型与 RedisScript 的类型一致。 |
T RedisTemplate.execute(RedisCallback<T> action) | 在Redis连接中执行给定的操作。只要有可能,动作对象抛出的应用程序异常就会传播给调用者(只能取消选中)。Redis异常被转换为适当的DAO异常。允许返回结果对象,即域对象或域对象的集合。对给定对象与适用于Redis存储的二进制数据之间执行自动序列化/反序列化。注意:回调代码本身不应该处理事务!使用适当的事务管理器。通常,回调代码不得触及任何连接生命周期方法,如close,以让模板完成其工作。 |
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisStuWebApplicationTests {
//注入 RedisTemplate 或者 StringRedisTemplate 其中一个即可,前者是后者的父类。它们默认已经全部在容器种了.
//org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration 中已经自动将 RedisTemplate 添加到了容器中,直接获取使用即可.
@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Test
public void test1() throws InterruptedException {
ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
opsForValue.set("wmx_name", "Zhang San");//设置字符串值
System.out.println(opsForValue.get("wmx_name"));//Zhang San
stringRedisTemplate.expire("wmx_name", 60, TimeUnit.SECONDS);//设置 key 失效时间
// for (int i = 0; i < 15; i++) {
// System.out.println(stringRedisTemplate.getExpire("wmx_name"));
// System.out.println(stringRedisTemplate.getExpire("wmx_name", TimeUnit.SECONDS));
// Thread.sleep(1000);
// }
//判断是否含有指定的 key
System.out.println(stringRedisTemplate.hasKey("wmx_name") + ", " + stringRedisTemplate.hasKey("wmx_name_2"));//true, false
opsForValue.set("age", "33");
opsForValue.set("address", "长沙");
stringRedisTemplate.delete("age");//删除 key
Date stopDate = new Date();
stopDate.setTime(System.currentTimeMillis() + (60 * 1000));
stringRedisTemplate.expireAt("address", stopDate);//设置 key 1 分钟后失效
List<RedisClientInfo> clientList = stringRedisTemplate.getClientList();
System.out.println(clientList);
System.out.println(redisTemplate.getDefaultSerializer());//获取默认序列化方式
Set<String> keys = stringRedisTemplate.keys("*");//获取当前库下所有的 key
System.out.println("keys=" + keys);
Boolean move = stringRedisTemplate.move("address", 2);//将 address 移动 2 号数据库
System.out.println("move=" + move);
opsForValue.set("info", "描述");
System.out.println(opsForValue.get("info"));
stringRedisTemplate.rename("info", "summary");//对 key 进行重命名
}
}
演示源码:src/test/java/com/wmx/wmxredis/WmxRedisApplicationTests.java · 汪少棠/wmx-redis - Gitee.com
其它常用操作与方法
redisTemplate.getConnectionFactory().getConnection().flushAll(); //清空 redis 所有数据库(all databases)中的所有数据(all keys) |
redisTemplate.getConnectionFactory().getConnection().flushDb(); //清空 redis 当前连接的数据库(selected database)中的所有数据(all keys) |
生产环境禁用Keys而推荐用Scan & 模糊删除
https://gitee.com/wangmx1993/material/blob/blob/master/doc/csdn/md/生产环境禁用Keys而推荐用Scan.md。
模糊删除key:wmx-redis/blob/master/src/main/java/com/wmx/wmxredis/controller/RedisCacheUtil.java。
RedisTemplate 序列化方式
1、StringRedisTemplate 继承 RedisTemplate,主要区别就是前者默认使用 StringRedisSerializer 序列化 String,后者默认使用 JdkSerializationRedisSerializer 序列化对象。
2、RedisTemplate<K, V> 可以用来存储对象,如 Map、List 、Set、POJO 等,但对象需要实现 Serializable 序列化接口。此种方式序列化时,以二进制数组方式存储,内容没有可读性。
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("id", 1001);
dataMap.put("name", "张三");
HashOperations opsForHash = redisTemplate.opsForHash();
opsForHash.putAll("map_1",dataMap);
redisTemplate.expire("map_1",60,TimeUnit.SECONDS);
Map map_11 = opsForHash.entries("map_1");
System.out.println(map_11);//{name=张三, id=1001}
3、StringRedisTemplate 专门用来存储字符串,StringRedisTemplate extends RedisTemplate<String, String>。序列化接口 org.springframework.data.redis.serializer.RedisSerializer 的实现类如下:
序列化方式 | 描述 |
---|---|
FastJsonRedisSerializer | 采用 com.alibaba.fastjson 进行序列化与反序列化 ,提供了 (Class type) 参数的构造器,需要传入对象类型。 |
GenericFastJsonRedisSerializer | 提供了基本的 GenericFastJsonRedisSerializer() 构造器。 |
GenericJackson2JsonRedisSerializer | 与 Jackson2JsonRedisSerializer 功能差不多,构造函数可以指定 Class,默认为 String. |
GenericToStringSerializer | 需要调用者传一个对象到字符串互转的 Converter(相当于转换为字符串的操作交给转换器去做) |
Jackson2JsonRedisSerializer | 采用 com.fasterxml.jackson 进行序列化与反序列化。优点是速度快,序列化后的字符串短小精悍,不需要实现Serializable接口。构造函数必须传入 Class 类型。 |
JdkSerializationRedisSerializer | RedisTemplate 默认的序列化方式。要求存储的对象必须实现java.io.Serializable接口。序列化后的结果非常庞大。存储的为二进制数据,对开发者不友好。 |
OxmSerializer | 以 xml 格式存储,解析起来比较复杂,效率也比较低 |
StringRedisSerializer | StringRedisTemplate 默认的字符串序列化方式。key 和 value 都会采用此方式进行序列化,是被推荐使用的,对开发者友好,轻量级,效率也比较高。此时只能是 String,不能是其它对象,否则报错。 |
4、本文环境 Spring Boot 2.1.3 + Java JDK 1.8,下面以指定 Jackson2JsonRedisSerializer 序列化方式为例:
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
//自定义 RedisTemplate 序列化方式
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();//创建 RedisTemplate,key 和 value 都采用了 Object 类型
redisTemplate.setConnectionFactory(redisConnectionFactory);//绑定 RedisConnectionFactory
//创建 Jackson2JsonRedisSerializer 序列方式,对象类型使用 Object 类型,
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);//设置一下 jackJson 的 ObjectMapper 对象参数
// 设置 RedisTemplate 序列化规则。因为 key 通常是普通的字符串,所以使用 StringRedisSerializer 即可。
// 而 value 是对象时,才需要使用序列化与反序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());// key 序列化规则
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value 序列化规则
redisTemplate.setHashKeySerializer(new StringRedisSerializer());// hash key 序列化规则
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);// hash value 序列化规则
redisTemplate.afterPropertiesSet();//属性设置后操作
return redisTemplate;//返回设置好的 RedisTemplate
}
}
在线演示源码:src/main/java/com/wmx/wmxredis/config/RedisConfig.java · 汪少棠/wmx-redis - Gitee.com
5、RedisTemplate 使用就很简单了,保存数据与读取数据时,直接操作对象即可,会自动进行序列化与反序列化:
import com.wmx.wmxredis.beans.Person;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* RedisTemplate 操作 redis
*
* @author wangMaoXiong
*/
@RestController
public class RedisController {
/**
* 从容器中获取 RedisTemplate 实例
*/
@Resource
private RedisTemplate redisTemplate;
/**
* 保存数据,设置缓存:http://localhost:8080/redis/save?id=1000&name=张三
*
* @param person
* @return
*/
@GetMapping("redis/save")
public String redisCache(Person person) {
ValueOperations opsForValue = redisTemplate.opsForValue();
ListOperations opsForList = redisTemplate.opsForList();
HashOperations opsForHash = redisTemplate.opsForHash();
person.setBirthday(new Date());
//设置缓存。演示三种数据类型:字符串、列表、hash
opsForValue.set(RedisController.class.getName() + "_string" + person.getId(), person);
opsForList.rightPushAll(RedisController.class.getName() + "_list" + person.getId(), person, person);
opsForHash.put(RedisController.class.getName() + "_map", "person" + person.getId(), person);
//设置 key 失效时间
redisTemplate.expire(RedisController.class.getName() + "_string" + person.getId(), 60, TimeUnit.SECONDS);
redisTemplate.expire(RedisController.class.getName() + "_list" + person.getId(), 60, TimeUnit.SECONDS);
redisTemplate.expire(RedisController.class.getName() + "_map", 60, TimeUnit.SECONDS);
return "缓存成功.";
}
/**
* 查询缓存:http://localhost:8080/redis/get?personId=1000
*
* @param personId
* @return
*/
@GetMapping("redis/get")
public List<Person> getRedisCache(@RequestParam Integer personId) {
//1、演示三种数据类型:字符串、列表、hash
ValueOperations opsForValue = redisTemplate.opsForValue();
ListOperations opsForList = redisTemplate.opsForList();
HashOperations opsForHash = redisTemplate.opsForHash();
//2、读取缓存,如果 key 不存在,则返回为 null.
Person person = (Person) opsForValue.get(RedisController.class.getName() + "_string" + personId);
List<Person> personList = opsForList.range(RedisController.class.getName() + "_list" + personId, 0, -1);
Person person1 = (Person) opsForHash.get(RedisController.class.getName() + "_map", "person" + personId);
System.out.println("person=" + person);
System.out.println("personList=" + personList);
System.out.println("person1=" + person1);
return personList;
}
}
在线演示源码: src/main/java/com/wmx/wmxredis/controller/RedisController.java · 汪少棠/wmx-redis - Gitee.com src/main/java/com/wmx/wmxredis/beans/Person.java · 汪少棠/wmx-redis - Gitee.com |
温馨提示! 关于 RedisTemplate 的序列化,实际生产中遇到一次需要将数据库查出来的一个 List 对象放到 Redis 中缓存,当时数据量达到 20 万行,结果 put 到缓存的时间长达 6、7 秒,然后当使用 alibaba 的 fastjson 手动先将 List 对象序列化为字符串,然后作为普通的字符串使用 ValueOperations set 到缓存,此时 set 的时间降到了 100 毫秒内,而 fastjson 序列化对象同样非常之快。取值时,同样将取出的字符串使用 fastjosn 手动反序列化成 List 即可。 |
分布式锁需求分析 与 主流实现方式
基于 Redis 实现分布式锁
1、分布式索最常见的一种方案就是使用 Redis 做分布式锁,使用 Redis 做分布式锁的思路是:在 redis 中设置一个值表示加了锁,然后释放锁的时候就把这个 key 删除。
// 获取锁
// NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间
SET anyLock unique_value NX PX 30000
// 释放锁:通过执行一段lua脚本
// 释放锁涉及到两条指令,这两条指令不是原子性的
// 需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
2、一定要用 "SET key value NX PX milliseconds" 命令:否则先设置了值,再设置过期时间,这个不是原子性操作,有可能在设置过期时间之
前宕机,会造成死锁(key永久存在)
3、value 要具有唯一性:在解锁的时候,需要验证 value 是和加锁的一致才删除 key。
这是避免了一种情况:假设A获取了锁,过期时间30s,此时35s之后,锁已经自动释放了,A
去释放锁,但是此时可能B获取了锁。A客户端就不能删除B的锁了。
4、下面通过 RedisTemplate 进行实现:
/**
* http://localhost:8080/redis/execute?key=wwww&value=1rui
* <p>
* Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit)
* Boolean setIfAbsent(K key, V value, Duration timeout)
* * 1、key 不存在时进行设值,返回 true; 否则 key 存在时,不进行设值,返回 false.
* * 2、此方法相当于先设置 key,然后设置 key 的过期时间,它的操作是原子性的,是事务安全的。
* * 3、相当于:SET anyLock unique_value NX PX 30000,NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间
* T execute(RedisScript<T> script, List<K> keys, Object... args):执行给定的脚本。
* * 1、多个操作使用 lau 脚本统一执行是事务安全的,具有原子性
* * 2、脚本中 KEYS[x] 是对 keys 进去取值,ARGV[x] 是对 args 进行取值,索引从1开始.
* * 3、返回脚本执行的结果,类型与 RedisScript 的类型一致。
*
* @param key
* @param value
* @return
* @throws InterruptedException
*/
@GetMapping("redis/execute")
public Map<String, Object> execute(@RequestParam String key, @RequestParam String value) throws InterruptedException {
Map<String, Object> returnMap = new HashMap<>();
Boolean ifAbsent = false;
try {
ifAbsent = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(60));
if (!ifAbsent) {
returnMap.put("code", 500);
returnMap.put("msg", "程序正在处理中,请稍后再试!");
return returnMap;
}
TimeUnit.SECONDS.sleep(10);//休眠 10 秒,模拟执行业务代码
System.out.println("执行业务代码.");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (ifAbsent){
//接口执行完毕后删除 key,key 不存在时 execute 方法返回 0
//此种脚本删除的方式在 redis 集群部署时会报错,实际上直接使用 redisTemplate.delete(cacheKey) 也是可以的
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = RedisScript.of(script, Long.class);
// 返回删除key的个数,未删除成功时,返回 0
Object execute = redisTemplate.execute(redisScript, Arrays.asList(key), value);
returnMap.put("data", execute);
}
}
returnMap.put("code", 200);
returnMap.put("msg", "seccess");
return returnMap;
}
在线演示源码:src/main/java/com/wmx/wmxredis/controller/RedisController.java · 汪少棠/wmx-redis - Gitee.com
并发压测:src/main/java/com/wmx/hb/controller/FlowConfigController.java · 汪少棠/hb - Gitee.com
工具类封装
/src/main/java/com/wmx/wmxredis/controller/RedisCacheUtil.java。
实现商品秒杀
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)