Redis
Redis 是一个开源的,高级的键值对存储和一个适用的解决方案,用于构建高性能,可扩展的 Web 应用程序。
场景:在互联网中经常用来缓存热点数据:1. redis 数据在内存中,可以保证读取的高效(接近每秒数十万次);2. 减少下层持久层数据库读取压力,像 mongodb,每秒近千次就有压力;3. redis 单线程运行,天然具备读写的原子性
使用:1. 先 get 读取 redis,没有读到再去 db;将 db 读到的数据 set 到 redis 中,返回数据。2. 更新:del 掉 redis 的数据,写数据库。
Redis 有六(五)种基本数据结构,分别是:string(字符串)、list(列表)、hash(字典)、set(集合)、zset(有序集合)和 Stream(流)。
Spring整合了整合了 Redis ,我们可以导入 Spring Data Redis 来实现对 Redis 的使用。
Spring Data Redis 提供了统一的操作模版(RedisTemplate)封装了对 Jedis、Lettuce 的 API 操作,所以实际上访问的还是 Jedis、Lettuce 等 API。目前,Spring Data Redis 暂时只支持 Jedis、Lettuce 的内部封装,而 Redisson 是由 redisson-spring-data 来支持。
1. 在实际开发中,大部分还是使用 Jedis。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!-- 去掉 Lettuce 的依赖,因为 Spring Boot 优先使用 Lettuce 作为 Redis 客户端 -->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入 Jedis 的依赖,使 Spring Boot 实现对 Jedis 的自动化配置-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- 实现对 json 序列化工具-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
<!---->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
配置文件:
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
timeout: 0
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1
2. RedisTemplate
RedisTemplate 提供了 Redis 操作不同类型的数据的模版,所以这个类一定要看一下。
3. 序列化
存储在 Redis 中的数据是二进制数据。这里用到了 org.springframework.data.redis.serializer.RedisSerializer<T>
@Nullable
byte[] serialize(@Nullable T t) throws SerializationException;
@Nullable
T deserialize(@Nullable byte[] bytes) throws SerializationException;
衍生出的序列化有四种方式:JDK 序列化方式(基本不用,存储带有十六进制信息)、String 序列化方式(常用)、JSON 序列化(常用)和 XML 序列化方式。
String 序列化:org.springframework.data.redis.serializer.StringRedisSerializer
@Override
public String deserialize(@Nullable byte[] bytes) {
return (bytes == null ? null : new String(bytes, charset));
}
@Override
public byte[] serialize(@Nullable String string) {
return (string == null ? null : string.getBytes(charset));
}
JSON 序列化:com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer
public byte[] serialize(Object object) throws SerializationException {
if (object == null) {
return new byte[0];
} else {
try {
return JSON.toJSONBytes(object, new SerializerFeature[]{SerializerFeature.WriteClassName});
} catch (Exception var3) {
throw new SerializationException("Could not serialize: " + var3.getMessage(), var3);
}
}
}
public Object deserialize(byte[] bytes) throws SerializationException {
if (bytes != null && bytes.length != 0) {
try {
return JSON.parseObject(new String(bytes, IOUtils.UTF8), Object.class, defaultRedisConfig, new Feature[0]);
} catch (Exception var3) {
throw new SerializationException("Could not deserialize: " + var3.getMessage(), var3);
}
} else {
return null;
}
}
在实际使用中,我们会将 redis 的 key 和 value 都使用 String 序列化。这里我们采用将 key 使用 String 序列化和 value 使用 Json 序列化的方式,具体使用如下:
在配置文件中配置:
/**
* 使用配置文件将 Redis 默认的序列化方式修改为自己需要的,因为在传输过程中不能只是使用原数据直接存储到 redis 中
* 这里对于 key 的序列化使用了 string 序列化
* value 使用了 json 序列化
*/
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
//创建 RedisTemplate 对象
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 RedisFactory ,它就是实现多种 Java Redis 客户端接入的工厂,针对四种类型有4个连接实现,这样就可以满足不同连接请求
redisTemplate.setConnectionFactory(factory);
// 使用 String 序列化方式序列化 key
redisTemplate.setKeySerializer(RedisSerializer.string());
// 使用 JSON 序列化方式序列化 value(json 序列化结果会包含类名(@class)
redisTemplate.setValueSerializer(RedisSerializer.json());
return redisTemplate;
}
/**
* redis 提供了 Pub/Sub 功能,可以实现简单的订阅功能。
*
*/
@Bean
public RedisMessageListenerContainer listenerContainer(RedisConnectionFactory factory) {
// 创建对象
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
// 设置工厂
container.setConnectionFactory(factory);
// 添加监听器
container.addMessageListener(new TestPatternTopicMessageListener(), new ChannelTopic("TEST"));
container.addMessageListener(new TestPatternTopicMessageListener(), new ChannelTopic("AOTEMAN"));
return container;
}
}
在访问数据库时,我们会创建 dao 包,存放每个 DO 对应的 Dao 对应。那么,对于每一个 CacheObject 类,我们也应该创建一个对应的 Dao 类。
/**
* dao 包下存放每个 DO 对应的 Dao。对应 UserCacheObject
*/
@Repository
public class UserCacheDAO {
/**
* user:用户编号,通过静态变量,声明 KEY 的前缀,并且用冒号作为分割
*/
private static final String KEY_PATTERN = "user:%d";
/**
* 通过 @Resource 注入指定名字的 RedisTemplate 对应的 operations 对象
* 用以明确每个 KEY 的类型
*/
@Resource(name = "redisTemplate")
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
private ValueOperations<String, String> operations;
/**
* 声明 KEY_PATTERN 对应的 KEY 拼接方法,避免散落在每个方法中。
* @param id ID
* @return key 拼接
*/
public static String buildKey(Integer id) {
return String.format(KEY_PATTERN, id);
}
public UserCacheObject get(Integer id) {
String key = buildKey(id);
String value = operations.get(key);
return JSONUtil.parseObject(value, UserCacheObject.class);
}
public void set(Integer id, UserCacheObject object) {
String key = buildKey(id);
String value = JSONUtil.toJSONString(object);
operations.set(key, value);
}
}
测试:
@Resource
private StringRedisTemplate stringRedisTemplate;
@Test
public void testJDKSerializer() {
redisTemplate.opsForValue().set("jdk","value");
}
@Test
public void testStringSetKeyUserCache() {
UserCacheObject object = new UserCacheObject();
object.setId(1);
object.setName("悲伤");
object.setGender(2);
String format = String.format("user:%d", object.getId());
redisTemplate.opsForValue().set(format, object);
}
@Test
public void testStringGetKeyUserCache() {
String key = String.format("user:%d", 1);
Object value = redisTemplate.opsForValue().get(key);
System.out.println(value);
}
@Test
public void testPipe() {
stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (int i = 0; i < 3; i++) {
connection.set(String.format("gui:%d", i).getBytes(), ("pipe" + i).getBytes());
}
for (int i = 0; i < 3; i++) {
connection.get(String.format("gui:%d", i).getBytes());
}
return null;
});
}
@Test
public void testSession() {
String resultsRed = stringRedisTemplate.execute(new SessionCallback<String>() {
@Override
public String execute(RedisOperations operations) throws DataAccessException {
for (int i = 0; i < 100; i++) {
operations.opsForValue().set(String.format("Gui:%d",i),"cool");
}
return (String) operations.opsForValue().get(String.format("Gui:%d", 0));
}
});
System.out.println(resultsRed);
}
@Test
public void testListener() throws InterruptedException {
for (int i = 0; i < 10; i++) {
// 发布消息
stringRedisTemplate.convertAndSend("TEST", "gui" + i);
Thread.sleep(1000l);
}
}
}
4. Script
Redis 提供 Lua 脚本,满足我们希望组合排列使用 Redis 的命令,保证串行执行的过程中,不存在并发的问题。同时,通过将多个命令组合在同一个 Lua 脚本中,一次请求,直接处理。
创建 resources/compareAndSet.lua
脚本,实现 CAS 功能。
if redis.call('GET', KEYS[1]) ~= ARGV[1] then
return 0
end
redis.call('SET',KEYS[1],ARGV[2])
return 1
测试:
@Test
public void testLua() throws IOException {
List<String> strings = Files.readAllLines(Paths.get("src/main/resources/lua","compareAndSet.lua"));
//
StringBuilder stringBuilder = new StringBuilder();
strings.forEach(stringBuilder::append);
DefaultRedisScript<Long> script = new DefaultRedisScript<>(stringBuilder.toString(), Long.class);
//
Long execute = stringRedisTemplate.execute(script, Collections.singletonList("gui:1"));
System.out.println(execute);
}