关于Redis
Redis是一款使用K-V结构的、基于内存实现数据存取的NoSQL非关系型数据库。
使用Redis的主要目的是“缓存数据”,以提高查询数据的效率,对数据库也有一定的保护作用。
需要注意:缓存的数据可能存在“不一致”的问题,因为,如果修改了数据库(例如MySQL)中的数据,而缓存(例如Redis)中的数据没有一并更新,则缓存中的数据是不准确的!但是,并不是所有的场景都要求数据非常准确!
所以,使用Redis的前提条件:
- 对数据的准确性要求不高
- 例如:新浪微博的热搜排名、某个热门视频的播放量
- 数据的修改频率不高
- 例如:电商平台中的类别、电商平台中的品牌
反之,某些情况下是不应该使用Redis的,例如:在秒杀商品时,使用Redis记录一些频繁修改的数据!
在操作系统的终端下,通过redis-cli
命令即可登录Redis控制台:
redis-cli
在Redis控制台中,使用set
和get
命令就可以存取基本类型的数据:
set name liucangsong
get name
在Redis中,有5种典型数据类型:字符串、Hash、Set、zSet、List,在结合编程时,通常,只需要使用“字符串”即可,在程序中,非字符串类型的数据(例如对象、集合等)都会通过工具转换成JSON格式的字符串再存入到Redis中,后续,从Redis中取出的也是字符串,再通过工具转换成原本的类型(例如对象、集合等)即可。
1.1. Redis的简单操作
当已经安装Redis,并确保环境变量可用后,可以在命令提示符窗口(CMD)或终端(IDEA的Terminal,或MacOS/Linux的命令窗口)中执行相关命令。
在终端下,可以通过redis-cli
登录Redis客户端:
redis-cli
在Redis客户端中,可以通过ping
检测Redis是否正常工作,将得到PONG
的反馈:
ping
在Redis客户端中,可以通过set
命令向Redis中存入或修改简单类型的数据:
set name jack
在Redis客户端中,可以通过get
命令从Redis中取出简单类型的数据:
get name
如果使用的Key并不存在,使用
get
命令时,得到的结果将是(nil)
,等效于Java中的null
在Redis客户端中,可以通过keys
命令检索Key:
keys *
keys a*
注意:默认情况下,Redis是单线程的,keys
命令会执行整个Redis的检索,所以,执行时间可能较长,可能导致阻塞!
1.2. 在Spring Boot项目中读写Redis
1.2.1. 添加依赖
需要添加spring-boot-starter-data-redis
依赖项:
<!-- Spring Data Redis:读写Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
以上依赖项默认会连接localhost:6379
,并且无用户名、无密码,所以,当你的Redis符合此配置,则不需要在application.properties
中添加任何配置就可以直接编程。
如果需要显式的配置,各配置项的属性名分别为:
spring.redis.host
spring.redis.port
spring.redis.username
spring.redis.password
1.2.2. 配置RedisTemplate
在使用以上依赖项实现Redis编程时,需要使用到的工具类型为RedisTemplate
,调用此类的对象的方法,即可实现读写Redis中的数据。
在使用之前,应该先在配置类中使用@Bean
方法创建RedisTemplate
,并实现对RedisTemplate
的基础配置,则在项目的根包下创建config.RedisConfiguration
类:
package cn.tedu.csmall.product.config;
import lombok.extern.slf4j.Slf4j;
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.RedisSerializer;
import java.io.Serializable;
/**
* Redis的配置类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Configuration
public class RedisConfiguration {
public RedisConfiguration() {
log.info("加载配置类:RedisConfiguration");
}
@Bean
public RedisTemplate<String, Serializable> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> redisTemplate
= new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.json());
return redisTemplate;
}
}
1.2.3. 使用ValueOperations
读写一般值数据
使用RedisTemplate
访问一般值(字符串、数值等)数据时,需要先获取ValueOperations
对象,再调用此对象的API进行数据操作。
例如:测试向Redis中写入一个字符串:
@Autowired
RedisTemplate<String, Serializable> redisTemplate;
@Test
void testValueOpsSet() {
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
String key = "username";
String value = "admin";
ops.set(key, value);
log.debug("已经向Redis中写入Key={}且Value={}的数据!", key, value);
}
由于声明的RedisTemplate
的值的泛型是Serializable
,所以,从Redis中读取到的值的类型会是Serializable
接口类型。
例如:测试从Redis中读取此前写入的字符串:
@Test
void testValueOpsGet() {
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
// 从Redis中读取数据
String key = "username";
Serializable value = ops.get(key);
log.debug("已经从Redis中读取Key={}的数据,Value={}", key, value);
}
由于配置RedisTemplate
时,使用的值序列化器是JSON(redisTemplate.setValueSerializer(RedisSerializer.json());
),所以,可以直接写入对象,会被自动处理为JSON格式的字符串。
另外,由于声明的RedisTemplate
的值的泛型是Serializable
,所以,写入的值的类型必须实现了Serializable
接口。
例如:测试向Redis中写入一个对象:
@Test
void testValueOpsSetObject() {
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
String key = "brand1";
Brand brand = new Brand();
brand.setId(1L);
brand.setName("大白象");
brand.setEnable(1);
ops.set(key, brand);
log.debug("已经向Redis中写入Key={}且Value={}的数据!", key, brand);
}
例如:测试从Redis中读取此前写入的对象:
@Test
void testValueOpsGetObject() {
ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
String key = "brand1";
Serializable value = ops.get(key);
log.debug("已经从Redis中读取Key={}的数据,Value={}", key, value);
log.debug("读取到的值类型是:{}", value.getClass().getName());
if (value instanceof Brand) {
Brand brand = (Brand) value;
log.debug("将读取到的值类型转换为Brand类型,成功:{}", brand);
} else {
log.debug("读取到的值类型不是Brand类型,无法实现类型转换!");
}
}
1.2.4. 查询Redis中已有的Key
直接调用RedisTemplate
的keys()
方法,即可查询当前Redis中有哪些Key。
例如:测试查询Redis中所有的Key:
@Test
void testKeys() {
Set<String> keys = redisTemplate.keys("*");
for (String key : keys) {
log.debug("{}", key);
}
}
1.2.5. 删除Redis中的数据
删除数据时,不关心值的类型,只需要知道Key即可,所以,删除数据时直接调用RedisTemplate
的delete()
方法即可。
例如:测试删除Redis中的某个数据:
@Test
void testDelete() {
String key = "name";
Boolean result = redisTemplate.delete(key);
log.debug("尝试删除Redis中Key={}的数据,操作结果为:{}", key, result);
}
提示:RedisTemplate
的API中,还有批量删除的操作,例如(以下是RedisTemplate
的部分源代码):
public Long delete(Collection<K> keys) {
if (CollectionUtils.isEmpty(keys)) {
return 0L;
} else {
byte[][] rawKeys = this.rawKeys(keys);
return (Long)this.execute((connection) -> {
return connection.del(rawKeys);
}, true);
}
}
1.2.6. 读写List列表数据
在操作List列表数据之前,需要先调用RedisTemplate
对象的opsForList()
方法,得到ListOperations
对象,再进行列表数据的操作。
在存入列表数据时,ListOperations
支持从左侧压栈来存入数据,或从右侧压栈来存入数据,这2者的区别如下图所示:
通常,从右侧压栈存入数据比较符合大多情况下的需求。
例如:测试写入列表数据:
@Test
void testRightPushList() {
// push:压栈(存入数据)
// pop:弹栈(拿走数据)
// 使用RedisTemplate向Redis存入List数据:
// 1. 需要调用 opsForList() 得到 ListOperations 对象
// 2. ListOperations每次只能存入1个列表项数据
List<Brand> brands = new ArrayList<>();
for (int i = 1; i <= 8; i++) {
Brand brand = new Brand();
brand.setId(i + 0L);
brand.setName("测试品牌" + i);
brands.add(brand);
}
ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
String key = "brandList";
for (Brand brand : brands) {
opsForList.rightPush(key, brand);
}
}
调用ListOperations
对象的size()
方法即可获取列表的长度。
例如:获取列表的长度:
@Test
void testListSize() {
ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
String key = "brandList";
Long size = opsForList.size(key);
log.debug("列表 key={} 的长度(元素数量)为:{}", key, size);
}
如果需要读取数据,首先必须了解,Redis中的列表数据项即有正数的索引(下标),也有负数的索引(下标),正数的是从左侧第1位使用0
开始向右顺序编号,而负数的是从右侧第1位使用-1
并向左侧递减的编号,如下图所示:
使用ListOperations
的range()
方法可以获取列表的区间段子列表。
例如:测试获取列表数据:
@Test
void testListRange() {
ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
String key = "brandList";
long start = 0L;
long end = -1L;
List<Serializable> list = opsForList.range(key, start, end);
for (Serializable serializable : list) {
log.debug("列表项:{}", serializable);
}
}
1.3. 封装Redis的读写
在当前项目中,品牌、类别这些数据应该是适合使用Redis的!
以缓存品牌数据为例,可以先在根包下创建repository.IBrandCacheRepository
接口:
package cn.tedu.csmall.product.repository;
import cn.tedu.csmall.product.pojo.vo.BrandListItemVO;
import cn.tedu.csmall.product.pojo.vo.BrandStandardVO;
import java.util.List;
import java.util.Set;
/**
* 处理品牌缓存数据的存储接口
*
* @author java@tedu.cn
* @version 0.0.1
*/
public interface IBrandCacheRepository {
/**
* 将某个品牌数据保存到Redis中
*
* @param brandStandardVO 品牌数据
*/
void save(BrandStandardVO brandStandardVO);
/**
* 从Redis中取出品牌数据
*
* @param id 品牌id
* @return 匹配的品牌数据,如果Redis中没有匹配的数据,将返回null
*/
BrandStandardVO get(Long id);
/**
* 将品牌列表数据保存到Redis中
*
* @param brandList 品牌列表数据
*/
void saveList(List<BrandListItemVO> brandList);
/**
* 从Redis中取出品牌列表数据
*
* @return 品牌列表数据
*/
List<BrandListItemVO> getList();
/**
* 获取所有品牌缓存数据的Key
*
* @return 所有品牌缓存数据的Key
*/
Set<String> getAllKeys();
/**
* 删除所有缓存的品牌数据
*
* @param keys 所有缓存的品牌数据的Key的集合
* @return 删除的数据的数量
*/
Long deleteAll(Set<String> keys);
}
然后,在根包下创建repository.impl.BrandCacheRepositoryImpl
类,实现以上接口:
package cn.tedu.csmall.product.repository.impl;
import cn.tedu.csmall.product.pojo.entity.Brand;
import cn.tedu.csmall.product.pojo.vo.BrandListItemVO;
import cn.tedu.csmall.product.pojo.vo.BrandStandardVO;
import cn.tedu.csmall.product.repository.IBrandCacheRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Repository;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* 处理品牌缓存数据的存储实现类
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Repository
public class BrandCacheRepositoryImpl implements IBrandCacheRepository {
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
public static final String KEY_ITEM_PREFIX = "brand:item:";
public static final String KEY_LIST = "brand:list";
public BrandCacheRepositoryImpl() {
log.debug("创建处理缓存的对象:BrandCacheRepositoryImpl");
}
@Override
public void save(BrandStandardVO brandStandardVO) {
ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
String key = getItemKey(brandStandardVO.getId());
opsForValue.set(key, brandStandardVO);
}
@Override
public BrandStandardVO get(Long id) {
ValueOperations<String, Serializable> opsForValue = redisTemplate.opsForValue();
String key = getItemKey(id);
Serializable value = opsForValue.get(key);
if (value != null) {
if (value instanceof BrandStandardVO) {
return (BrandStandardVO) value;
}
}
return null;
}
@Override
public void saveList(List<BrandListItemVO> brandList) {
ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
for (BrandListItemVO brand : brandList) {
opsForList.rightPush(KEY_LIST, brand);
}
}
@Override
public List<BrandListItemVO> getList() {
ListOperations<String, Serializable> opsForList = redisTemplate.opsForList();
long start = 0L;
long end = -1L;
List<Serializable> list = opsForList.range(KEY_LIST, start, end);
List<BrandListItemVO> brands = new ArrayList<>();
for (Serializable serializable : list) {
brands.add((BrandListItemVO) serializable);
}
return brands;
}
@Override
public Set<String> getAllKeys() {
String allKeysPattern = "brand:*";
return redisTemplate.keys(allKeysPattern);
}
@Override
public Long deleteAll(Set<String> keys) {
return redisTemplate.delete(keys);
}
private String getItemKey(Long id) {
return KEY_ITEM_PREFIX + id;
}
}
在src/test/java
的根包下创建repository.BrandCacheRepositoryTests
测试类,测试以上方法:
package cn.tedu.csmall.product.repository;
import cn.tedu.csmall.product.pojo.vo.BrandStandardVO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@Slf4j
@SpringBootTest
public class BrandCacheRepositoryTests {
@Autowired
IBrandCacheRepository repository;
@Test
void testSave() {
BrandStandardVO brandStandardVO = new BrandStandardVO();
brandStandardVO.setId(1L);
brandStandardVO.setName("华为");
repository.save(brandStandardVO);
log.debug("存入数据完成!");
}
@Test
void testGet() {
Long id = 1L;
BrandStandardVO brandStandardVO = repository.get(id);
log.debug("获取数据完成:{}", brandStandardVO);
}
}