学习目标
Redis简介
Redis的安装使用
Redis的数据类型
SpringBoot整合Redis
声明式缓存
Redis的常见问题
事务
分布式锁
持久化策略
淘汰策略
Redis简介
Redis 是完全开源的,遵守 BSD 协议,是一个高性能的 key-value 数据库。
特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
Redis的优势:
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Set 及Zset 数据类型操作。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
Redis的安装使用
Linux安装
-
先安装gcc编译器,可以用来编译c、c++等代码
yum -y install gcc //-y表示自动安装
-
安装Redis
wget http://download.redis.io/releases/redis-3.2.5.tar.gz //下载redis安装包到usr/local tar xzf redis-2.8.17.tar.gz cd redis-2.8.17 make
-
Redis的配置(进入到redis目录下的redis.conf文件)
cd /usr/local/redis-5.0.4 vi redis.conf(本人的redis放在/usr/local)
-
注释掉 bind 127.0.0.1
-
关闭保护模式 protected mode no
-
使用Redis
-
1、启动服务器 src中 ./redis-server …/redis.conf
-
2、启动客户端 src中 ./redis-cli
Windows安装
- 从官网下载redis的windows版本
- 解压
- 启动服务器端
- 启动客户端
Redis的数据类型
数据类型有:
-
string 字符串(适合保存单个数据)
set key value get key 可以设置失效时间 set key value EX 10 //10秒钟之后失效
-
hash 哈希(适合保存复杂类型数据,如:一般可以用来存储Java中的一个完整的自定义对象)
//hmset是存储hash值的指令, //user是当前hash的key //name "zhangsan" age 23 sex "nan" 是 key对应的值 127.0.0.1:6379> hmset user name "zhangsan" age 23 sex "nan" OK //hmget获取hash中的某一个属性的值 127.0.0.1:6379> hmget user name 1) "zhangsan" 127.0.0.1:6379> hmget user age 1) "23" //hgetall是获取hash中的所有属性对应的值 127.0.0.1:6379> hgetall user 1) "name" 2) "zhangsan" 3) "age" 4) "23" 5) "sex" 6) "nan"
-
list 列表(适合保存有序的、可重复的数据)
采用的链表结构进行数据存储
lpush 从右向左添加
rpush 从左向右添加
lrange key start stop
//lpush用来存储一个列表的命令。interesting是列表的名称,"basketball"列表中的值 127.0.0.1:6379> lpush interesting "basketball" (integer) 1 127.0.0.1:6379> lpush interesting "football" "ball" (integer) 3 //lrange输出列表中的数据的命令, interesting就是列表的名称 。 0 2是列表的开始输出索引和结束索引。 127.0.0.1:6379> lrange interesting 0 2 1) "ball" 2) "football" 3) "basketball"
-
set 无序集合(适合保存无序的,不可重复的数据)
sadd key member //存数据 smembers key //取数据 案例: 127.0.0.1:6379> sadd strset "a" "b" "c" (integer) 3 127.0.0.1:6379> smembers strset 1) "b" 2) "c" 3) "a"
-
zset 有序集合(适合保存有序的,不可重复的数据)
zadd key score member (score是一个数字,zset就是通过这个数字进行排序,可以重复) zrangebyscore key 0 1000 //通过分数排序输出 有序集合是按照score进行排序
SpringBoot整合Redis
1)依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2)配置文件
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0
spring.redis.jedis.pool.max-active=100
spring.redis.jedis.pool.max-wait=100ms
spring.redis.jedis.pool.max-idle=100
spring.redis.jedis.pool.min-idle=10
3)配置RedisTemplate
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(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);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
4)使用RestTemplate
常用方法:
- opsForValue 获得string类型的操作对象
- opsForHash 获得hash类型的操作对象
…
5) 缓存使用的流程
按id查询商品的过程
1) 以id为键查询Redis缓存,如果能查到就返回数据,结束
2)如果查不到,就查询数据库,数据库查到,缓存到Redis,返回数据
3)如果数据库查不到,返回null,结束
4)增删改数据库的同时,要修改缓存
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements IGoodsService {
public static final String TYPE = "GOODS-";
@Autowired
private RedisTemplate<String,Object> redisTemplate;
public Goods getGoodsById(Long id){
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
//1) 以id为键查询Redis缓存,如果能查到就返回数据,结束
Object value = ops.get(TYPE + id);
//2)如果查不到,就查询数据库
if(value == null){
System.out.println("缓存不存在,查询数据库");
// 数据库查到,缓存到Redis,返回
Goods goods = this.getById(id);
if(goods != null){
System.out.println("数据库存在,保存到缓存");
ops.set(TYPE + id,goods);
}else{
System.out.println("数据库不存在,返回null");
}
return goods;
}else{
System.out.println("缓存存在,返回"+value);
//如果能查到就返回数据,结束
return (Goods) value;
}
}
}
声明式缓存
SpringBoot项目需要的依赖,配置文件同上
1.在启动类上添加注解 @EnableCaching
2.Redis的配置类
@Configuration
public class RedisConfig {
@Bean
public RedisCacheConfiguration provideRedisCacheConfiguration(){
//加载默认配置
RedisCacheConfiguration conf = RedisCacheConfiguration.defaultCacheConfig();
//返回Jackson序列化器
return conf.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
}
3)缓存注解
-
@CacheConfig 使用在Service类上,如:@CacheConfig(cacheNames = “books”)
-
@Cacheable 使用在查询方法上,让方法优先查询缓存
-
@CachePut 使用在更新和添加方法上,数据库更新和插入数据后同时保存到缓存里
-
@CacheEvict 使用在删除方法上,数据库删除后同时删除缓存
-
@Caching使用在删除方法上里面可以装多个@CacheEvict,数据库删除后同时删除缓存
注意:实体类必须实现序列化接口
@CacheConfig(cacheNames = "GOOD")
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goodss> implements IGoodService {
@Autowired
private GoodsMapper goodsMapper;
@Cacheable(key = "T(String).valueOf(#id)")
@Override
public Goodss selectOneGoods(Long id) {
return goodsMapper.selectOneGoods(id);
}
@Cacheable(cacheNames = "goods-page",key = "T(String).valueOf(#page.current)")
@Override
public IPage<Goodss> getSelectGoodss(IPage<Goodss> page) {
return goodsMapper.getSelectGoodss(page);
}
@CachePut(key="T(String).valueOf(#goodss.id)")
@CacheEvict(cacheNames = "goods-page",allEntries = true)
@Override
public Goodss saveGoods(Goodss goodss) {
this.save(goodss);
return goodss;
}
@CachePut(key="T(String).valueOf(#goodss.id)")
@CacheEvict(cacheNames = "goods-page",allEntries = true)
@Override
public Goodss updateGoods(Goodss goodss) {
this.updateById(goodss);
return goodss;
}
@Caching(evict = {@CacheEvict (key = "T(String).valueOf(#id)"),@CacheEvict(cacheNames = "goods-page",allEntries = true)})
@Override
public void deleteGoods(Long id) {
this.removeById(id);
}
}
Redis的常见问题
1)缓存击穿
高并发的情况下,短时间内缓存会被穿过,请求直接打到数据库上,可能导致数据库压力过大。
解决方案:对代码上锁(双重检查锁)
2)缓存穿透
高并发的情况下,如果查询不存在的数据,因为缓存和数据库都不存在,请求都会打到数据库上,可能导致系统崩溃。
解决方案:
1) 保存不存在的数据到缓存中,设置一定过期时间
2) 布隆过滤器(直接过滤掉不存在数据的请求) 不能准确判断是否存在数据,能准确判断数据不存在
3)缓存雪崩
高并发的情况下,缓存服务器重启或热点数据同时过期,全部访问数据库,导致数据库宕机
解决方案:
1)配置缓存集群
2)尽量给热点数据设置不一样的过期时间,相对均匀
解决代码
public Goods getGoodsById(Long id){
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
Object value = ops.get(TYPE + id);
//外层先读缓存,缓存如果有,就不执行同步块
if(value == null) {
synchronized (this) {
//1) 以id为键查询Redis缓存,如果能查到就返回数据,结束
value = ops.get(TYPE + id);
//2)如果查不到,就查询数据库
if (value == null) {
System.out.println("缓存不存在,查询数据库");
// 数据库查到,缓存到Redis,返回
Goods goods = this.getById(id);
if (goods != null) {
System.out.println("数据库存在,保存到缓存");
ops.set(TYPE + id, goods);
} else {
System.out.println("数据库不存在,返回null");
//保存空数据到缓存中,设置过期时间
ops.set(TYPE + id,new Goods(),30, TimeUnit.SECONDS);
}
return goods;
} else {
System.out.println("缓存存在,返回" + value);
//如果能查到就返回数据,结束
return (Goods) value;
}
}
}
System.out.println("缓存存在,返回" + value);
return (Goods) value;
}
事务
Redis的事务是将一系列操作打包,一起提交。没有原子性,隔离性的,也没有回滚事务。
multi 启动事务
exec 提交事务
discard 放弃事务
watch 监视某个数据,如果修改该数据时,在另一个事务中对该数据进行了修改,当前的修改就被放弃
总结:
事务中如果出现语法错误,整个事务无法执行;如果出现数据错误,事务可以成功一部分,失败一部分。
PS:incr 增加数值,decr 减少数值
分布式锁
Redis可以作为分布式锁,让所有服务共同访问,内部的数据可以使用watch(乐观锁)机制进行监控。
悲观锁能保证线程安全,但是性能低
Redis提供乐观锁,性能较高
Redis内部是单线程,不存在线程安全问题
案例:模拟秒杀功能,商品库存10,1000用户来抢,不能出现超买的情况
@Repository
public class GoodsCache {
Logger logger = Logger.getLogger(GoodsCache.class);
@Autowired
private RedisTemplate<String,String> redisTemplate;
public String buy(){
Boolean execute = redisTemplate.execute(new SessionCallback<Boolean>() {
@Override
public Boolean execute(RedisOperations redisOperations) throws DataAccessException {
//开启乐观锁,当前线程执行exec释放锁之前,其他线程进行修改都会失败
redisOperations.watch("goods_num");
//读取商品数量
int num = Integer.parseInt(redisOperations.opsForValue().get("goods_num").toString());
if (num <= 0) {
logger.error("已售完");
return false;
}
//开启事务
redisOperations.multi();
//修改商品数量
redisOperations.opsForValue().increment("goods_num", -1);
//提交事务,释放锁
List list = redisOperations.exec();
//如果事务执行失败,返回的集合中数据为空
if (list == null || list.isEmpty()) {
logger.error("购买失败");
return false;
}
logger.error("购买成功" + list.get(0));
return true;
}
});
return String.valueOf(execute);
}
}
@Controller
@RequestMapping("/products")
public class ProductController {
@ResponseBody
@RequestMapping("/buy")
public JSONResult buy(){
return new JSONResult(1,goodsService.buy());
}
}
持久化策略
为什么持久化?Redis属于内存式数据库,程序关闭后数据会清空,有时候需要将内存中的数据长期在文件保存起来
持久化策略
AOF:默认每秒对数据进行持久化
RDB:按条件触发持久化操作(任意一个)
900 1 900秒中修改1次
300 10 300秒中修改10次
60 10000 60秒中修改10000次
配置方法
RDB
AOF
appendonly yes / no yes开启AOF
appendfsync everysec 每秒保存
如何选择?
允许少量数据丢失,性能比较高----RDB
只允许很少数据丢失----AOF
不允许数据丢失----RDB + AOF
淘汰策略
为什么要淘汰?Redis数据保存在内存中,数据太多会出现溢出问题,Redis会根据某些策略淘汰一些数据
64位系统,上限就是内存上限;32位最大4G
配置最大内存:
max-memory 配置0就是无上限(默认)
LRU算法:Least Recently Used 最近最少使用算法,淘汰长期不用的缓存
淘汰策略:
maxmemory-policy
值:
noevication(默认) 不淘汰
allkeys-lru(推荐) 使用LRU淘汰比较少使用的键
volatile-lru 在过期的键中淘汰较少使用的
allkeys-random 在所有键中随机淘汰
volatile-random 在过期键中随机淘汰
volatile-ttl 在过期键中淘汰存活时间短的键