Spring Boot集成redis(redsiTemplate,缓存注解,事务,流水线)
关于redis
redis是一个高性能的NoSQL的key-value内存数据库,主要被用作缓存,它的value支持5种数据结构如String(不仅是字符串,也可以浮点数或者整数,此时可以自增自减)、List(字符串链表,可以从两端push或pop元素,也可以根据偏移量进行裁剪)、Set(包含字符串的无序容器,包含的每个字符串都是独一无二的)、ZSet(是字符串成员和浮点数分值的有序映射,可以根据分支范围或成员获取元素,也可以排序)、Hash(包含字符串键值对的无序散列表)。redis的键是有过期时间的,也就是会在一定时间后这个键和值会被删除,单位是秒,为-1永不过期。虽然redis整个数据库都加载在内存中进行操作,但是也会按照一定规则将数据持久化到磁盘。redis持久化有两种方式,一种是快照方式,将存在于某一时刻的所有数据写入硬盘。一种是只追加文件方式,将被执行的写命令写入硬盘。
Spring Boot对于redis集成有两种使用方式:一种是手动操作RedisTemplate,另一种是使用redis缓存注解。
由于redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响服务器重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久,因为save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务 。
相关的依赖
<!--SpringBoot的Redis支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--SpringBoot缓存支持,使用缓存注解要用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
相关的配置
spring:
redis:
host: 192.168.247.134
port: 6379
#lettuce
lettuce:
pool:
#最大连接数
max-active: 8
#最大等待数
max-wait: 1000
#最大空闲数
max-idle: 8
cache:
#缓存管理器的类型
type: redis
redis:
#超时时间(ms)
time-to-live: 3600000
#禁用默认前缀
use-key-prefix: false
使用RedisTemplate操作redis
- 使用StringRedisTemplate操作redis
spring-boot-starter-data-redis提供了RedisTemplate类,这个类封装了对Redis基本数据结构的常用操作,其子类StringRedisTemplate则提供了对字符串的常用操作
//注入StringRedisTemplate
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* StringRedisTemplate操作字符串
*/
public void operateString(){
//设置元素
stringRedisTemplate.opsForValue().set("k1","v1");
//获取元素
String value = stringRedisTemplate.opsForValue().get("k1");
//在指定key的value后追加字符串
stringRedisTemplate.opsForValue().append("key1"," is a key");
}
/**
* StringRedisTemplate操作List
*/
public void operateList(){
String key = "newList";
//单元素左压入栈
stringRedisTemplate.opsForList().leftPush(key,"value1");
//多元素右压入栈
stringRedisTemplate.opsForList().rightPushAll(key,"value2","value3");
//获取指定index的元素
String str = stringRedisTemplate.opsForList().index(key,2);
//获取指定index的元素组成的List
List<String> stringList = listOperations.range(key,1,2);
//设置指定index的value
stringRedisTemplate.opsForList().set(key,1,"value1");
//删除某个值,从left开始删除,指定删除个数
stringRedisTemplate.opsForList().remove(key,2,"value1");
//从list的left侧弹出元素
stringRedisTemplate.opsForList().leftPop(key);
}
- 使用RedisTemplate操作redis
jedis和lettuce都是redis的客户端,现在Spring Boot 2.x默认使用lettuce作为redis的客户端,jedis在实现上是直连redis-server,在多线程间共享一个jedis实例是线程不安全的,因此想要在多线程场景中使用jedis,需要使用连接池来管理连接,而不是每次生成新的连接。lettuce则在多线程共享一个连接实例的场景下是线程安全的,并且支持redis的高级功能如流水线、集群等。
如果redis操作的元素是对象,应该使用的是RedisTemplate,使用时,要操作的对象元素必须实现Serializable接口,对象的属性如果是自定义类型也必须实现Serializable接口。
RedisTemplate默认使用的是JdkSerializationRedisSerializer,如果用这个序列化器保存对象,在redis客户端中看到的是乱码,实际上能正常反序列化对象。使用Jackson2JsonRedisSerializer可以使得redis保存的值更为直观,但是会包含对象的类型信息,类型信息在反序列化时有作用。另外,保存字符串建议使用StringRedisSerializer,保存对象建议使用JdkSerializationRedisSerializer或者Jackson2JsonRedisSerializer,否则会出现各种意外。以下是经Jackson2JsonRedisSerializer序列化的信息例子:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-No1TihUK-1580794604756)(1569553851973.png)]
/**
* Redis缓存配置类
*/
@Configuration
public class RedisCacheConfig {
//IP
@Value("${spring.redis.host}")
private String redisHost;
//端口
@Value("${spring.redis.port}")
private int port;
/**
* Redis连接工厂
* @return
*/
@Bean
public RedisConnectionFactory redisConnectionFactory(){
//配置Lettuce客户端的主机IP和端口
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisHost,port);
//配置redis密码(此处忽略)
return lettuceConnectionFactory;
}
/**
* 配置 RdisTemplate
* @return
*/
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
//这里的参数redisConnectionFactory会在redisTemplate注入的时候同样以redisTemplate注入的方式注入
RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
//设置redisTemplate的连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置Jackson2JsonRedisSerializer
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
//设置可见性(属性访问器,属性可见性)
objectMapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
//设置允许自动包含的类型(NON_FINAL)
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
//设置对象映射器
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//设置template的键序列化器(String,List,Set...,非Hash)
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置template的值序列化器(String,List,Set...,非Hash)
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//设置template的Hash键序列化器
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
//设置template的Hash值序列化器
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory){
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
return stringRedisTemplate;
}
}
/**
* 测试操作Hash,List,Set
*/
@RestController
@RequestMapping("/seller")
public class SellerUserController{
//优先按Bean名称注入
@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/testRedis")
public void testRedis(){
/*测试对象*/
ProductCategory productCategory1 = new ProductCategory();
productCategory1.setCategoryId(1);
productCategory1.setCategoryName("Tom likes");
productCategory1.setCategoryType(99);
productCategory1.setCreateTime(new Date());
ProductCategory productCategory2 = new ProductCategory();
productCategory2.setCategoryId(2);
productCategory2.setCategoryName("Michael doesn't like");
productCategory2.setCategoryType(101);
productCategory2.setUpdateTime(new Date());
/* 测试RedisTemplate操作hash*/
redisTemplate.opsForHash().put("hash1","hashKey1","hashVal1");
Object testHashObj = redisTemplate.opsForHash().get("hash1","hashKey1");
Map<String,ProductCategory> testHashMap = new HashMap<>();
testHashMap.put("hashKey1",productCategory1);
testHashMap.put("hashKey2",productCategory2);
redisTemplate.opsForHash().putAll("hash1",testHashMap);
Map<Object,Object> readMap1 = redisTemplate.opsForHash().entries("hash1");
Map<String,ProductCategory> readMap2 = (Map<String,ProductCategory>) redisTemplate.opsForHash().entries("hash1");
/*测试RedisTemplate操作list*/
redisTemplate.opsForList().leftPushAll("list1","listVal1","listVal2");
Object testListObj = redisTemplate.opsForList().index("list1",0);
List<ProductCategory> testList = new ArrayList<>();
testList.add(productCategory1);
testList.add(productCategory2);
redisTemplate.opsForList().leftPushAll(testList);
/*测试RedisTemplate操作Set*/
redisTemplate.opsForSet().add("set1","setVal1","setVal2");
//设置过期时间
redisTemplate.expire("set1",RedisConstant.EXPIRE,TimeUnit.SECONDS);
Set<Object> testSetObj = redisTemplate.opsForSet().members("set1");
}
}
/**
* 操作Hash(只是示例,与上面配置可能不相关)
*/
@Resource
private RedisTemplate<String,Map<String,Employee>> employeeRedisTemplate;
public void operateHash(){
String key = "newHash";
//为指定hash添加field为field1,value为value1的hash元素
employeeRedisTemplate.opsForHash().put(key,"field1",new Employee());
//以map的形式为指定的hash添加元素
Map<String,Employee> newMap = new HashMap();
newMap.put("field2",new Employee());
newMap.put("field3",new Employee());
employeeRedisTemplate.opsForHash().putAll(key,newMap);
//为指定hash获取指定field的元素
Object emplyee = employeeRedisTemplate.opsForHash().get(key,"field1");
//获取指定hash的所有key的Set
Set<Object> keySet = employeeRedisTemplate.opsForHash().keys(key);
//获取指定hash的所有value的List
List<Object> valueList = employeeRedisTemplate.opsForHash().values(key);
//获取指定的hash的所有key-value对
Map<Object,Object> kvMap = employeeRedisTemplate.opsForHash().entries(key);
//删除指定hash的指定field的键值对,可传入多个field参数
employeeRedisTemplate.opsForHash().delete(key,"field1","field2");
}
RedisTemplate操作Redis事务
- 要理解redis的事务,首先要知道redis服务器采用单线程单进程处理客户端请求,它将所有客户端的命令存储在一个缓存区中,然后一个一个执行。redis事务的作用就是保证事务中所有命令连续执行,不被其他客户端打断。在redis中,执行事务先要执行multi命令,然后输入需要在事务中执行的命令,最后再执行exec命令。当redis从一个客户端接收到multi命令时,redis会将这个客户端之后发送的所有命令存储在队列中,直到从这个客户端接收到exec命令为止。接着redis会在不被其他客户端打断的情况下,一个一个执行存储在队列的命令。在Python客户端,redis事务底层包含了流水线,也就是Python客户端会存储事务包含的所有命令,在事务执行时一次发送所有命令给redis,然后等待所有返回。这种流水线的方式减少了客户端和redis服务器之间的网络通信次数来提升redis执行多个命令的性能。在关系数据库中,为了保持数据一致性,在用户向数据库服务器发送begin后,数据库会对被访问的数据进行加锁(悲观锁机制),直到事务被提交或者回滚。假如有其他客户端试图对加索的数据行进行修改,那么这个客户端将被阻塞,所以这种加锁的缺点是假如持有锁的客户端运行越慢,等待解锁的客户端被阻塞的时间越长。redis的watch命令(watch命令先发送给redis服务器,再发送multi命令等事务相关命令)并不会对数据加锁,而是监视数据是否发生了变化(乐观锁机制),假如数据发生了变化,redis事务将返回失败。这种乐观锁的优势在于不需要客户端等待持有锁的客户端,当事务失败时进行重试就行了。当事务中的命令执行失败时,其后面的命令仍将继续执行。此外,除了exec用于执行事务,可以用discard来放弃watch并清空所有进入事务队列的命令,有点类似关系数据库的回滚操作。
- Redis事务的执行过程:watch命令监控Redis客户端命令的键值对(相当于乐观锁机制的版本号)-> multi命令开始事务->Redis客户端命令进入队列 -> exec命令执行事务 : 判断watch命令是否检测到键值对变化(即使是修改为与原来相同的值也视为变化)-> 取消监控的键值对
- redisTemplate不保证在同一个连接中执行同一个事务中的multi,exec,discard操作,spring-data-redis提供一种方式可以保证同一个事务中的多个操作在同一个连接中执行,就是使用了SessionCallback(或者开启redisTemplate的enableTransactionSupport),在方式2中如果是查询值则在redisOperations.exec()命令执行后将查询结果集返回给redisTemplate
- 除了SessionCallback其实还有RedisCallback,但是SessionCallback提供了良好的封装,RedisCallback操作较复杂。
/*测试使用Redis的事务机制:方式1*/
@RequestMapping("/testTrans1")
@ResponseBody
public void testTrans1(){
/*测试redis事务*/
try{
//开启事务支持
redisTemplate.setEnableTransactionSupport(true);
//监视值
redisTemplate.watch("testTrans1");
//开始事务
redisTemplate.multi();
//开始操作
redisTemplate.opsForValue().set("testTrans1","test888");
//测试值
Object object = redisTemplate.opsForValue().get("testTrans1");
//执行事务
redisTemplate.exec();
}catch (Exception e){
throw e;
}
}
/*测试使用Redis的事务机制:方式2*/
@RequestMapping("/testTrans2")
@ResponseBody
public void testTrans2(){
List<Object> redisResult = (List<Object>) redisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations redisOperations) throws DataAccessException {
//监控V_key1
redisOperations.watch("V_key1");
//开始事务
redisOperations.multi();
//操作
redisOperations.opsForValue().set("K_" + i, "val_" + i);
//执行事务
return redisOperations.exec();
}
});
}
RedisTemplate操作Redis流水线(管道)
- executePipelined()中的SessionCallback必须返回null,流水线才能返回查询结果集。流水线允许客户端一次批量提交redis命令给服务器的,而服务器将多个命令请求的结果在一次命令回复中返回给客户端。因此提高了效率。
/*测试redis流水线*/
public void testPipeline(){
List<Object> pipeLineResult = (List<Object>) redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
//set
//for(int i=0;i<100;i++){
//1. Value
//redisOperations.opsForValue().set("K_"+ i ,productCategory1);
//2. List
//redisOperations.opsForList().rightPush("L_","val_" + i);
//3. Hash
//redisOperations.opsForHash().put("H_","hKey_" + i,"hVal_"+ i);
//}
//get
//1. Value
redisOperations.opsForValue().get("K_52");
//2. List
redisOperations.opsForList().range("L_",1,99);
//3. Hash
redisOperations.opsForHash().entries("H_");
return null;
}
});
}
使用缓存注解操作redis
Spring对原生的Java Caching进行了缓存抽象,使用缓存抽象后可以直接操作CacheManager管理Cache,在配置文件中配置使用redis缓存管理器,由于不能再注解中配置缓存超时时间,所以在配置文件中配置超时时间,缓存管理器默认使用带前缀的缓存key名称,可以把这个设置禁止掉,这些配置也可以不用配,有默认值
cache:
#缓存管理器的类型,默认redis
type: redis
redis:
#超时时间(ms),默认-1永不超时
time-to-live: 3600000
#禁用默认前缀,默认启用
use-key-prefix: false
要使用缓存注解还需要在主应用程序启用缓存注解
@EnableCaching
@SpringBootApplication
public class Demo20190808Application {
public static void main(String[] args) {
SpringApplication.run(Demo20190808Application.class, args);
}
}
-
@Cacheable,注解在方法上,表示能够根据参数对方法结果进行缓存,注解value参数必须指定,表示缓存名称,key表示缓存entry的key,conditon表示条件,符合spEL表达式,为true才缓存,unless表示否定条件,为true不缓存,该注解常用在查询方法中。@Cacheable的运行时机有两个,一是方法运行之前,一是方法执行之后,因此不能使用#result
-
@CachePut,每次调用方法都会调用该注解,保证方法调用的同时,希望结果能被缓存,也有value,key,condition等注解参数,常用在新增、更新方法中。只有当@Cacheable和@CachePut指向同一Cache中同一key,展示数据才能正常更新,只在方法执行之后才执行。
-
@CacheEvict,清空缓存,注解参数beforeInvocation指定是否在方法执行之前就清空,默认为false,即在方法执行之后执行清空,假如方法在返回之前发生异常则不会清空。allEntries指定是否清空指定名称的Cache的所有的entry,默认为false。也有value,key,condition等注解参数,常用在删除方法中
-
@CacheConfig,注解在类中,为本类的缓存注解统一配置,比如配置cacheNames
-
@Caching,复合缓存注解,示例
@Caching(cacheable = {
@Cacheable(value = "emp",key = "#p0")
},
put = {
@CachePut(value = "emp",key = "#p0")
},evict = {
@CacheEvict(value = "emp",key = "#p0")
})
使用keyGenerator生成key
@Bean
public KeyGenerator keyGenerator(){
return new KeyGenerator() {
@Override
public Object generate(Object o, Method method, Object... params) {
return "["+Arrays.asList(params).get(0).toString()+"]" ;
}
};
}
缓存注解使用示例代码
@CacheConfig(cacheNames = "userCache")
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired(required = false)
private UserDao userDao;
@PostMapping("insert")
public User insert(@RequestBody User user){
String key = KeyUtil.genUniqueKey();
user.setUserId(key);
User result = userDao.insert(user);
return result;
}
@Cacheable(keyGenerator = "keyGenerator", condition = "#id != null")
@GetMapping("findById")
public User findById(@RequestParam String id){
User user = userDao.findById(id);
return user;
}
@CachePut(key = "'['+#result.getUserId()+']'", condition = "#result.getUserId() != null")
@PutMapping("update")
public User update(@RequestBody User user){
Query query = new Query();
query.addCriteria(Criteria.where("_id").is(user.getUserId()));
Update update = new Update();
update.set("location",user.getLocation());
update.set("userName",user.getUserName());
update.set("age",user.getAge());
update.set("birth",user.getBirth());
userDao.updateFirst(query,update);
User result = userDao.findById(user.getUserId());
return result;
}
@CacheEvict(key = "'['+#id+']'",beforeInvocation = true)
@DeleteMapping("delete")
public void delete(@RequestParam String id){
Query query = new Query();
query.addCriteria(Criteria.where("_id").is(id));
userDao.remove(query);
}
}
spEL常用上下文:
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root对象 | 当前被调用的方法名 | #root.methodName |
target | root对象 | 当前被调用的目标对象 | #root.target |
ArgumentName | 执行上下文 | 被调用方法的参数 | #user.id |
result | 执行上下文 | 方法执行后的返回值 | #result,@CachePut和@CacheEvit都可以用 |
redis分布式锁
略。另外写了一篇文章解释这个东西。
redis发布订阅功能
略。一般使用流行的消息中间件实现发布订阅功能。