Redis 概念
REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统。
Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
它通常被称为数据结构服务器,因为值(value)可以是 字符串(String) , 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。
Redis 特点
Redis 与其他 key - value 缓存产品有以下三个特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
Redis 优势
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
Redis 的安装
1 Redis Linux 下安装 戳这里
2 Redis mac 安装和启动测试
1. 官网http://redis.io/ 下载最新的稳定版本,这里是4.0.9
2. sudo mv 到 /usr/local/
-
- sudo tar -zxf redis-4.0.9.tar 解压文件
4. 进入解压后的目录 cd redis-4.0.9
5. sudo make test 测试编译
6. sudo make install
-
7. 进入src 目录启动redis:./redis-server ../redis.conf
-
8 这了需要新开一个窗口对redis 进行操作:
进入src 目录运行redis-cli:
cd /usr/local/redis-4.0.9/src/
./redis-cli
mac 下安装也可以使用 homebrew,homebrew 是 mac 的包管理器。
1、执行 brew install redis
2、启动 redis,可以使用后台服务启动 brew services start redis。或者直接启动:redis-server /usr/local/etc/redis.conf
Redis 配置
Redis 的配置文件位于 Redis 安装目录下,文件名为 redis.conf。
你可以通过 CONFIG 命令查看或设置配置项。
Redis 配置
Redis 数据类型
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
string(字符串)
string是redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。
string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
string类型是Redis最基本的数据类型,一个键最大能存储512MB。
实例
OK
redis 127.0.0.1:6379> GET name
"runoob"
在以上实例中我们使用了 Redis 的 SET 和 GET 命令。键为 name,对应的值为 runoob。
注意:一个键最大能存储512MB。
hash(哈希)
Redis hash 是一个键值(key=>value)对集合。
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。
实例
redis> HMSET myhash field1 "Hello" field2 "World"
"OK"
redis> HGET myhash field1
"Hello"
redis> HGET myhash field2
"World"
以上实例中 hash 数据类型存储了包含用户脚本信息的用户对象。 实例中我们使用了 Redis HMSET, HGETALL 命令,user:1 为键值。
每个 hash 可以存储 2^32 -1 键值对(40多亿)。
list(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
实例
redis 127.0.0.1:6379> lpush runoob redis
(integer) 1
redis 127.0.0.1:6379> lpush runoob mongodb
(integer) 2
redis 127.0.0.1:6379> lpush runoob rabitmq
(integer) 3
redis 127.0.0.1:6379> lrange runoob 0 10
1) "rabitmq"
2) "mongodb"
3) "redis"
redis 127.0.0.1:6379>
列表最多可存储 2^32 - 1 元素 (4294967295, 每个列表可存储40多亿)。
set(集合)
Redis的Set是string类型的无序集合。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
sadd 命令
添加一个 string 元素到 key 对应的 set 集合中,成功返回1,如果元素已经在集合中返回 0,如果 key 对应的 set 不存在则返回错误。
sadd key member
实例
redis 127.0.0.1:6379> sadd runoob redis
(integer) 1
redis 127.0.0.1:6379> sadd runoob mongodb
(integer) 1
redis 127.0.0.1:6379> sadd runoob rabitmq
(integer) 1
redis 127.0.0.1:6379> sadd runoob rabitmq
(integer) 0
redis 127.0.0.1:6379> smembers runoob
1) "redis"
2) "rabitmq"
3) "mongodb"
注意:以上实例中 rabitmq 添加了两次,但根据集合内元素的唯一性,第二次插入的元素将被忽略。
集合中最大的成员数为 2^32 - 1(4294967295, 每个集合可存储40多亿个成员)。
zset(sorted set:有序集合)
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
zadd 命令
添加元素到集合,元素在集合中存在则更新对应score
zadd key score member
实例
redis 127.0.0.1:6379> zadd runoob 0 redis
(integer) 1
redis 127.0.0.1:6379> zadd runoob 0 mongodb
(integer) 1
redis 127.0.0.1:6379> zadd runoob 0 rabitmq
(integer) 1
redis 127.0.0.1:6379> zadd runoob 0 rabitmq
(integer) 0
redis 127.0.0.1:6379> > ZRANGEBYSCORE runoob 0 1000
1) "mongodb"
2) "rabitmq"
3) "redis"
Redis 在Spring boot 中的使用
实际的业务中随着业务量的增大,访问量也是慢慢跟着变大,尤其是到达一定的临界点的时候服务器将会不堪其重造成一种假死的状态给用户以“崩溃”感觉,严重的时候甚至可能直接宕机,而我们传统的做法就是:1.应用程序方面采用多节点的负载均衡;2.数据库方面采用主从读写分离、分库、分表等,而现在呢又多了一种方式采用nosql产品来做cache层来分离高并发的压力,也确实起到了很好的效果,市场上已经有很多nosql产品,我们这里就使用redis 来解决这个问题。
1 引入 spring-boot-starter-redis
org.springframework.boot
spring-boot-starter-redis
2 添加配置文件application.properties
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=192.168.0.58
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0
3 添加cache的配置类
@Configuration
@EnableCaching public class RedisConfig extends CachingConfigurerSupport{
@Bean public KeyGenerator keyGenerator() { return new KeyGenerator() {
@Override public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName()); for (Object obj : params) {
sb.append(obj.toString());
} return sb.toString();
}
};
}
@SuppressWarnings("rawtypes")
@Bean public CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager rcm = new RedisCacheManager(redisTemplate); //设置缓存过期时间 //rcm.setDefaultExpiration(60);//秒
return rcm;
}
@Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet(); return template;
}
}
4 手动方式存储
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(Application.class)
public class TestRedis {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate redisTemplate;
@Test
public void test() throws Exception {
stringRedisTemplate.opsForValue().set("aaa", "111");
Assert.assertEquals("111", stringRedisTemplate.opsForValue().get("aaa"));
}
@Test
public void testObj() throws Exception {
User user=new User("[email protected]", "aa", "aa123456", "aa","123");
ValueOperations operations=redisTemplate.opsForValue();
operations.set("com.neox", user);
operations.set("com.neo.f", user,1,TimeUnit.SECONDS);
Thread.sleep(1000);
//redisTemplate.delete("com.neo.f");
boolean exists=redisTemplate.hasKey("com.neo.f");
if(exists){
System.out.println("exists is true");
}else{
System.out.println("exists is false");
}
// Assert.assertEquals("aa", operations.get("com.neo.f").getUserName());
}
}
5 通过注解方式使用Redis
先来了解下基本的注解以及其作用吧
@CacheEvict
即应用到移除数据的方法上,如删除方法,调用方法时会从缓存中移除相应的数据
@CachePut
应用到写数据的方法上,如新增/修改方法,调用方法时会自动把相应的数据放入缓存:
Tip: Spring Redis默认使用JDK进行序列化和反序列化,因此被缓存对象需要实现java.io.Serializable接口,否则缓存出错。
Tip: 当被缓存对象发生改变时,可以选择更新缓存或者失效缓存,但一般而言,后者优于前者,因为执行速度更快。
Watchout! 在同一个Class内部调用带有缓存注解的方法,缓存并不会生效。
-->在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存
@Cacheable
应用到读取数据的方法上,即可缓存的方法,如查找方法:先从缓存中读取,如果没有再调用方法获取数据,然后把数据添加到缓存中:-->@Cacheable根据运行流程,如下@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存
○ value: 缓存名称
○ key: 缓存键,一般包含被缓存对象的主键,支持Spring EL表达式
○ unless: 只有当查询结果不为空时,才放入缓存
@Caching
有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id-->user;username--->user;email--->user的缓存;此时就需要@Caching组合多个注解标签了。
如用户新增成功后,添加id-->user;username--->user;email--->user到缓存;
使用的service层的代码如下:
/**
* Created by Administrator on 2017/7/24.
*/
@Service("userSerivce")
public class UserSerivceImpl {
@Autowired
UserRepository userRepository;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate redisTemplate;
//unless-->用于否决缓存更新的,不像condition,该表达只在方法执行之后判断,此时可以拿到返回值result进行判断了
@Cacheable(value="userCache",key="'user:'+#username",unless = "#result==null")
public User getUserByUsername(String username){
User user = null;
List userlist = userRepository.findByUsername(username);
if(userlist!=null && userlist.size()>0){
user = userlist.get(0);
}
return user;
}
// @Cacheable(value="userCache#120",key="'user:'+#username",unless = "#result==null")
// public User getUserByUsername(String username){
// User user = null;
// List userlist = userRepository.findByUsername(username);
// if(userlist!=null && userlist.size()>0){
// user = userlist.get(0);
// }
// return user;
// }
//allEntries-->是否移除所有数据
//beforeInvocation-->是调用方法之前移除/还是调用之后移除
@CacheEvict(value = "userCache",key="'user:'+#user.username")
public void delUserById(User user){
userRepository.delete(user);
}
public String setUserInRedis(){
stringRedisTemplate.opsForValue().set("abc","123",60L, TimeUnit.SECONDS);
return stringRedisTemplate.opsForValue().get("abc");
// redisTemplate.opsForList();//可直接放入实现序列化的pojo
}
public void delUserInRedis(){
stringRedisTemplate.delete("abc");
}
//condition-->满足缓存条件的数据才会放入缓存,condition在调用方法之前和之后都会判断
@CachePut(value="userCache",key = "#user.username",condition = "#user.username<='100'")
public User save(User user){
userRepository.save(user);
return user;
}
@Caching(
put={
@CachePut(value = "userCache", key = "'user:'+#user.id"),
@CachePut(value = "userCache", key = "'user:'+#user.username")
}
)
public User addUser(User user){
userRepository.save(user);
return user;
}
@UserSaveCache
public User addUser2(User user){
userRepository.save(user);
return user;
}
@Cacheable(value="userCache",key="'user:'+#username",condition = "#root.target.canCache()",unless = "#result==null")
public User getUserByUsername2(String username){
User user = null;
List userlist = userRepository.findByUsername(username);
if(userlist!=null && userlist.size()>0){
user = userlist.get(0);
}
return user;
}
@Cacheable(value="userCache",key="'user:'+#username",condition = "#root.target.notCache()")
public User getUserByUsername3(String username){
User user = null;
List userlist = userRepository.findByUsername(username);
if(userlist!=null && userlist.size()>0){
user = userlist.get(0);
}
return user;
}
public boolean canCache(){
return true;
}
public boolean notCache(){
return false;
}
}
使用的controller层的代码如下:
@RestController
public class UserController {
@Autowired
UserRepository userRepository;
@Autowired
UserSerivceImpl userSerivce;
@GetMapping("/users/moreparam18/{username}")
public User getUserByUsername(@PathVariable String username){
//页数从0开始算,比如第一页应传page=0
return userSerivce.getUserByUsername(username);
}
@DeleteMapping("/users/del/{id}")
public void delUserById12(@PathVariable Long id){
User user = userRepository.findOne(id);
if(user !=null){
userSerivce.delUserById(user);
}
}
@GetMapping("/users/moreparam19")
public String setUserInRedis(){
//页数从0开始算,比如第一页应传page=0
return userSerivce.setUserInRedis();
}
@GetMapping("/users/moreparam20")
public void delUserInRedis(){
//页数从0开始算,比如第一页应传page=0
userSerivce.delUserInRedis();
}
@PostMapping("/users/moreparam21/{username}")
public User save(@PathVariable String username){
//页数从0开始算,比如第一页应传page=0
User user = new User();
user.setUsername(username);
user.setPassword("123456");
user.setName(username);
userSerivce.save(user);
return user;
}
@PostMapping("/users/moreparam22/{username}")
public User addUser(@PathVariable String username){
//页数从0开始算,比如第一页应传page=0
User user = new User();
user.setUsername(username);
user.setPassword("123456");
user.setName(username);
userSerivce.addUser(user);
return user;
}
@PostMapping("/users/moreparam23/{username}")
public User addUser2(@PathVariable String username){
//页数从0开始算,比如第一页应传page=0
User user = new User();
user.setUsername(username);
user.setPassword("123456");
user.setName(username);
userSerivce.addUser2(user);
return user;
}
@GetMapping("/users/moreparam24/{username}")
public User getUserByUsername2(@PathVariable String username){
//页数从0开始算,比如第一页应传page=0
return userSerivce.getUserByUsername2(username);
}
@GetMapping("/users/moreparam25/{username}")
public User getUserByUsername3(@PathVariable String username){
//页数从0开始算,比如第一页应传page=0
return userSerivce.getUserByUsername3(username);
}
}
小提示; 1. 1、首先执行@CacheEvict(如果beforeInvocation=true且condition 通过),如果allEntries=true,则清空所有
2. 2、接着收集@Cacheable(如果condition 通过,且key对应的数据不在缓存),放入cachePutRequests(也就是说如果cachePutRequests为空,则数据在缓存中)
3. 3、如果cachePutRequests为空且没有@CachePut操作,那么将查找@Cacheable的缓存,否则result=缓存数据(也就是说只要当没有cache put请求时才会查找缓存)
4. 4、如果没有找到缓存,那么调用实际的API,把结果放入result
5. 5、如果有@CachePut操作(如果condition 通过),那么放入cachePutRequests
6. 6、执行cachePutRequests,将数据写入缓存(unless为空或者unless解析结果为false);
7. 7、执行@CacheEvict(如果beforeInvocation=false 且 condition 通过),如果allEntries=true,则清空所有
流程中需要注意的就是2/3/4步:
如果有@CachePut操作,即使有@Cacheable也不会从缓存中读取;问题很明显,如果要混合多个注解使用,不能组合使用@CachePut和@Cacheable;官方说应该避免这样使用(解释是如果带条件的注解相互排除的场景);不过个人感觉还是不要考虑这个好,让用户来决定如何使用,否则一会介绍的场景不能满足
提供的SpEL上下文数据
Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档:
名字 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root对象 | 当前被调用的方法名 | #root.methodName |
method | root对象 | 当前被调用的方法 | #root.method.name |
target | root对象 | 当前被调用的目标对象 | #root.target |
targetClass | root对象 | 当前被调用的目标对象类 | #root.targetClass |
args | root对象 | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root对象 | 当前方法调用使用的缓存列表(如@Cacheable(value={"cache1", "cache2"})),则有两个cache | #root.caches[0].name |
argument name | 执行上下文 | 当前被调用的方法的参数,如findById(Long id),我们可以通过#id拿到参数 | #user.id |
result | 执行上下文 | 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,'cache evict'的beforeInvocation=false) | #result |
参考资料
Redis 教程
Spring boot redis的使用