Redis简介:Redis 是完全开源免费的,是一个高性能的key-value数据库,目前市面上主流的no-sql数据库有Redis、Memcache、Tair(淘宝自研发),Redis的官网:https://redis.io/
之前的博客已经写过redis的搭建,主从复制以及哨兵机制,本篇博客侧重于讲解底层原理,实战等...开始吧那就
1. Redis应用场景
相信我们程序员都用过或者听过Redis,那么我们首先谈下它有哪些应用场景,博主总结了以下几点:
① Token令牌的生成
② 短信验证码的code
③ 可以实现缓存查询数据,减轻我们的数据库的访问压力 Redis与mysql数据库不同步的问题
④ 分布式锁
⑤ 延迟操作(案例:订单超时未支付,也可以用RabbitMQ解决)
⑥ 分布式消息中间件(发布订阅,一般用的很少)
2. Redis线程模型
首先Redis官方是没有windows版本的,只有Linux版本,那么问题来了,为什么我们Windows本地可以安装? 答案:一些大牛把Linux版的epoll机制改成Select轮训,从而发布到github供我们下载,所以我们Windows上的Redis并不是官方的。
Redis的底层采用NIO中的多路IO复用的机制,能够非常好的支持这样的并发,从而保证线程安全问题;Redis单线程,也就是底层采用一个线程维护多个不同的客户端IO操作。但是Nio在不同的操作系统上实现的方式有所不同,在我们Windows操作系统使用Selector实现轮训时间复杂度是为O(N),而且还存在空轮训的情况,效率非常低, 其次是默认对我们轮训的数据有一定限制,所以支持上万的TCP连接是非常难。所以在Linux操作系统采用epoll实现事件驱动回调,不会存在空轮训的情况,只对活跃的 Socket连接实现主动回调这样在性能上有大大的提升,所以时间复杂度是为O(1)。
所以为什么单线程的Nginx、Redis都能够非常高支持高并发,最终都是Linux中的IO多路复用机制epoll。
Windows - 空轮训,相当于在Selector写了个for循环,一直在轮训,万一别人不给我发数据也去轮训一下,效率非常低;
Linux - epoll,给每一个TCP连接注册一个事件回调,一旦有人给我发数据,我就走我的事件回调,没给我发数据,我就不用去轮训,不会存在空轮训的情况,只对活跃的 Socket连接实现主动回调这样在性能上有大大的提升。
3. Redis - Value的五种数据类型
① String类型 - <key,value> 【用的最多】
String是Redis最基本的类型,一个key对应一个value,String类型是二进制安全的。意思是Redis的String可以包含任何数据。比如Jpg图片或者序列化的对象, String类型是Redis最基本的数据类型,一个键最大能存储512MB。
命令:set name zhangsan,get name
② hash类型 - <key,<key1,value>> 案例:购物车
我们可以将Redis中的Hash类型看成具有<key,<key1,value>>,其中同一个key可以有多个不同key值的<key1,value>,所以该类型非常适合于存储值对象的信息。如Username、Password和Age等。如果Hash中包含很少的字段,那么该类型的数据也将仅占用很少的磁盘空间。
命令:hmset user name zhangsan 【解读:key为user,value为hashmap<name,zhangsan>】,hgetall user
可以支持多个key-hmset user name zhangsan age 22
③ list类型 - 案例:秒杀时一个库存对应多个令牌桶token
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
命令:lpush userlist zhangsan lisi wangwu
④ set - 特点:value不允许重复,如果重复则覆盖 【基本不用】
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
⑤ zset - 【基本不用】
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
拓展:Redis如何存放一个java对象?
答案: ① json(即String类型),② 二进制
方式①实现:Redis Desktop Manager工具不会乱码
@Component
public class RedisUtils {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void setString(String key, String value) {
setString(key, value, null);
}
public void setString(String key, String value, Long timeOut) {
stringRedisTemplate.opsForValue().set(key, value);
if (timeOut != null) {
stringRedisTemplate.expire(key, timeOut, TimeUnit.SECONDS);
}
}
public String getString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
}
@RestController
public class RedisController {
@Autowired
private RedisUtils redisUtils;
@GetMapping("/addUser")
public String addUser(UserEntity userEntity) {
String json = JSONObject.toJSONString(userEntity);
redisUtils.setString("userEntity", json);
return "success";
}
@RequestMapping("/getUser")
public UserEntity getUser(String key) {
String json = redisUtils.getString(key);
UserEntity userEntity = JSONObject.parseObject(json, UserEntity.class);
return userEntity;
}
}
方式②实现:(注意:需要序列化的对象(如UserEntity)一定要实现Serializable接口) Redis Desktop Manager会乱码
@Component
public class RedisTemplateUtils {
@Resource //@Resource通过名称注入,注意这里不能用@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void setObject(String key, Object value) {
setObject(key, value, null);
}
public void setObject(String key, Object value, Long timeOut) {
redisTemplate.opsForValue().set(key, value);
if (timeOut != null) {
redisTemplate.expire(key, timeOut, TimeUnit.SECONDS);
}
}
public Object getObject(String key) {
return redisTemplate.opsForValue().get(key);
}
}
@RestController
public class RedisController {
@Autowired
private RedisTemplateUtils redisTemplateUtils;
@GetMapping("/addUser")
public String addUser(UserEntity userEntity) {
redisTemplateUtils.setObject("userEntity", userEntity);
return "success";
}
@RequestMapping("/getUser")
public UserEntity getUser(String key) {
return (UserEntity) redisTemplateUtils.getObject(key);
}
}
两种方式区别:二进制只适合于Java对象,json是通用的。
4. Mysql与Redis数据同步解决方案
方式①:当数据库有数据变动时,直接清空Redis对应的该数据,此次或下次再同步到Redis
方式②:直接采用MQ订阅MySQL binlog日志文件增量同步到Redis中,整个过程采用最终一致性方案。类似于之前博客的主从复制
方式③:使用阿里巴巴的开源框架Canal(后期单独拿一篇博客讲述)
5. Redis的持久化机制
Redis因为某种原因的情况下宕机之后,数据是不会丢失的,原理就是持久化机制 ~
同理,我们用的ehcache也有持久化机制,大部分的缓存框架都会有基本功能:淘汰策略、持久机制。
Redis的持久化的机制有两种:aof、rdb(默认),下面分别讲解:
Redis提供了两种持久化的机制,分别为RDB、AOF实现,RDB采用定时(全量)持久化机制,但是服务器因为某种原因宕机后可能数据会丢失,AOF是基于数据日志操作实现的持久化(增量)。
RDB:(全量同步)
Redis已经帮助我默认开启了rdb存储,以快照的形式将数据持久化到磁盘的是一个二进制的文件dump.rdb, 可以在redis.conf中搜索到,生成的rdb文件为
,存放的目录为
Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:
save 900 1 # 在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。
save 300 10 # 在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
save 60 10000 # 在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
AOF:(增量同步)
AOF在Redis的配置文件中存在三种同步策略,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,能够保证数据不丢失,但是效率非常低。
appendfsync everysec #每秒钟同步一次,可能会丢失1s内的数据,但是效率非常高。【默认 - 建议使用】
appendfsync no #从不同步。高效但是数据不会被持久化。
Redis默认没有开启aof存储,那么如何开启aof? 非常简单,只需要在redis.conf中把默认的配置改为yes即可
,aof同步的文件为
原理:aof是以执行命令的形式实现同步。每次执行都会记录写的操作,如set name zhangsan等都会被记录下来,appendfsync everysec一秒后从缓冲区同步到aof文件中去。
拓展:全量同步(rdb)与增量同步(aof)区别:
① 就是每天定时(避开高峰期)或者是采用一种周期的实现将数据拷贝另外一个地方。频率不是很大,但是可能会造成数据的丢失。
② 增量同步采用行为操作对数据实现同步,频率非常高、对服务器同步的压力也是非常大的、保证数据不丢失。
6. Redis内存淘汰策略
概念:将Redis用作缓存时,如果内存空间用满,就会自动驱逐老的数据,防止内存撑爆。
Redis默认有六种淘汰策略:
noeviction:当内存使用达到阈值的时候,执行命令直接报错
allkeys-lru:在所有的key中,优先移除最近未使用的key。(推荐)
volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key。
allkeys-random:在所有的key中,随机移除某个key。
volatile-random:在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。
在redis.conf文件中,设置Redis 内存大小的限制,比如:
当数据达到限定大小后,会选择配置的策略淘汰数据,通过配置maxmemory-policy置Redis的淘汰策略,默认也是注掉的,如果自定义内存大小的话,建议使用allkeys-lru,即
。
【关于maxmemory的设置,如果redis的应用场景是作为db使用,那不要设置这个选项,因为db是不能容忍丢失数据的。
如果作为cache使用,则可以启用这个选项(其实既然有淘汰策略,那就是cache了。。。),默认官方没有对maxmemory做限制,理论上默认最大内存限制为当前机器可用内存】
7. Redis自动过期机制
在实际开发过程中经常会遇到一些有时效性数据,比如订单超时功能,30分钟未支付应该将订单改为已失效状态。在关系型数据库中一般都要增加一个字段记录数据的到期时间,然后通过定时任务周期性的进行检查,这种方式性能太差了。Redis本身就对键过期提供了很好的支持。
实现需求:处理订单过期自动取消,比如下单30分钟未支付自动更改订单状态为已失效(超时)
解决方案:① 定时器 ② RabbitMQ的死信/延时队列 ③ 利用Redis的过期机制
Redis过期机制:在Redis中可以使用expire命令设置一个键的存活时间(ttl: time to live),过了这段时间,该键就会自动被删除,EXPIRE命令的使用方法如下:
expire key ttl(单位秒) 【命令返回1表示设置ttl成功,返回0表示键不存在或者设置失败。】
key失效监听实战:
把RedisMessageListenerContainer注入到Spring容器
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
编写Redis监听器
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Autowired
private OrderMapper orderMapper;
/**
* 【监听】 - 当我们key失效的时候执行该方法
*/
@Override
public void onMessage(Message message, byte[] pattern) {
String expireKey = message.toString();
System.out.println(expireKey + "失效啦~");
// 前缀判断 - 每个服务,前缀事先商量好,防止冲突 eg:orderToken_XXXXXXX // 或者是根据库区分
OrderEntity orderEntity = orderMapper.getOrderNumber(expireKey);
if (orderEntity == null) {
return;
}
// 获取订单状态
Integer orderStatus = orderEntity.getOrderStatus();
// 如果该订单状态为待支付的情况下,直接将该订单修改为已经超时 (0待支付 1已支付 2已失效)
if (orderStatus == 0) {
orderMapper.updateOrderStatus(expireKey, 2);
// 库存在加上1...此处省略,自行实现
}
// 不设置超时时间,但是可能被淘汰回收策略回收掉,也会走回调方法
}
}
@Data
public class OrderEntity {
private Long id;
private String orderName;
private Integer orderStatus;
private String orderToken;
private String orderNumber;
public OrderEntity(Long id, String orderName, String orderNumber, String orderToken) {
this.id = id;
this.orderName = orderName;
this.orderNumber = orderNumber;
this.orderToken = orderToken;
}
}
@Mapper
public interface OrderMapper {
@Insert("insert into order_table values (null,#{orderName},0,#{orderToken},#{orderNumber})")
int insertOrder(OrderEntity OrderEntity);
@Select("SELECT id,order_name,order_status,order_token,order_number FROM order_table " +
"where order_token=#{orderToken};")
OrderEntity getOrderNumber(String orderToken);
@Update("update order_table set order_status=#{orderStatus} where order_token=#{orderToken}")
int updateOrderStatus(String orderToken, Integer orderStatus);
}
@Component
public class RedisUtils {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void setString(String key, String value) {
setString(key, value, null);
}
public void setString(String key, String value, Long timeOut) {
stringRedisTemplate.opsForValue().set(key, value);
if (timeOut != null) {
stringRedisTemplate.expire(key, timeOut, TimeUnit.SECONDS);
}
}
public String getString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
}
@RestController
public class OrderController {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RedisUtils redisUtils;
@RequestMapping("/addOrder")
public String addOrder() {
// 1.提前生成订单token 临时且唯一
String orderToken = UUID.randomUUID().toString();
Long orderNumber = System.currentTimeMillis();
// 2.将我们的token存放到redis中
redisUtils.setString(orderToken, orderNumber + "", 60L); // 60秒
OrderEntity orderEntity = new OrderEntity(null, "腾讯视频年度vip", orderNumber + "", orderToken);
return orderMapper.insertOrder(orderEntity) > 0 ? "success" : "fail";
}
}
启动项目,调用addOrder接口,会看到数据库新增一条数据:
60秒后,redis中的该key删除,进入监听器,并根据该key判断订单状态,如果为1,则不操作,如果依旧为0,则改为2:
【原理】:
① 创建订单的时候绑定一个订单token 存放在redis中(有效期只有30分钟) key为token,value为订单编号。
② 对该key绑定过期事件回调,判断状态从而修改订单状态。
8. Redis缓存雪崩&缓存击穿&缓存穿透
【缓存穿透】
缓存穿透是指定key不存在的key,频繁的高并发查询,导致缓存是无法命中,如上图所示,当黑客利用for循环,随机生成orderNumber去访问getOrder接口,则每次大概率会穿透Redis进入MySQL数据库,导致大量IO操作,从而会导致数据库的压力非常大。
解决方案:
① 接口实现api的限流、防御ddos(模拟请求)、接口频率限制、网关实现黑名单(核心)
② 从数据库和Redis如果都查询不到数据的情况下,将数据库的空值写入到缓存中,加上短时间的有效期 【针对于黑客使用相同的key进行攻击,一定程度上可以减轻频繁数据库IO操作】
③ 布隆过滤器
默认情况下,数组值为0,数组取值(0或1)
为什么要有三个hash函数计算出3个hash值?防止冲突,减少重叠的概率
布隆过滤器:布隆过滤器适用于判断一个元素在集合中是否存在,但是可能会存在误判的问题。
Bloom Filter基本实现原理采用位数组与联合函数一起实现,实现的原理采用二进制向量数组和随机映射hash函数。
为什么存在误判问题?
因为有可能在查询的时候,会根据key计算hash值,该值可能与布隆过滤器存放的其它元素的hash值产生重叠,即上图中的sex,hash值数组对应的下标都为1,即代表布隆过滤器中存在该key,而实际上该key是不存在的。
如何减少冲突(误判)概率?
二进制数组长度设置大一点,代码直接把fpp调低一点即可:
BloomFilter<Integer> integerBloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, 0.001);
代码实现&应用场景:
在上图代码中我们可以用redis很好的降低数据库访问压力,那么用户频繁访问redis,也会对redis造成很大压力,这时候我们就引入布隆过滤器,如果不存在直接不走redis,直接返回null给客户端即可:
<!--引入布隆过滤器 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>22.0</version>
</dependency>
@Mapper
public interface OrderMapper {
@Insert("insert into order_table values (null,#{orderName},0,#{orderToken},#{orderNumber})")
int insertOrder(OrderEntity OrderEntity);
@Select("select * from order_table where id=#{id}")
OrderEntity getById(Integer orderId);
@Select("select id from order_table")
List<Integer> getOrderIds();
}
@RestController
public class OrderController {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RedisUtils redisUtils;
@Autowired
private RedisTemplateUtils redisTemplateUtils;
BloomFilter<Integer> integerBloomFilter;
@RequestMapping("/addOrder")
public String addOrder() {
// 1.提前生成订单token 临时且唯一
String orderToken = UUID.randomUUID().toString();
Long orderNumber = System.currentTimeMillis();
// 2.将我们的token存放到rdis中
redisUtils.setString(orderToken, orderId + "", 10L);
OrderEntity orderEntity = new OrderEntity(null, "QQ黄钻", orderNumber + "", orderToken);
return orderMapper.insertOrder(orderEntity) > 0 ? "success" : "fail";
}
@RequestMapping("/getOrder")
public OrderEntity getOrder(Integer orderId) {
// 0.判断我们的布隆过滤器
if (!integerBloomFilter.mightContain(orderId)) {
System.out.println("从布隆过滤器中查询不存在");
return null;
}
// 如果正好碰巧,orderId在redis和mysql都没有,也没关系,
// 就算布隆过滤器产生了误判的情况,也不会影响整个业务,顶多穿透了布隆而已
// 1.先查询Redis中数据是否存在
OrderEntity orderRedisEntity = (OrderEntity) redisTemplateUtils.getObject(orderId + "");
if (orderRedisEntity != null) {
System.out.println("直接从Redis中返回数据");
return orderRedisEntity;
}
// 2. 查询数据库的内容
System.out.println("从DB查询数据");
OrderEntity orderDBEntity = orderMapper.getOrderById(orderId);
if (orderDBEntity != null) {
System.out.println("将Db数据放入到Redis中");
redisTemplateUtils.setObject(orderId + "", orderDBEntity);
}
return orderDBEntity;
}
/**
* 从数据库预热id到布隆过滤器中(预热机制)
* @return
*/
@RequestMapping("/dbToBulong")
public String dbToBulong() {
List<Integer> orderIds = orderMapper.getOrderIds();
integerBloomFilter = BloomFilter.create(Funnels.integerFunnel(), orderIds.size(), 0.01);
for (int i = 0; i < orderIds.size(); i++) {
// 添加到我们的布隆过滤器中
integerBloomFilter.put(orderIds.get(i));
}
return "success";
}
}
首先用预热机制,把所有订单id存到布隆过滤器
分别请求:http://localhost:8080/getOrder?orderId=1,http://localhost:8080/getOrder?orderId=2,
http://localhost:8080/getOrder?orderId=1,http://localhost:8080/getOrder?orderId=2,会发现控制台打印信息:
请求一个不存在的id,http://localhost:8080/getOrder?orderId=3,会发现直接返回null,并打印从布隆查询不存在。
由此可见,使用布隆过滤器可以有效减少redis的压力。
【缓存击穿】
在高并发的情况下,当一个热点key(经常使用到key)过期时,因为访问该key请求过多,多个请求同时发现该缓存key过期,这时候同时查询数据库,同时向Redis写入缓存数据,对我们数据库压力非常大。
做电商项目的时候,把这个热点key就称为"爆款"。
解决方案:
① 分布式集群环境 - 使用分布式锁技术:多个请求同时只要谁能够获取到锁,谁就能够去数据库查询将数据查询的结果放入Redis中(直接在上面代码的红色箭头处上锁即可)
② 单机环境用Lock锁或者Synchronized
③ 软过期 - 对热点key设置无限有效期或者异步延长时间
【缓存雪崩】
缓存雪崩,是指在某一个时间段,缓存集中过期失效,突然给数据库产生了巨大的压力,甚至击垮数据库的情况。
产生雪崩的原因之一,比如马上就要到双十一零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
解决方案:对不用的数据使用不同的失效时间,或者采用失效时间加上随机因子。
例如:做电商项目的时候,一般是采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。
小结:缓存穿透是指key不存在情况下,缓存击穿是指击穿 单个热点key失效的在并发的查询的情况下,缓存雪崩是指多个key失效的情况下(在某一个时间段,缓存集中过期失效)。
9. Redis哨兵原理,分片,平滑迁移,缩容扩容...
等待更新ing...