1. 使用Jedis
1.1 准备工作
在maven项目的pom.xml文件中加上这个依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
我们之前操作redis的数据,都是通过redis-cli这个客户端。所以我们在用Java操作redis时,首先要获取这个客户端对象了。
Jedis jedis = new Jedis(ip, port);
这个jedis就相当于redis-cli了。
上面列举的所有指令,在Jedis中都有对应的方法实现它。
举一些例子,就不做注释了,大家看代码就能知道是什么操作命令。
1.2 key相关
Set<String> result1 = jedis.keys("*");
Boolean result2 = jedis.exists("key1");
Long result3 = jedis.expire("key", 20);
Long result4 = jedis.ttl("key");
String result5 = jedis.type("key");
String result6 = jedis.rename("oldkey", "newkey");
Long result7 = jedis.del("key1", "key2", "key3");
1.3 string相关
String result1 = jedis.set("key1", "1111");
Long result2 = jedis.setnx("key2", "2222");
String result3 = jedis.setex("key3", 30, "3333");
String result4 = jedis.mset("k1", "v1", "k2", "v2");
Long result5 = jedis.msetnx("k1", "v1", "k2", "v2", "k3", "v3");
String result6 = jedis.get("key1");
List<String> result7 = jedis.mget("k1", "k2");
Long result8 = jedis.append("key2", "222");
Long result9 = jedis.strlen("key3");
Long result10 = jedis.incr("key1");
Long result11 = jedis.decr("key1");
Long result12 = jedis.incrBy("key1", 200);
Long result13 = jedis.decrBy("key1", 200);
String result14 = jedis.getrange("key1", 0, -1);
Long result15 = jedis.setrange("key1", 2, "11111");
1.4 list相关
Long result1 = jedis.lpush("list1", "a", "b", "c");
Long result2 = jedis.rpush("list1", "d", "e", "f");
String result3 = jedis.lset("list1", 2, "p");
List<String> result4 = jedis.lrange("list1", 0, -1);
String result5 = jedis.lpop("list1");
String result6 = jedis.rpop("list1");
String result7 = jedis.lindex("list1", 1);
Long result8 = jedis.llen("list1");
Long result9 = jedis.lrem("list1", 0, "e");
String result10 = jedis.ltrim("list1", 0, -2);
1.5 hash相关
我们知道hash的value是field-value键值对,一个key对应多个键值对,这样我们想到,可以用Map去存储这些键值对。
我们创建一个Map
Map<String, String> map = new HashMap<>();
map.put("name", "zhangsan");
map.put("age", "30");
map.put("score", "98.5");
接下来就是用jedis的操作了
Long result1 = jedis.hset("hash1", map);
String result2 = jedis.hget("hash1", "age");
List<String> result3 = jedis.hmget("hash1", "name", "score");
Map<String, String> result4 = jedis.hgetAll("hash1");
Long result5 = jedis.hdel("hash1", "name");
Long result6 = jedis.hlen("hash1");
Boolean result7 = jedis.hexists("hash1", "school");
Set<String> result8 = jedis.hkeys("hash1");
List<String> result9 = jedis.hvals("hash1");
Long result10 = jedis.hsetnx("hash1", "score", "21.1");
1.6 set相关
Long result1 = jedis.sadd("set1", "name", "age", "score", "address");
Long anotherSet = jedis.sadd("set2", "cat", "dog", "age", "bird");
Set<String> result2 = jedis.smembers("set1");
Boolean result3 = jedis.sismember("set1", "age");
Long result4 = jedis.scard("set1");
Long result5 = jedis.srem("set1", "name", "age");
List<String> result6 = jedis.srandmember("set1", 1);
Set<String> result7 = jedis.spop("set1", 1);
Long result8 = jedis.smove("age", "set1", "set2");
Set<String> result9 = jedis.sdiff("set1", "set2");
Set<String> result10 = jedis.sinter("set1", "set2");
Set<String> result11 = jedis.sunion("set1", "set2");
1.7 zset相关
zset类似于hash,value部分存储的是value-score键值对,所以我们在创建zset对象时,也需要先创建一个Map。
Map<String, Double> map = new HashMap<>();
map.put("v1", 10.0);
map.put("v2", 15.0);
map.put("v3", 5.0);
map.put("v4", 12.0);
接下来是jedis操作zset的方法
Long result1 = jedis.zadd("zset1", map);
Set<String> result2 = jedis.zrange("zset1", 1, 2);
Set<String> result3 = jedis.zrangeByScore("zset1", 4.0, 13.0);
Long result4 = jedis.zcard("zset1");
Long result5 = jedis.zcount("zset1", 4.0, 13.0);
Long result6 = jedis.zrank("zset1", "v2");
Double result7 = jedis.zscore("zset1", "v3");
Long result8 = jedis.zrem("zset1", "v1", "v5");
2. 使用SpringDataRedis
2.1 介绍说明
上面使用jedis能完美还原redis的所有基础操作命令,但我总感觉不太优雅,一是因为Jedis的方法实在是太多了,完全背下这么多操作命令也不太现实,费时费力;二是因为我们开发Java的web项目,都是使用Spring框架完成的,而Jedis是Redis官方推出的,并没有与Spring这个大家族融合在一起。为了解决这个问题,Spring官方推出了一个SpringDataRedis。
SpringDataRedis针对Jedis提供了如下功能:
- 提供了一个高度封装的RedisTemplate类,自动管理连接池。
- 针对Jedis客户端中大量api进行归类封装,同一类型操作封装为一个Operation接口。
ValueOperations 对应方法 redisTemplate.opsForValue();
ListOperations 对应方法 redisTemplate.opsForList();
HashOperations 对应方法 redisTemplate.opsForHash();
SetOperations 对应方法 redisTemplate.opsForSet();
ZSetOperations 对应方法 redisTemplate.opsForZSet();
- 提供对key的bound(绑定)便捷化操作api,通过bound封装指定类型的key,然后进行一系列操作而无需“显式”再次指定key。同样对应有五种bound类。
BoundValueOperations 对应方法 redisTemplate.boundValueOps(key);
BoundListOperations 对应方法 redisTemplate.boundListOps(key);
BoundHashOperations 对应方法 redisTemplate.boundHashOps(key);
BoundSetOperations 对应方法 redisTemplate.boundSetOps(key);
BoundZSetOperations 对应方法 redisTemplate.boundZSetOps(key);
- 将事务操作封装,由容器控制。
- 针对数据的序列化/反序列化,提供了多种可选择策略。
2.2 准备工作
在maven项目的pom.xml中加入这个依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在properties或yml配置文件中指定redis的服务地址
spring:
redis:
host: 10.40.18.10
port: 6379
database: 0
这只是一部分基础配置,其余的配置若需要,可自行查询添加。
这里需要说明一件事,在放这个配置前查看一下pom.xml文件parent标签里的版本,例如这里的是2.7.3版本。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
如果版本是3.0.0及以上,我们在写redis的配置文件时会有一点不一样的地方,后文同。
spring:
data:
redis:
host: 10.40.18.10
port: 6379
database: 0
可能还有其他不同之处,待我慢慢发掘。
2.3 操作五种数据类型
首先使用@Autowired注入RedisTemplate。
@Autowired
private RedisTemplate redisTemplate;
2.3.1 key的操作
常用操作(key的操作)
Boolean result1 = redisTemplate.delete("key");
List<String> list = new ArrayList<>();
list.add("key1");
list.add("key2");
list.add("key3");
Long result2 = redisTemplate.delete(list);
Boolean result3 = redisTemplate.expire("key", 20, TimeUnit.SECONDS);
Long result4 = redisTemplate.getExpire("key");
Boolean result5 = redisTemplate.hasKey("key");
2.3.2 string的操作
string类型相关操作。我们这里只列出使用boundValueOps方法的操作,至于opsForValue方式,自行尝试即可,后文皆是如此。
// 之后都是操作这个strKey对象操作value,就免于再次指定key了,相当于把key封装起来了
BoundValueOperations strKey = redisTemplate.boundValueOps("strKey");
strKey.set("value1");
strKey.set("value1", 300, TimeUnit.SECONDS);
Boolean result1 = strKey.expire(1, TimeUnit.MINUTES);
String result2 = (String) strKey.get();
Boolean result3 = redisTemplate.delete("strKey");
Integer result4 = strKey.append("111");
Long result5 = strKey.size();
这里有一个问题,我们在set这个key为strKey的对象后,去redis客户端看一下,发现它的key是这样的
\xAC\xED\x00\x05t\x00\x06strKey
我们使用get获取value时,却没有问题。
原因:RedisTemplate模板类在操作redis时默认使用JdkSerializationRedisSerializer进行序列化。
解决方案:
在redisTemplate所处类加一个方法,把序列化方式改为stringRedis序列化方式。
@Autowired(required = false)
public void setRedisTemplate(RedisTemplate redisTemplate) {
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(stringSerializer);
this.redisTemplate = redisTemplate;
}
当然最好还是创建一个RedisTempleteUtil类,把上面这个方法放到这个类里面。
@autowired(required = false)作用在方法上,当方法有参数时,如果IOC容器中有方法参数的对象,那么会自动注入并执行方法一次;如果IOC容器中没有方法的参数对象,那么这个方法不会被执行,不管这个方法上有多少个参数,只要有一个参数对象是IOC容器中没有的,这个方法便不会被执行。如果方法没有参数,那么会被执行一次。
2.3.3 list的操作
BoundListOperations listKey = redisTemplate.boundListOps("listKey");
Long result1 = listKey.leftPush("v1");
Long result2 = listKey.rightPush("v2");
Long result3 = listKey.leftPushAll("v3", "v4", "v5");
Long result4 = listKey.rightPushAll("v3", "v4", "v5");
Boolean result5 = listKey.expire(1, TimeUnit.MINUTES);
List result6 = listKey.range(0, -1);
String result7 = (String) listKey.leftPop();
String result8 = (String) listKey.rightPop();
String result9 = (String) listKey.index(3);
Long result10 = listKey.size();
listKey.set(3, "v6");
Long result11 = listKey.remove(4L, "v3");
2.3.4 hash的操作
BoundHashOperations hashKey = redisTemplate.boundHashOps("hashKey");
hashKey.put("field1", "value1");
Boolean result1 = hashKey.expire(1, TimeUnit.MINUTES);
Map<String, String> map = new HashMap<>();
map.put("field2", "value2");
map.put("field3", "value3");
map.put("field4", "value4");
hashKey.putAll(map);
Set keys = hashKey.keys();
List values = hashKey.values();
String result2 = (String) hashKey.get("field3");
Map result3 = hashKey.entries();
Long result4 = hashKey.delete("field3");
Boolean result5 = hashKey.hasKey("field3");
2.3.5 set的操作
BoundSetOperations setKey = redisTemplate.boundSetOps("setKey");
setKey.add("v1", "v2", "v3", "v4");
Boolean result1 = setKey.expire(3, TimeUnit.MINUTES);
Set result2 = setKey.members();
Boolean result3 = setKey.isMember("v4");
Long result4 = setKey.size();
Long result5 = setKey.remove("v3");
Boolean result6 = redisTemplate.delete("setKey");
2.3.6 zset的操作
BoundZSetOperations zsetKey = redisTemplate.boundZSetOps("zsetKey");
Boolean result1 = zsetKey.add("v1", 30);
Boolean result2 = zsetKey.add("v2", 60);
Boolean result3 = zsetKey.add("v3", 50);
Boolean result4 = zsetKey.add("v4", 100);
Set result5 = zsetKey.range(0, -1);
Double result6 = zsetKey.score("v1");
Long result8 = zsetKey.size();
Long result9 = zsetKey.count(40, 80);
Set result10 = zsetKey.rangeByScore(40, 80);
Set result11 = zsetKey.rangeWithScores(0, -1);
Long result12 = zsetKey.rank("v1");
Long result13 = zsetKey.remove("v2");
Long result14 = zsetKey.removeRange(1, 2);
Double result15 = zsetKey.incrementScore("v4", 100);
2.4 搭配MySQL使用
上面都是SpringDataRedis一些基础操作,并没有投入实战,我们这边联合MySQL数据库一起使用,看看redis在实际开发中起到了什么样的作用。
我们做一个简单的例子,建一张表t_goods,写几个接口对其进行增删改查。
2.4.1 准备工作
首先在依赖方面,因为我们要用Java连接MySQL数据库,使用的是阿里巴巴的durid连接池,所以我们加入这两个依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
我们使用mybatis-plus框架操作数据库命令,所以导入这个依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
使用redis、Spring Cache,导入这两个依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置文件方面,首先要指定访问端口,然后指定MySQL和redis的连接。这里要注意,我们要指定连接池为druid连接池。
server:
port: 8081
spring:
redis:
host: 10.40.18.10
port: 6379
database: 0
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://10.40.18.10:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
指定mybatis-plus的映射规则,我们这里主键选用uuid的方式。
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: assign_uuid
这里我们就做4个简单的接口,增加商品、根据id查询商品信息、更改商品信息、删除商品。
2.4.2 查询逻辑
查询商品信息时,我们要求在根据id查询商品名时先从redis中查询,如果没有,再从数据库中查询并更新到redis,设置5分钟有效期。
@Override
public GoodsDTO getGoods(String id) {
log.info("从redis中查询数据");
String key = "goods_" + id.substring(0, 3);
GoodsDTO goodsDTO = (GoodsDTO) redisTemplate.boundValueOps(key).get();
if (goodsDTO != null) {
return goodsDTO;
}
log.info("从mysql中查询数据");
GoodsPO goodsPO = goodsMapper.selectById(id);
goodsDTO = new GoodsDTO();
goodsDTO.setName(goodsPO.getName());
goodsDTO.setStock(goodsPO.getStock());
goodsDTO.setPrice(goodsPO.getPrice());
redisTemplate.boundValueOps(key).set(goodsDTO, 5, TimeUnit.MINUTES);
return goodsDTO;
}
这里我们需要注意一点,就是我们知道string数据类型存储的key和value都是字符串,那怎么用value存储对象呢?我们想到了json的格式,但是RedisTemplate的默认序列化方式为JdkSerializationRedisSerializer,我们需要给他改成Jackson2JsonRedisSerialize,我们把以下代码放到上文创建的RedisTempleteUtil类中。
@Bean
public RedisTemplate<Serializable, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Serializable, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(new LaissezFaireSubTypeValidator(),
ObjectMapper.DefaultTyping.EVERYTHING);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
对象在redis中是这样存储的
"["com.suye.dto.GoodsDTO",{"name":"刀哥的牛至","price":88.8,"stock":60}]"
格式转换一下
["com.suye.dto.GoodsDTO", {
"name": "刀哥的牛至",
"price": 88.8,
"stock": 60
}]
我们发现这就是标准的json格式,这样我们就能顺利地把Java对象存到字符串中了。
2.4.3 修改逻辑
更改商品信息时,需要查看redis有无缓存,若有,更新缓存,重置有效时间。
@Override
public String updateGoods(UpdateGoodsRO updateGoodsRO) {
String key = "goods_" + updateGoodsRO.getId().substring(0, 3);
GoodsPO goodsPO = goodsMapper.selectById(updateGoodsRO.getId());
goodsPO.setName(updateGoodsRO.getName());
goodsPO.setStock(updateGoodsRO.getStock());
goodsPO.setPrice(updateGoodsRO.getPrice());
int result = goodsMapper.updateById(goodsPO);
GoodsDTO goodsDTO = (GoodsDTO) redisTemplate.boundValueOps(key).get();
if (goodsDTO != null) {
goodsDTO.setName(updateGoodsRO.getName());
goodsDTO.setPrice(updateGoodsRO.getPrice());
goodsDTO.setStock(updateGoodsRO.getStock());
redisTemplate.boundValueOps(key).set(goodsDTO, 5, TimeUnit.MINUTES);
}
return result == 1 ? "成功" : "失败";
}
2.4.4 删除逻辑
删除商品信息时,需要删除redis中对应的缓存。
@Override
public String delGoods(String id) {
String key = "goods_" + id.substring(0, 3);
GoodsDTO goodsDTO = (GoodsDTO) redisTemplate.boundValueOps(key).get();
if (goodsDTO != null) {
redisTemplate.delete(key);
}
int result = goodsMapper.deleteById(id);
return result == 1 ? "成功" : "失败";
}
3. 使用Spring Cache
3.1 介绍说明
我们在上面使用redis做缓存时:
- 查询缓存中有无要找的数据,如果有,直接返回结果。
- 如果无,查询数据库,将结果存入缓存,并返回结果。
- 数据更新时,先更新数据库,然后更新缓存或删除缓存。
- 数据删除时,删除对应的缓存。
我们发现一个问题,所有使用到缓存的api都要按这个步骤去书写,这样就有很多逻辑相似的代码。并且我们也需要显式调用redis的api,这样增加了项目和redis的耦合度。
我们想到,我们可以把查询缓存、写入缓存这种操作的代码封装起来,用框架实现,这让我们可以更加专注于处理业务逻辑。进而我们发现,这就是AOP的思想。
Spring Cache就是这样的框架,利用AOP,实现基于注解的缓存功能,并进行合理抽象,让业务代码不用关心底层使用的是什么缓存(redis、mongoDB等),只需要加注解,就可以使用缓存功能了。
3.2 准备工作
导入Spring Cache的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
在yml文件里添加配置
spring:
redis:
host: 10.40.18.10
port: 6379
database: 0
cache:
type: redis
redis:
time-to-live: 3600000
cache-null-values: true
use-key-prefix: true
在主启动类上添加注解,开启缓存功能
@SpringBootApplication
@EnableCaching
public class RedisPracticeApplication {
public static void main(String[] args) {
SpringApplication.run(RedisPracticeApplication.class, args);
}
}
3.3 Spring Cache常用注解
3.3.1 @EnableCaching
放在Springboot的启动类或者Cache配置类上,开启缓存功能,上文有操作演示。
3.3.2 @Cacheable
在查询时,会先从缓存中取,若取不到再发起对数据库的访问,方法返回值加入缓存。
这个注解以及@CacheEvict、@CachePut注解都有两个重要的属性,value和key。value是缓存的名称,比如一张表的增删改查等都可以用一个value。每个value下面可以有多个key,key是每条数据的唯一标识。存到redis里实际的key就是这样的:value::key。
这个key可以通过SpEL内置对象动态获取,可以获取方法返回值、方法参数等。
3.3.3 @CacheEvict
通常配置在删除的方法上,用于从缓存中移除一条或多条对应数据。
3.3.4 @CachePut
通常配置在新增和修改操作上,将方法的返回值放到缓存中。
这里更多还是用在修改操作上,因为新增数据操作一般不需要放数据入缓存,还有就是我们一般都会以唯一主键作为key,而现在的key多数都是id自增或者UUID生成,而不需要前端传入,所以我们无法通过SpEL获取id作为key的值。
3.3.5 @CacheConfig
用于配置该类中会用到的一些共用的缓存配置。
3.3.6 @Caching
配置在方法上,组合多个Cache注解使用。也可以放在定义注解上。
3.3.7 自定义缓存注解
例如这个@Caching注解,如果内部注解较多,会显得比较乱,而且复用性较差。所以我们可以使用自定义注解把这些注解组合到一个注解中,之后在方法上使用这个自定义缓存注解就可以了。
3.4 在项目中使用Spring Cache
我们还是以上文的增删改查接口上进行Spring Cache的添加。
3.4.1 查询
@Override
@Cacheable(value = "goods", key = "#id.substring(0, 3)")
public GoodsDTO getGoods(String id) {
log.info("从mysql中查询数据");
GoodsPO goodsPO = goodsMapper.selectById(id);
GoodsDTO goodsDTO = new GoodsDTO();
goodsDTO.setName(goodsPO.getName());
goodsDTO.setStock(goodsPO.getStock());
goodsDTO.setPrice(goodsPO.getPrice());
return goodsDTO;
}
这里需要注意一个问题,我们需要给goodsDTO类实现序列化接口,否则会导致序列化错误。这里存入redis数据库的value就不是json格式,而是我们看不懂的格式了。
3.4.2 更改
@Override
@CachePut(value ="goods", key = "#updateGoodsRO.id.substring(0, 3)")
public String updateGoods(UpdateGoodsRO updateGoodsRO) {
GoodsPO goodsPO = goodsMapper.selectById(updateGoodsRO.getId());
goodsPO.setName(updateGoodsRO.getName());
goodsPO.setStock(updateGoodsRO.getStock());
goodsPO.setPrice(updateGoodsRO.getPrice());
int result = goodsMapper.updateById(goodsPO);
return result == 1 ? "成功" : "失败";
}
这里逻辑发生了改动,上面我们需求是如果有缓存,则更新缓存,但是这里无论有无缓存,我们都更新了缓存。这也体现了Spring Cache没有SpringDataRedis灵活的特点。
3.4.3 删除
@Override
@CacheEvict(value = "goods", key = "#id.substring(0, 3)")
public String delGoods(String id) {
int result = goodsMapper.deleteById(id);
return result == 1 ? "成功" : "失败";
}