本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Redis 是一种运行在内存的数据库,支持 7 种数据类型的存储。Redis 是一个开源、使用 ANSIC 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、键值数据库,并提供多种语言的 API。Redis 是基于内存的,所以运行速度很快,大约是关系数据库几倍到几十倍的速度。在测试中,Redis 可以在 1s 内完成 10 万次的读写,性能十分高效。如果我们将常用的数据存储在 Redis 中,用来代替关系数据库的查询访问,网站性能将可以得到大幅提高。
RedisTemplate
创建 RedisTemplate
@Bean(name = redisTemplate)
public RedisTemplate<Object, Object> initRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
// RedisTemplate 会自动初始化 StringRedisSerializer,所以这里直接获取
RedisSerializer stringRedisSerializer = redisTemplate.getStringSerializer();
// 设置字符串序列化器,这样 Spring 就会把 Redis 的 key 当作字符串处理了
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(stringRedisSerializer);
redisTemplate.setConnectionFactory(initConnectionFactory());
return redisTemplate;
}
使用 SessionCallBack 接口
// 让 RedisTemplate 回调,在同一连接下执行多个 Redis 命令
public void useSessionCallback(RedisTemplate redisTemplate) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperation ro) throws DataAccessException {
ro.opsForValue().set("key1", "value1");
ro.opsForHash().put("hash", "field", "hvalue");
return null;
}
});
}
// 也可以使用 Lambda 表达式改写代码
public void useSessionCallback(RedisTemplate redisTemplate) {
redisTemplate.execute((RedisOperation ro) -> {
ro.opsForValue().set("key1", "value1");
ro.opsForHash().put("hash", "field", "hvalue");
return null;
});
}
在 Spring Boot 中配置和使用 Redis
配置文件 application.yml
spring:
redis:
# 配置连接池属性
jedis:
pool:
min-idle: 5
max-active: 10
max-idle: 10
max-wait: 2000
# 配置 Redis 服务器属性
port: 6379
host: 192.168.11.131
password: 123456
# Redis 连接超时时间,单位主运秒
timeout: 1000
RedisTemplate 会默认使用 JdkSerializationRedisSerializer 进行序列化键值,这样便能够存储到 Redis 服务器中。Redis 服务器存入的便是一个经过序列化后的特殊字符串,有时候对于跟踪并不是很友好 。如果我们在 Redis 只是使用字符串 ,那么使用其自动生成的 StringRedisTemplate 即可,但是这样就只能支持字符串了,并不能支持 Java 对象的存储。为了克服这个问题,可以通过设置 RedisTemplate 的序列化器来处理。
自定义序列化(推荐使用 StringRedisTemplate)
为 Redis 客户端查看操作数据, redisTemplate 需要进行序列化设置, 默认配置的 jdk 序列化会导致在客户端查看不了数据(仍可使用内在函数存取修改, 只是查看不了), 为避免这种情况发生, 使用 StringRedisTemplate 或自行配置序列化, 自行配置可参考如下代码:
/**** imports ****/
@Configuration
public class MyRedisConfig {
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
////参照StringRedisTemplate内部实现指定序列化器
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(keySerializer());
redisTemplate.setHashKeySerializer(keySerializer());
redisTemplate.setValueSerializer(valueSerializer());
redisTemplate.setHashValueSerializer(valueSerializer());
return redisTemplate;
}
private RedisSerializer<String> keySerializer(){
return new StringRedisSerializer();
}
//使用Jackson序列化器
private RedisSerializer<Object> valueSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
简单测试代码 test:
@RunWith(SpringRunner.class)
@SpringBootTest
public class MyConfigRedisTemplateTest {
@Autowired
// 在 MyRedisConfig 文件中配置了 redisTemplate 的序列化之后,客户端也能正确显示键值对了
private RedisTemplate redisTemplate;
@Test
public void test(){
redisTemplate.opsForValue().set("wujinxing", "lige");
System.out.println(redisTemplate.opsForValue().get("wujinxing"));
Map<String, Object> map = new HashMap<>();
for (int i=0; i<10; i++){
User user = new User();
user.setId(i);
user.setName(String.format("测试%d", i));
user.setAge(i+10);
map.put(String.valueOf(i),user);
}
redisTemplate.opsForHash().putAll("测试", map);
BoundHashOperations hashOps = redisTemplate.boundHashOps("测试");
Map map1 = hashOps.entries();
System.out.println(map1);
}
static class User implements Serializable {
private int id;
private String name;
private long age;
// 省略getter, setter, toString...
}
}
操作 Redis 数据类型(String, hash, set 等)
常见操作(均在 Controller 上使用, 仅做测试, 实际项目应在 Service 层使用 Redis):
/**** imports ****/
@Controller
@RequestMapping("/redis")
public class RedisController {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisController.class);
@Autowired
private RedisTemplate redisTemplate = null ;
@Autowired
private StringRedisTemplate stringRedisTemplate = null ;
@RequestMapping("/stringAndHash")
@ResponseBody
public Map<String, Object> testStringAndHash() {
redisTemplate.opsForValue().set("key1", "value1");
// 注意这里使用了 JDK 的序列化器,所以 Redis 保存时不是整数,不能运算
redisTemplate.opsForValue().set("int_key", "1");
stringRedisTemplate.opsForValue().set("int", "1");
// 使用运算
stringRedisTemplate.opsForValue().increment("int", 1);
// 获取底层 Jedis 连接
Jedis jedis = (Jedis) stringRedisTemplate.getConnectionFactory().getConnection().getNativeConnection();
// 减1操作,这个命令 RedisTemplate 不支持,所以我先获取底层的连接再操作
jedis.decr("int");
Map<String, String> hash = new HashMap<String, String>();
hash.put("field1", "value1");
hash.put("field2", "value2");
// 存入一个散列数据类型
stringRedisTemplate.opsForHash().putAll("hash", hash);
// 新增一个字段
stringRedisTemplate.opsForHash().put("hash", "field3", "value3");
// 绑定散列操作的 key , 这样可 以连续对同一个散列数据类型进行操作
BoundHashOperations hashOps = stringRedisTemplate.boundHashOps("hash");
// 删除两个字段
hashOps.delete("field1", "field2");
// 新增一个字段
hashOps.put("filed4", "value5");
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
@RequestMapping("/list")
@ResponseBody
public Map<String, Object> testList(){
//链表从左到右的顺序为v10, v8, v6, v4, v2
stringRedisTemplate.opsForList().leftPushAll("list1", "v2","v4","v6","v8","v10");
//链表从左到右的顺序为v1, v3, v5, v7, v9
stringRedisTemplate.opsForList().rightPushAll("list2", "v1","v3","v5","v7","v9");
//绑定list2操作链表
BoundListOperations listOps = stringRedisTemplate.boundListOps("list2");
Object result1 = listOps.rightPop();//从右边弹出一个成员
LOGGER.info("list2的最右边元素为: "+result1.toString());
Object result2 = listOps.index(1); //获取定位元素, 下标从0开始
LOGGER.info("list2下标为1的元素为"+result2.toString());
listOps.leftPush("v0"); //从左边插入链表
Long size = listOps.size();//求链表长
LOGGER.info("list2的长度为: "+size);
List element = listOps.range(0, size-2); //求链表区间成员
LOGGER.info("list2从0到size-2的元素依次为: "+element.toString());
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
@RequestMapping("/set")
@ResponseBody
public Map<String, Object> testSet(){
//重复的元素不会被插入
stringRedisTemplate.opsForSet().add("set1", "v1","v1","v3","v5","v7","v9");
stringRedisTemplate.opsForSet().add("set2", "v2","v4","v6","v5","v10","v10");
//绑定sert1集合操作
BoundSetOperations setOps = stringRedisTemplate.boundSetOps("set1");
setOps.add("v11", "v13");
setOps.remove("v1", "v3");
Set set = setOps.members();//返回所有元素
LOGGER.info("集合中所有元素: "+set.toString());
Long size = setOps.size();//求成员数
LOGGER.info("集合长度: "+String.valueOf(size));
Set inner = setOps.intersect("set2"); //求交集
setOps.intersectAndStore("set2", "set1_set2");//求交集并用新的集合保存
LOGGER.info("集合的交集: "+inner.toString());
Set diff = setOps.diff("set2"); //求差集
setOps.diffAndStore("set2","set1-set2"); //求差集并用新的集合保存
LOGGER.info("集合的差集: "+diff.toString());
Set union = setOps.union("set2"); //求并集
setOps.unionAndStore("set2", "set1=set2"); //求并集并用新的集合保存
LOGGER.info("集合的并集: "+union.toString());
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
/**
* redis操作有序集合
* @return
*/
@RequestMapping("/zset")
@ResponseBody
public Map<String, Object> testZSet(){
Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
for(int i=1; i<=9; i++){
//分数
double score = i*0.1;
//创建一个TypedTuple对象, 存入值和分数
ZSetOperations.TypedTuple typedTuple = new DefaultTypedTuple<String>("value" + i, score);
typedTupleSet.add(typedTuple);
}
LOGGER.info("新建的set: "+typedTupleSet.toString());
//往有序集合插入元素
stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet);
//绑定zset1有序集合操作
BoundZSetOperations<String, String> zSetOps = stringRedisTemplate.boundZSetOps("zset1");
zSetOps.add("value10", 0.26);
Set<String> setRange = zSetOps.range(1,6);
LOGGER.info("下标下1-6的set: " + setRange.toString());
//按分数排序获取有序集合
Set<String> setScore = zSetOps.rangeByScore(0.2, 0.6);
LOGGER.info("按分数排序获取有序集合: "+ setScore.toString());
//定义值范围
RedisZSetCommands.Range range = new RedisZSetCommands.Range();
range.gt("value3"); //大于value3
//range.gte("value3"); //大于等于value3
//range.lt("value8"); //小于value8
range.lte("value8"); //小于等于value8
//按值排序, 注意这个排序是按字符串排序
Set<String> setLex = zSetOps.rangeByLex(range);
LOGGER.info("按值排序: "+setLex.toString());
zSetOps.remove("value9", "value2"); //删除元素
Double score = zSetOps.score("value8"); //求分数
LOGGER.info("求value8的分数: "+score);
//在下标区间 按分数排序, 同时返回value和score
Set<ZSetOperations.TypedTuple<String>> rangeSet = zSetOps.rangeWithScores(1,6);
LOGGER.info("在下标区间 按分数排序, 同时返回value和score: "+rangeSet.toString());
//在下标区间 按分数排序, 同时返回value和score
Set<ZSetOperations.TypedTuple<String>> scoreSet = zSetOps.rangeByScoreWithScores(1,6);
LOGGER.info("在下标区间 按分数排序, 同时返回value和score: "+scoreSet.toString());
//按从大到小排序
Set<String> reverseSet = zSetOps.reverseRange(2, 8);
LOGGER.info("按从大到小排序: "+reverseSet.toString());
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
@RequestMapping("/multi")
@ResponseBody
public Map<String, Object> testMulti(){
stringRedisTemplate.opsForValue().set("key1", "value1");
/*List list = (List) stringRedisTemplate.execute((RedisOperations operations)->{
operations.watch("key1");
operations.multi();
operations.opsForValue().set("key2", "value2");
//operations.opsForValue().increment("key1", 1);
//获取的值将为null, 因为redis知识把命令放入队列
Object value2 = operations.opsForValue().get("key2");
System.out.println("命令在队列, 所以value2为null [ " + value2 + " ] ");
operations.opsForValue().set("key3", "value3");
Object value3 = operations.opsForValue().get("key3");
System.out.println("命令在队列, 所以value3为null [ " + value3 + " ] ");
//执行exce()命令,将先判断key1是否在监控后被修改过, 如果是则不执行事务, 否则就执行事务
return operations.exec();
});
System.out.println(list);*/
Map<String, Object> map = new HashMap<>();
map.put("success", true);
return map;
}
}
Service 层使用 Redis
@Service
public class CityServiceImpl implements CityService {
private static final Logger LOGGER = LoggerFactory.getLogger(CityServiceImpl.class);
@Autowired
private CityMapper cityMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 获取城市逻辑:
* 如果缓存存在,从缓存中获取城市信息
* 如果缓存不存在,从 DB 中获取城市信息,然后插入缓存
*/
@Override
public City findCityById(Long id){
//从缓存中获取城市信息
String key = "city_"+id;
ValueOperations<String,City> operations = redisTemplate.opsForValue();
//缓存存在
boolean hasKey = redisTemplate.hasKey(key);
if(hasKey){
City city = operations.get(key);
LOGGER.info("CityServiceImpl.findCityById() : 从缓存中获取了城市 >> " + city.toString());
return city;
}
//从DB中获取城市
City city = cityMapper.findById(id);
//插入缓存
operations.set(key,city,10,TimeUnit.SECONDS); //缓存的时间仅有十秒钟
LOGGER.info("CityServiceImpl.findCityById() : 城市插入缓存 >> " + city.toString());
LOGGER.info("刚才加入redis的数据是: "+operations.get(key));
return city;
}
@Override
public Long saveCity(City city) {
return cityMapper.saveCity(city);
}
/**
* 更新城市逻辑:
* 如果缓存存在,删除
* 如果缓存不存在,不操作
*/
@Override
public Long updateCity(City city) {
Long ret = cityMapper.updateCity(city);
//缓存存在,删除缓存
String key = "city_" + city.getId();
boolean hasKey = redisTemplate.hasKey(key);
if (hasKey){
redisTemplate.delete(key);
LOGGER.info("CityServiceImpl.updateCity() : 从缓存中删除城市 >> " + city.toString());
}
return ret;
}
@Override
public Long deleteCity(Long id) {
Long ret = cityMapper.deleteCity(id);
String key = "city_" + id;
boolean hasKey = redisTemplate.hasKey(key);
if(hasKey){
redisTemplate.delete(key);
LOGGER.info("CityServiceImpl.deleteCity() : 从缓存中删除城市 ID >> " + id);
}
return ret;
}
}