文章目录
redis环境搭建
redis数据结构
String类型、Hsh类型、List类型、Set类型 、Sorted-Sets
String类型
String是redis最基本的类型,一个key对应一个value,sring类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象, Sring类型是Redis最基本的数据类型,一个键最大能存储512MB
[root@location bin]# ./redis-cli
127.0.0.1:6379> auth root
OK
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> get name
"zhangsan"
Hash类型
我们可以将Redis中的Hash类型看成具有<key,<key1,value>>,其中同一个key可以有多个不同key值的<key1,value>,所以该类型非常适合于存储值对象的信息。如Username、Password和Age等。如果Hash中包含很少的字段,那么该类型的数据也将仅占用很少的磁盘空间。
127.0.0.1:6379> HMSET fristHash1 wangwu 20
OK
127.0.0.1:6379> HGETALL fristHash1
1) "wangwu"
2) "20"
127.0.0.1:6379>
List类型
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
LRANGE firstPush 0 10 获取的时候需要写上范围
127.0.0.1:6379> LPUSH firstPush zhangsan lisi wanwu ermazi
(integer) 4
127.0.0.1:6379> LRANGE firstPush
(error) ERR wrong number of arguments for 'lrange' command
127.0.0.1:6379> LRANGE firstPush 0 10
1) "ermazi"
2) "wanwu"
3) "lisi"
4) "zhangsan"
127.0.0.1:6379>
Redis 集合(Set)
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
SADD nameSet zhangsan zhangsan lisi wanwu
SMEMBERS nameSet
127.0.0.1:6379> SADD nameSet zhangsan zhangsan lisi wanwu
(integer) 3
127.0.0.1:6379> SMEMBERS nameSet
1) "lisi"
2) "wanwu"
3) "zhangsan"
127.0.0.1:6379>
Redis 有序集合(sorted set)
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
ZADD sortSet 1 redis
ZRANGE sortSet 0 10 WITHSCORES
Redis持久化机制
全量同步与增量同步的区别
全量同步:就是每天定时(避开高峰期)或者采用一个周期实现将数据拷贝到一个地方也就是Rdb存储。
增量同步:比如采用对行为的操作实现对数据的同步,也就是AOF。
全量与增量的比较:增量同步比全量同步更加消耗服务器的内存,但是能够更加的保证数据的同步。
RDB与AOF实现持久化的区别
Redis提供了两种持久化的机制,分别为RDB、AOF实现,RDB采用定时(全量)持久化机制,但是服务器因为某种原因宕机后可能数据会丢失,AOF是基于数据日志操作实现的持久化,所以AOF采用增量同步的方案。
Redis已经帮助我默认开启了rdb存储。
Redis的RDB与AOF同步配置
RDB
Redis默认采用rdb方式实现数据的持久化,以快照的形式将数据持久化到磁盘的是一个二进制的文件dump.rdb, 在redis.conf文件中搜索“dump.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内存快照。
Set(包含增加和覆盖)、del
Set name yushengjun
Set name mayikt
Del name
Aof
在Redis的配置文件中存在三种同步方式,它们分别是:
appendfsync always #每次有数据修改发生时都会****写入AOF文件,能够保证数据不丢失,但是效率非常低。
appendfsync everysec #每秒钟同步一次,**可能会丢失1s内的数据,但是效率非常高。
appendfsync no #从不同步。高效但是数据不会被持久化。
直接修改redis.coonf中 appendonly yes
建议最好还是使用everysec** 既能够保证数据的同步、效率也还可以。
Aof是以执行命令的形式实现同步
SpringBoot整合Redis
创建一个maven项目,添加jar
<dependencies>
<!-- 集成commons工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- 集成lombok 框架 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.30</version>
</dependency>
<!-- SpringBoot-整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
创建 application.yml
spring:
redis:
host: 192.168.198.15
password: root
port: 6379
#设置存入第二个库
database: 1
创建启动类
package com.oyzk;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Kingkang
* @title RedisApplication springboot启动类
* @create 2022/12/2
**/
@SpringBootApplication
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class);
}
}
创建redis 工具类
package com.oyzk.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author Kingkang
* @title RedisUtils redis 工具类
* @create 2022/12/2
**/
@Component
public class RedisUtils {
/**
* 获取我们的redis模板
*/
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void setString(String key,String value){
setString(key,value,null);
}
/**
*为了防止有的人把redis当成数据用,设置有效期
* @param key
* @param value
* @param timeout 超时时间
*/
public void setString(String key,String value,Long timeout){
//设置key和value
stringRedisTemplate.opsForValue().set(key,value);
if (timeout!=null){
//通过key设置过期时间
stringRedisTemplate.expire(key,timeout, TimeUnit.SECONDS);
}
}
public String getString(String key){
return stringRedisTemplate.opsForValue().get(key);
}
}
创建UserEntity
package com.oyzk.enity;
import lombok.Data;
import java.io.Serializable;
/**
* @author Kingkang
* @title UserEntity
* @create 2022/12/2
**/
@Data
public class UserEntity implements Serializable {
private Long userId;
private String userName;
}
创建 IndexController
package com.oyzk.controller;
import com.alibaba.fastjson.JSONObject;
import com.oyzk.enity.UserEntity;
import com.oyzk.utils.RedisTemplateUtils;
import com.oyzk.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Kingkang
* @title IndexController2
* @create 2022/12/2
**/
@RestController
public class IndexController{
@Autowired
private RedisUtils redisUtils;
@RequestMapping("/setRedisKey")
public String setRedisKey(UserEntity userEntity){
redisUtils.setString("userEntity", JSONObject.toJSONString(userEntity));
return "存储成功";
}
@RequestMapping("/getRedisKey")
public UserEntity getRedisKey(String key) {
String userEntityJson = redisUtils.getString(key);
UserEntity userEntity = JSONObject.parseObject(userEntityJson, UserEntity.class);
return userEntity;
}
}
测试
http://localhost:8080/setRedisKey?userId=1&userName
http://localhost:8080/getRedisKey?key=userEntity
使用redis二进制形式存储对象
创建一个 RedisTemplateUtils
package com.oyzk.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @author Kingkang
* @title RedisTemplateUtils
* @create 2022/12/2
**/
@Component
public class RedisTemplateUtils {
@Resource
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){
//设置key和value
redisTemplate.opsForValue().set(key,value);
if (timeOut!=null){
//通过key设置过期时间
redisTemplate.expire(key,timeOut, TimeUnit.SECONDS);
}
}
public Object getObject(String key){
return redisTemplate.opsForValue().get(key);
}
}
更新IndexController
package com.oyzk.controller;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.oyzk.enity.UserEntity;
import com.oyzk.utils.RedisTemplateUtils;
import com.oyzk.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Kingkang
* @title IndexController
* @create 2022/12/2
**/
@RestController
public class IndexController {
@Autowired
private RedisUtils redisUtils;
@Autowired
private RedisTemplateUtils redisTemplateUtils;
@RequestMapping("/setRedisKey1")
public String setRedisKey(UserEntity userEntity){
// redisUtils.setString("userEntity", JSONObject.toJSONString(userEntity));
//
redisTemplateUtils.setObject("userEntity",userEntity);
return "存储成功";
}
@RequestMapping("/getRedisKey1")
public UserEntity getRedisKey(String key){
// String userEntityJson = redisUtils.getString("userEntity");
// UserEntity userEntity = JSONObject.parseObject(userEntityJson, UserEntity.class);
// return userEntity;
return (UserEntity)redisTemplateUtils.getObject(key);
}
}
测试
设置值,
http://localhost:8080/setRedisKey1?userId=2&userName=wangwu
http://localhost:8080/getRedisKey1?key=userEntity
MySQL与Redis一致性解决同步问题
开启@EnableCaching
引入jar
<!--mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
application.yml文件加入数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
urL: jdbc:mysql://127.0.0.1:3306/frame?serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=false
username: root
password: root
创建entity
package com.oyzk.enity;
import lombok.Data;
import java.io.Serializable;
/**
* @author Kingkang
* @title PeopleEntity
* @create 2022/12/2
**/
@Data
public class PeopleEntity implements Serializable {
private int id;
private String name ;
private Double money;
}
创建PeopleController
package com.oyzk.controller;
import com.oyzk.enity.PeopleEntity;
import com.oyzk.mapper.PeopleMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author Kingkang
* @title PeopleController
* @create 2022/12/2
**/
@RestController
public class PeopleController {
@Autowired
private PeopleMapper peopleMapper;
/**
* redis 缓存下的cacheNames, key 'findPeopleAll' key一定要加双引号
* @return
*/
@RequestMapping("/findPeopleAll")
@Cacheable(cacheNames = "people",key = "'findPeopleAll'")
public List<PeopleEntity> findPeopleAll(){
return peopleMapper.findPeopleAll();
}
}
测试
http://localhost:8080/findPeopleAll
那怎么去认证是否生效呢?
把数据库里面的值手动的改变,再去查询,会发现值是没有变的
解决方式1:直接清除Redis的缓存,重新读取数据库即可
再去请求,就已经变掉了
假如我项目里有很多个这样的,所以这种方式比较笨,临时用用可以
方式2:使用mq异步订阅mysql binlog实现增量同步
一般都是使用mq去异步订阅,
方式3:使用alibaba的canal框架
canal底层也是使用mq异步订阅实现的
redis六种淘汰策略
将Redis用作缓存时,如果内存空间用满,就会自动驱逐老的数据。
noeviction
官方解释:当内存使用达到阈值的时候,所有引起申请内存的命令会报错。
自己理解:当内存使用达到阈值的时候,执行命令直接报错
allkeys-lru
官方解释:在主键空间中,优先移除最近未使用的key。(推荐)
自己理解:在所有的key中,优先移除最近未使用的key。(推荐)
volatile-lru
官方解释:在设置了过期时间的键空间中,优先移除最近未使用的key。
allkeys-random
官方解释:在主键空间中,随机移除某个key。
volatile-random
官方解释:在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl
官方解释:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。
如何配置Redis淘汰策略
在redis.conf文件中,
设置Redis 内存大小的限制,我们可以设置maxmemory ,当数据达到限定大小后,会选择配置的策略淘汰数据
比如:maxmemory 300mb。
通过配置
设置redis的淘汰策略。比如:maxmemory-policy volatile-lru,然后保存,重启就可以生效了
redis中的自动过期机制并回调,实现订单30分钟有效期
实现需求:处理订单过期的自动取消,比如下单30分钟,未支付自动更改订单状态
解决问题方案:
- 定时任务,30分钟后检查该笔订单是否已经支付。
- 根据key有效期事件回调实现。(这是要演示的)
原理: - 创建订单的时候绑定一个订单token存放在redis中(有效期只有30分钟)key=token value=订单id
- 对改key绑定过期事件回调,执行我们的回调方法传递:token
实现
第一步、配置redis.conf,以后重启redis,防止不生效
当我们的key失效时,可以执行我们的客户端回调监听的方法。需要在Redis中配置:
notify-keyspace-events “Ex”
重启redis并关闭防火墙
[root@location bin]# ps -aux |grep redis
root 1406 0.2 0.8 163148 8392 ? Rsl 09:51 0:17 ./redis-server *:6379
root 1866 0.0 0.0 112824 988 pts/0 S+ 11:46 0:00 grep --color=auto redis
[root@location bin]# kill -9 1406
[root@location bin]# ./redis-server ./redis.conf
[root@location bin]# systemctl stop firewalld
[root@location bin]#
关闭防火墙:systemctl stop firewalld
开启防火墙:ystemctl start firewalld
第二步、springboot整合key失效监听
1、数据库表
CREATE TABLE `order_number` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_name` varchar(255) DEFAULT NULL,
`order_status` int(11) DEFAULT NULL,
`order_token` varchar(255) DEFAULT NULL,
`order_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8;
2、创建OrderEntity
package com.oyzk.enity;
import lombok.Data;
/**
* @author Kingkang
* @title OrderEntity
* @create 2022/12/3
**/
@Data
public class OrderEntity {
public OrderEntity() {
}
private Long id;
private String orderName;
/**
* 0 待支付 1 已经支付
*/
private Integer orderStatus;
private String orderToken;
private String orderId;
public OrderEntity(Long id, String orderName, String orderId, String orderToken) {
this.id = id;
this.orderName = orderName;
this.orderId = orderId;
this.orderToken = orderToken;
}
}
3、创建OrderMapper
package com.oyzk.mapper;
import com.oyzk.enity.OrderEntity;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
/**
* @author Kingkang
* @title OrderMapper
* @create 2022/12/3
**/
public interface OrderMapper {
@Insert("insert into order_number values (null,#{orderName},0,#{orderToken},#{orderId})")
int insertOrder(OrderEntity OrderEntity);
@Select("SELECT ID AS ID ,order_name AS ORDERNAME ,order_status AS orderstatus,order_token as ordertoken,order_id as orderid FROM order_number\n" +
"where order_token=#{orderToken};")
OrderEntity getOrderNumber(String orderToken);
@Update("update order_number set order_status=#{orderStatus} where order_token=#{orderToken};")
int updateOrderStatus(String orderToken, Integer orderStatus);
}
###### 4、创建RedisListenerConfig
package com.oyzk.listener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
/**
* @author Kingkang
* @title RedisListenerConfig
* @create 2022/12/3
**/
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory){
RedisMessageListenerContainer container=new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
5、创建RedisKeyExpirationListener
package com.oyzk.listener;
import com.oyzk.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
/**
* @author Kingkang
* @title RedisKeyExpirationListener
* @create 2022/12/3
**/
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
* Redis失效事件 key
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
String expiraKey = message.toString();
System.out.println("进来了,expirakey:"+expiraKey+",失效了");
}
}
6、重启项目,并关闭linux的里的防火墙,
###### 7、在linux的操作redis,
set userName zhangsan ex 5 -设置一个名字,过期时间为5秒,五秒后,就会进入我们后台写入的一个监听方法
[root@location bin]# ./redis-cli
127.0.0.1:6379> auth root
OK
127.0.0.1:6379> set userName zhangsan ex 5
OK
127.0.0.1:6379>
这只是一个简单的实现,
8、 创建OrderController
redisUtils之前写过,在这里贴一下
package com.oyzk.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author Kingkang
* @title RedisUtils redis 工具类
* @create 2022/12/2
**/
@Component
public class RedisUtils {
/**
* 获取我们的redis模板
*/
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void setString(String key,String value){
setString(key,value,null);
}
/**
*为了防止有的人把redis当成数据用,设置有效期
* @param key
* @param value
* @param timeout 超时时间
*/
public void setString(String key,String value,Long timeout){
//设置key和value
stringRedisTemplate.opsForValue().set(key,value);
if (timeout!=null){
//通过key设置过期时间
stringRedisTemplate.expire(key,timeout, TimeUnit.SECONDS);
}
}
public String getString(String key){
return stringRedisTemplate.opsForValue().get(key);
}
}
package com.oyzk.controller;
import com.oyzk.enity.OrderEntity;
import com.oyzk.mapper.OrderMapper;
import com.oyzk.utils.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
/**
* @author Kingkang
* @title OrderController
* @create 2022/12/3
**/
@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 orderId = System.currentTimeMillis();
//2、将我们的token存放到redis 中
redisUtils.setString(orderToken,orderId+"",10l);
OrderEntity orderEntity = new OrderEntity(null, "测试订单有效期并且回调", orderId + "", orderToken);
return orderMapper.insertOrder(orderEntity)>0?"success":"fail";
}
}
9、 需要在我们RedisKeyExpirationListener监听类里面,完善一下回调方法
package com.oyzk.listener;
import com.oyzk.enity.OrderEntity;
import com.oyzk.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author Kingkang
* @title RedisKeyExpirationListener
* @create 2022/12/3
**/
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
* 待支付
*/
private static final Integer ORDER_STAYPAY = 0;
/**
* 失效
*/
private static final Integer ORDER_INVALID = 2;
@Resource
private OrderMapper orderMapper;
/**
* Redis失效事件 key
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
String expiraKey = message.toString();
System.out.println("进来了,expirakey:"+expiraKey+",失效了");
OrderEntity orderNumber = orderMapper.getOrderNumber(expiraKey);
if(orderNumber==null){
return;
}
//获取订单状态
Integer orderStatus = orderNumber.getOrderStatus();
//如果订单状态为待支付的情况下,直接将该订单修改为已超时
if(orderStatus.equals(ORDER_STAYPAY)){
orderMapper.updateOrderStatus(expiraKey,ORDER_INVALID);
//库存在加上1
}
}
}
10、 测试
http://localhost:8080/addOrder
10秒后进入我们的监听的方法
到这里自动过期机制并回调就已ok了。
Redis事务操作
Multi 开启事务
EXEC 提交事务
Watch 可以监听一个或者多个key,在提交事务之前是否有发生了变化 如果发生边了变化就不会提交事务,没有发生变化才可以提交事务 版本号码 乐观锁
watch name
multi
set name zhangsan
exec
Discard 取消提交事务
注意:Redis官方是没有提供回滚方法, 值提供了取消事务。
注意:Redis官方是没有提供回滚方法, 值提供了取消事务。
思考:Redis与Mysql中的事务有那些区别?
redis事务没有隔离性,mysql反之,
在redis里开启事务以后,其它线程依旧是可以对我的key做操作
思考:取消事务跟回滚有什么区别呢?
mysql开启了事务,对该行数据上行锁
commit可以提交
回滚:对事务取消和行锁都会撤销
redis没有回滚 单纯的取消事务(不提交事务) 不上锁
事务演示
这是时候我们通过key去拿是拿不到值的
进行提交
提交后,再查就可以查询到了
事务 演示同时对两个key进行做操作
两边同时操作同一key
第一个连接开启事务并对zhangsan这个key进行赋值
第二连接也对zhangsan这个key做操作,也是可以是操作成功
第一个连接进行提交并查询,通过keyzhangsan查询
第二个进行提交:
这时候我在回到第一个连接时,通过zhangsankey再去查,你会发现值以及变掉了
总结
在redis中使用multi对key开启事务,其它的线程开始也可以对该key执行set操作的,所以数据不安全,解决方案:我们可以用Watch
watch操作
第一个连接 multi lisi key
第二连接watch
第一个连接进行提交
第二连接watch再进行提交,就会报错
通过这个这个key去拿,发现已经有值了
总结
所以,我们这时就可以通过watch进行监听key有没有发生变化,如果有,就不能提交,没有就提交
Redis实现分布式锁
什么分布式锁? 举个场景
本地锁:在多个线程中,保证只有一个线程执行(线程安全的问题)
分布锁:在分布式中,保证只有一个jvm执行(多个jvm线程安全问题)
如果我们服务器是集群的时候,定时任务可能会重复执行 可以采用分布式锁解决
Redis实现分布式锁基于SetNx命令,因为在Redis中key是保证是唯一的。所以当多个线程同时的创建setNx时,只要谁能够创建成功谁就能够获取到锁。
Set 命令 每次set时,可以修改原来旧值;
SetNx命令 每次SetNx检查该 key是否已经存在,如果已经存在的话不会执行任何操作。返回为0 如果已经不存在的话直接新增该key。1:新增key成功 0 失败
获取锁的时候:当多个线程同时创建SetNx k,只要谁能够创建成功谁就能够获取到锁。
释放锁:可以对该key设置一个有效期可以避免死锁的现象。
基于redis实现分布式锁的思路
1、获取锁
多个不同的jvm同时创建一个相同的标记使用setnx命令,因为(全局唯一的)redisKey 必须保证是唯一的,只要谁能够创建成功谁就能够获取锁
SetNx**命令 每次SetNx检查该 key是否已经存在,如果已经存在的话不会执行任何操作。返回为0 如果已经不存在的话直接新增该key。1:新增key成功 0 失败
2、释放锁
对我们的redis的key设置一个有效期(或者主动删除该key) 可以灵活的自动的释放该全局唯一的标记,其他的jvm重新进入到获取锁资源。
3、超时锁(没有获取锁、已经获取锁)
等待获取锁的超时时间
已经获取到锁 锁的有效期5s
Zookeeper实现分布式锁思路
Zookeeper实现分布式锁核心采用临时节点+事件通知,因为zk节点路径是保证全局唯一的,当多个线程同时创建该临时节点,只要谁能够创建成功谁就能够获取到锁。
获取锁:当多个线程同时创建该临时节点,只要谁能够创建成功谁就能够获取到锁。
释放锁:关闭当前Session连接,自动的删除当前的zk节点路径,其他线程重新进入到获取锁阶段。
分布式锁的应用场景有那些
1、分布式任务调度平台保证任务的幂等性
2、分布式全局id 的生成
分布式锁实现方案:
- 基于数据库方式实现
- 基于Zk方式实现 采用临时节点+事件通知
- 基于Redis方式实现 setnx 方式
Redis分布式锁核心代码(仅限于单机版本)
创建maven项目,添加jar
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!-- 集成commons工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
</dependencies>
创建RedisUtil
package com.oyzk.utils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* @author Kingkang
* @title RedisUtil redis工具类
* @create 2022/12/5
**/
public class RedisUtil {
private static String IP = "192.168.198.15";
//Redis的端口号
private static int PORT = 6379;
//可用连接实例的最大数目,默认值为8;
//如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
private static int MAX_ACTIVE = 100;
//控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。
private static int MAX_IDLE = 20;
//等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;
private static int MAX_WAIT = 3000;
private static int TIMEOUT = 3000;
//在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
private static boolean TEST_ON_BORROW = true;
//在return给pool时,是否提前进行validate操作;
private static boolean TEST_ON_RETURN = true;
private static JedisPool jedisPool = null;
/**
* redis过期时间,以秒为单位
*/
public final static int EXRP_HOUR = 60 * 60; //一小时
public final static int EXRP_DAY = 60 * 60 * 24; //一天
public final static int EXRP_MONTH = 60 * 60 * 24 * 30; //一个月
/**
* 初始化Redis连接池
*/
private static void initialPool() {
try {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(MAX_ACTIVE);
config.setMaxIdle(MAX_IDLE);
config.setMaxWaitMillis(MAX_WAIT);
config.setTestOnBorrow(TEST_ON_BORROW);
jedisPool = new JedisPool(config, IP, PORT, TIMEOUT, "root");
} catch (Exception e) {
//logger.error("First create JedisPool error : "+e);
e.getMessage();
}
}
/**
* 在多线程环境同步初始化
*/
private static synchronized void poolInit() {
if (jedisPool == null) {
initialPool();
}
}
/**
* 同步获取Jedis实例
*
* @return Jedis
*/
public synchronized static Jedis getJedis() {
if (jedisPool == null) {
poolInit();
}
Jedis jedis = null;
try {
if (jedisPool != null) {
jedis = jedisPool.getResource();
}
} catch (Exception e) {
e.getMessage();
// logger.error("Get jedis error : "+e);
}
return jedis;
}
/**
* 释放jedis资源
*
* @param jedis
*/
public static void returnResource(final Jedis jedis) {
if (jedis != null && jedisPool != null) {
jedisPool.returnResource(jedis);
}
}
public static Long sadd(String key, String... members) {
Jedis jedis = null;
Long res = null;
try {
jedis = getJedis();
res = jedis.sadd(key, members);
} catch (Exception e) {
//logger.error("sadd error : "+e);
e.getMessage();
}
return res;
}
}
创建RedisLock
package com.oyzk.utils;
import redis.clients.jedis.Jedis;
import sun.plugin.javascript.JSClassLoader;
import java.util.UUID;
/**
* @author Kingkang
* @title RedisLock
* @create 2022/12/5
**/
public class RedisLock {
/**
*
* @param lockKey 在redis中创建的key值
* @param notLockTime 尝试获取锁的超时时间
* @param timeOut 锁的有效期,防止死锁的产生
* @return 返回lock成功值
*/
public String getLock(String lockKey,int notLockTime,int timeOut){
//获取redis 连接
Jedis jedis = RedisUtil.getJedis();
//计算我们尝试获取锁的超时时间
Long endTime=System.currentTimeMillis()+ notLockTime;
//当前系统时间小于endTime说明获取锁没有超时 继续循环 否则退出循环
while (System.currentTimeMillis()<endTime){
String lockValue = UUID.randomUUID().toString();
//当多个不同的jvm同时创建一个相同的rediskey 只要谁能够创建成功谁就能获取锁
if(jedis.setnx(lockKey,lockValue)==1){
//设置锁的有效期
jedis.expire(lockKey,timeOut/1000);
return lockValue;
//退出循环
}
//否则情况下, 继续循环
}
try {
if(jedis!=null){
jedis.close();
}
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/**
* 释放锁
* @return
*/
public boolean unLock(String lockey,String lockValue){
//获取redis连接
Jedis jedis = RedisUtil.getJedis();
try {
//判断获取锁的值,如果通过key返回来的值与要删除的值一致,说明删除的是自己,防止误删除
if(lockValue.equals(jedis.get(lockey))){
return jedis.del(lockey)>0?true:false;
}
}catch (Exception e){
e.printStackTrace();
} finally {
if(jedis!=null){
jedis.close();
}
}
return false;
}
/**
* 获取锁
* 执行业务
* 释放锁
*/
}
创建测试
package com.oyzk.service;
import com.oyzk.utils.RedisLock;
import org.apache.commons.lang3.StringUtils;
/**
* @author Kingkang
* @title OrderService
* @create 2022/12/5
**/
public class OrderService {
private static final String LOCKKEY="pro_orderId";
public static void testLoack(){
RedisLock redisLock = new RedisLock();
String lockValue = redisLock.getLock(LOCKKEY, 5000,5000);
if (StringUtils.isEmpty(lockValue)){
System.out.println(Thread.currentThread().getName()+",获取锁失败了,");
}
//执行我们的业务逻辑
System.out.println(Thread.currentThread().getName()+",获取锁成功了,"+lockValue);
//释放锁 加上有效期后,就算没有释放锁也没有关系
// redisLock.unLock(LOCKKEY,lockValue);
}
public static void main(String[] args) {
testLoack();
}
/**
* 尝试获取锁为什么次数限制?
* 如果我们业务逻辑5s内没有执行完毕呢?
*
* 分场景:
* 1、锁的超时时间根据业务场景来预估
* 2、可以自己延迟锁的时间
* 3、在提交事务 的时候检查锁是否已经超时 如果已经超时则回滚(手动回滚) 否则提交
*
* 仅限于单击版本
*/
}
redis集群高可用环境
redis主从复制
基本概念:
单个Redis如果因为某种原因宕机的话,可能会导致Redis服务不可用,可以使用主从复制实现一主多从,主节点负责写的操作,从节点负责读的操作,主节点会定期将数据同步到从节点中,保证数据一致性的问题。
第一步、 准备三台linux,里面都安装了redis
我这里是打算是 redis1 为主,redis2、redis3为从
第二步、配置redis.conf
redis1不用动,
配置redis2的redis.conf
#编辑文件
vi /usr/redis7/bin/redis.conf
相关配置Redis.conf
进入redis.conf
搜索
/# replicaof <masterip> <masterport>
slaveof 192.168.198.15 6379
masterauth root
配置好以后,关闭防火墙:
systemctl stop firewalld
重启redis
[root@location bin]# ps aux|grep redis
root 1529 0.2 0.8 163148 8424 ? Ssl 15:56 0:00 ./redis-server *:6379
root 1549 0.0 0.0 112824 988 pts/0 R+ 16:00 0:00 grep --color=auto redis
[root@location bin]# kill -9 1529
进入redis,查看信息 info replication
[root@location bin]# ./redis-cli
127.0.0.1:6379> auth root
OK
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:192.168.198.15
master_port:6379
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_read_repl_offset:98
slave_repl_offset:98
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:f957cee571628bcbd0ef7a0a41b78852d17a335c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:98
说明redis2已经配置成功,
测试
在redis1主节点负责写,从节点redis2只能是读,说明是对的
测试redis2进行写报错,因为从节点只能读
在redsi2执行get username ,获取redis1新增的username,是可以获取
总结
测试完毕,说明redis1和redis2主从关系已经配置完毕
接下来配置redis3,
注意:该主从同步方式存在 如果从节点非常多的话,会导致对主节点同步多个从节点压力非常大
可以采用树状类型解决该问题
那如果采用树状类型的话,redis3配置的ip应该是redis2的信息(ip port pwd),redis3配置就不演示,附一张图:
主从复制数据同步的过程
1.Redis从节点向主节点建立socket连接
2.Redis采用全量或者增量的形式将数据同步给从节点
从Redis2.8版本以后 过程采用增量和全量同步
全量复制:一般用于在初次的复制场景(从节点与主节点一次建立)
增量复制:网络出现问题,从节点再次连接主节点时,主节点补发缺少的数据,每次数据增量同步
主从复制存在那些缺陷
如果主节点存在了问题,整个Redis环境是不可以实现写的操作,需要人工更改配置变为主操作
如何解决该问题:使用哨兵机制可以帮助解决Redis集群主从选举策略。
主从哨兵机制
Redis的哨兵机制就是解决我们以上主从复制存在缺陷(选举问题),解决问题保证我们的Redis高可用,实现自动化故障发现与故障转移。
哨兵机制原理
1、哨兵机制每个10s时间只需要配置监听我们的主节点就可以获取当前整个Redis集群的环境列表,采用info 命令形式。
2、哨兵不建议是单机的,最好每个Redis节点都需要配置哨兵监听。
3、哨兵集群原理是如何:多个哨兵都执行同一个主的master节点,订阅到相同都通道,有新的哨兵加入都会向通道中发送自己服务的信息,该通道的订阅者可以发现新哨兵的加入,随后相互建立长连接。
4、Master的故障发现 单个哨兵会向主的master节点发送ping的命令,如果master节点没有及时的响应,哨兵会认为该master节点为“主观不可用状态”会发送给其他都哨兵确认该Master节点是否不可用,当前确认的哨兵节点数>=quorum(可配置),会实现重新选举。
实现哨兵机制1-树状型
第一步、copy 哨兵的配置文件
从redis-7.0.5copy到redis7
cp /usr/redis-7.0.5/sentinel.conf /usr/redis7/bin
cd /usr/redis7/bin
第二步、编辑sentinel.conf
vi sentinel.conf
三台redis都要修改 daemonize
意思是:后台启动
daemonize yes
修改 redis2的哨兵sentinal的mymaster
配置redis2
2 根据实际情况来,如果哨兵是三个建议是2,
sentinel monitor mymaster 192.168.198.15 6379 2
sentinel auth-pass mymaster root
配置redis1
sentinel monitor mymaster 192.168.198.15 6379 2
sentinel auth-pass mymaster root
配置redis3
sentinel monitor mymaster 192.168.198.15 6379 2
sentinel auth-pass mymaster root
第二步、启动哨兵
启动哨兵之前需要把防火墙关闭
启动命令
./redis-sentinel ./sentinel.conf
[root@location bin]# ls
dump.rdb redis-benchmark redis-check-aof redis-check-rdb redis-cli redis.conf redis-sentinel redis-server sentinel.conf
[root@location bin]# ./redis-sentinel ./sentinel.conf
第三步、测试
现在我们可以看一下,主从关系
[root@location bin]# ./redis-cli
127.0.0.1:6379> auth root
OK
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.198.16,port=6379,state=online,offset=37351,lag=1
master_failover_state:no-failover
master_replid:f957cee571628bcbd0ef7a0a41b78852d17a335c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:37780
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:37780
127.0.0.1:6379>
手动的把redis1主节点关掉
[root@location bin]# ps -aux|grep redis
root 1656 0.2 0.8 163148 8216 ? Ssl 16:02 0:12 ./redis-server *:6379
root 1746 0.5 0.8 163272 8684 ? Ssl 17:17 0:01 ./redis-sentinel *:26379 [sentinel]
root 1762 0.0 0.0 112824 984 pts/1 S+ 17:23 0:00 grep --color=auto redis
[root@location bin]# kill -9 1656
这时候就会在redis2和redis3进行选举了
查看关系
info replication
这时候你会发现redis2变成主节点了,此时redis2也可以写入了
重启redis1节点,此时redis1会变成从节点,
但是哨不能帮助我们解决数据同步的问题,所以要自己配置一下
修改redis.conf
slaveof 192.168.198.16 6379
masterauth root
重启redis1就可以解决
实现哨兵机制2
一个哨兵为主,其他都是跟随
第一步、还是copyredis哨兵配置文件
之前有配置过哨兵配置文件的,可以先删除,再进行copy
rm -rf sentinel.conf
cp /usr/redis-7.0.5/sentinel.conf /usr/redis7/bin
第二步、修改配置文件
sentinel.conf
以及redis.conf
# sentinel.conf
sentinel monitor mymaster 192.168.198.15 6379 1
sentinel auth-pass mymaster root
#redis.conf
# replicaof <masterip> <masterport>
slaveof 192.168.198.15 6379
masterauth root
第三步、重启哨兵
daemonize yes
[root@location bin]# ./redis-server ./redis.conf
[root@location bin]# ./redis-sentinel ./sentinel.conf
2049:X 06 Dec 2022 09:58:42.035 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2049:X 06 Dec 2022 09:58:42.036 # Redis version=7.0.5, bits=64, commit=00000000, modified=0, pid=2049, just started
2049:X 06 Dec 2022 09:58:42.036 # Configuration loaded
2049:X 06 Dec 2022 09:58:42.040 * Increased maximum number of open files to 10032 (it was originally set to 1024).
2049:X 06 Dec 2022 09:58:42.040 * monotonic clock: POSIX clock_gettime
2049:X 06 Dec 2022 09:58:42.040 # Warning: Could not create server TCP listening socket *:26379: bind: Address already in use
2049:X 06 Dec 2022 09:58:42.040 # Failed listening on port 26379 (TCP), aborting.
[root@location bin]# ps -aux |grep redis
root 1969 0.3 0.8 163148 8908 ? Sl 09:51 0:01 ./redis-sentinel *:26379 [sentinel]
root 2044 0.2 0.8 165708 8596 ? Ssl 09:58 0:00 ./redis-server *:6379
root 2051 0.0 0.0 112824 984 pts/2 R+ 09:59 0:00 grep --color=auto redis
[root@location bin]#
第三步、访问,查看信息
[root@location bin]# ./redis-cli
127.0.0.1:6379> auth root
OK
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:192.168.198.15
master_port:6379
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_read_repl_offset:97721
slave_repl_offset:97721
master_link_down_since_seconds:-1
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:2
slave0:ip=192.168.198.16,port=6379,state=online,offset=97721,lag=1
slave1:ip=192.168.198.17,port=6379,state=online,offset=97721,lag=0
master_failover_state:no-failover
master_replid:14ef6bd979658c2d5c63677b64d32297fbc02234
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:97721
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:92587
repl_backlog_histlen:5135
127.0.0.1:6379>
第四步、测试,断掉主节点redis,让哨兵自动选举
此时选举完:查看
sentinel配置文件也会自动的变化
Redis Cluster集群
传统Redis集群存在那些问题
Redis哨兵集群模式,每个节点都保存全量同步数据,冗余的数据比较多;而在Redis Cluster模式中集群中采用分片集群模式,可以减少冗余数据,缺点就是构建该集群模式成本非常高,
传统RedisCluster集群的原理
Redis3.0开始官方推出了集群模式 RedisCluster,原理采用hash槽的概念,预先分配14884个卡槽,并且将该卡槽分配给具体服务的节点;通过key进行crc16(key)%14884 获取余数,余数就是对应的卡槽的位置,一个卡槽可以存放多个不同的key,从而将读或者写转发到该卡槽的服务的节点。 最大的有点:动态扩容、缩容。
RedisCluster集群模式环境搭建
redis.conf
[root@location usr]# mkdir rediscluster
[root@location usr]# cd rediscluster/
mkdir redis7000
mkdir redis7001
mkdir redis7002
mkdir redis7003
mkdir redis7004
mkdir redis7005
每个配置文件内容
daemonize yes #后台启动
protected-mode no ; ## 允许外部访问
port 7005 #修改端口号,从7000到7005
cluster-enabled yes #开启cluster,去掉注释
cluster-config-file 7000nodes.conf #自动生成
cluster-node-timeout 15000 #节点通信时间
dbfilename 7000dump.rdb #修改对应的rdb文件名建议:端口号+dump.rdb
logfile /usr/rediscluster/redis7005/redis.log
启动我们的redis
/usr/redis/bin/redis-server /usr/rediscluster/redis7000/redis.conf
/usr/redis/bin/redis-server /usr/rediscluster/redis7001/redis.conf
/usr/redis/bin/redis-server /usr/rediscluster/redis7002/redis.conf
/usr/redis/bin/redis-server /usr/rediscluster/redis7003/redis.conf
/usr/redis/bin/redis-server /usr/rediscluster/redis7004/redis.conf
/usr/redis/bin/redis-server /usr/rediscluster/redis7005/redis.conf
连接一个redis
/usr/redis/bin/redis-cli -h 192.168.198.15 -p 7000
set username zhangsan
(error) CLUSTERDOWN Hash slot not served 说明没有分配hash槽\
#分配卡槽
/usr/redis/bin/redis-cli --cluster create 192.168.198.15:7000 192.168.198.15:7001 192.168.198.15:7002 192.168.198.15:7003 192.168.198.15:7004 192.168.198.15:7005 --cluster-replicas 1
(建议最好使用服务器的ip地址搭建)
分配我们hash操作
Can I set the above configuration? (type ‘yes’ to accept): yes
说明卡槽分配成功
/usr/redis/bin/redis-cli -h 192.168.198.15 -p 7000
set username zhangsan
不会自动的重定向
修改为Redis的集群方式连接
/usr/redis/bin/redis-cli -h 192.168.198.15 -p 7000 -c
set username zhangsan
/usr/redis/bin/redis-cli --cluster help
这样的话就支持转发
#查看当前服务器集群节点
192.168.198.15:7000> cluster nodes
说明已经成功了
RedisCluster集群模式扩容节点
首先新增两台Redis节点 一主、一从;
/usr/redis/bin/redis-server /usr/rediscluster/redis7006/redis.conf
/usr/redis/bin/redis-server /usr/rediscluster/redis7007/redis.conf
案例:
/usr/redis/bin/redis-cli --cluster add-node 新增集群地址:端口 集群中已经存在的任意一台连接地址:端口
实战:
/usr/redis/bin/redis-cli --cluster add-node 192.168.198.15:7006 192.168.198.15:7000
可能出现的错误
[ERO]Node 192.168.198.15:7000 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0
解决办法: 重新启动我们所有的redis、rdb和aof文件能够产生冲突 其次 可以使用情况当前所有的内容的数据源
默认在集群中是为master节点;
扩容从节点: aebf0e9f947a8565cee1ec20c753acd5640450a6 id为7006节点的
/usr/redis/bin/redis-cli --cluster add-node 192.168.198.15:7007 192.168.198.15:7000 --cluster-slave --cluster-master-id aebf0e9f947a8565cee1ec20c753acd5640450a6
运行效果
查看
[root@location rediscluster]# /usr/redis/bin/redis-cli -h 192.168.198.15 -p 7000 -c
192.168.198.15:7000> cluster nodes
如果中有三台master节点,则每台卡槽为5462
如果中有四台master节点,则每台卡槽为4096
分配卡槽
/usr/redis/bin/redis-cli --cluster reshard 192.168.198.15:7000
然后回车,输入yes即可
[root@location rediscluster]# /usr/redis/bin/redis-cli -h 192.168.198.15 -p 7000 -c
192.168.198.15:7000> cluster nodes
192.168.198.15:7001> set mm mm
-> Redirected to slot [125] located at 192.168.198.15:7006
OK
192.168.198.15:7006> get mm
RedisCluster master宕机效果
如果master宕机之后,会自动的在从节点会顺利的选为主节点;当原来的主节点在恢复启动的时候会变为从节点;
[root@location rediscluster]# ps aux|grep redis
root 2403 0.6 1.1 163208 11464 ? Ssl 10:57 0:12 /usr/redis/bin/redis-server *:7000 [cluster]
root 2405 0.6 1.0 162696 10032 ? Ssl 10:57 0:11 /usr/redis/bin/redis-server *:7001 [cluster]
root 2410 0.5 0.8 156552 8100 ? Ssl 10:57 0:10 /usr/redis/bin/redis-server *:7002 [cluster]
root 2415 0.1 0.8 156552 8044 ? Ssl 10:57 0:03 /usr/redis/bin/redis-server *:7003 [cluster]
root 2420 0.1 0.8 156552 8040 ? Ssl 10:57 0:03 /usr/redis/bin/redis-server *:7004 [cluster]
root 2428 0.1 0.8 156552 8044 ? Ssl 10:57 0:03 /usr/redis/bin/redis-server *:7005 [cluster]
root 2442 1.0 0.8 156552 8044 ? Ssl 10:59 0:17 /usr/redis/bin/redis-server *:7006 [cluster]
root 2447 0.1 0.8 156552 8024 ? Ssl 10:59 0:03 /usr/redis/bin/redis-server *:7007 [cluster]
root 2509 0.0 0.0 112824 984 pts/1 S+ 11:27 0:00 grep --color=auto redis
[root@location rediscluster]# kill 2442
[root@location rediscluster]# /usr/redis/bin/redis-cli -h 192.168.198.15 -p 7000 -c
192.168.198.15:7000> cluster nodes
d784718c901f7645cbc937b6a0f1763410e6c9e7 192.168.198.15:7002@17002 master - 0 1672284515000 3 connected 12288-16383
af6b71a9d858b5d66fcebc9fe606be3920995ad2 192.168.198.15:7001@17001 master - 0 1672284515000 2 connected 6827-10922
90f8bc451e60c3e15c21536bd456eb736a8eec8a 192.168.198.15:7003@17003 slave d784718c901f7645cbc937b6a0f1763410e6c9e7 0 1672284514000 4 connected
a15a69faa459a3e2722066cc349febad9489ee46 192.168.198.15:7007@17007 master - 0 1672284517195 9 connected 0-1364 5461-6826 10923-12287
18de4c238658eae8676c6f9e5deea8b8f9b48146 192.168.198.15:7004@17004 slave f0ea588089e2c64d496c53d93be633291dadbf13 0 1672284516168 5 connected
d60f9b17b148f7af523bcc83a67af02dda9f87b6 192.168.198.15:7005@17005 slave af6b71a9d858b5d66fcebc9fe606be3920995ad2 0 1672284515141 6 connected
f0ea588089e2c64d496c53d93be633291dadbf13 192.168.198.15:7000@17000 myself,master - 0 1672284512000 1 connected 1365-5460
aebf0e9f947a8565cee1ec20c753acd5640450a6 192.168.198.15:7006@17006 master,fail - 1672284495959 1672284492000 8 disconnected
192.168.198.15:7000>
如果重新启动:7006,7006就会自动的变为从节点
RedisCluster集群模式缩容节点
/usr/redis/bin/redis-cli --cluster reshard 192.168.198.15:7000 --cluster-from
需要缩容的卡槽节点 --cluster-to 接受卡槽的节点 --cluster-slots 4096
/usr/redis/bin/redis-cli --cluster reshard 192.168.198.15:7000 --cluster-from a15a69faa459a3e2722066cc349febad9489ee46 --cluster-to f0ea588089e2c64d496c53d93be633291dadbf13 --cluster-slots 4096
然后我们可以查询之前7007节点新增的mm,显示已经在7000节点,说明已经缩容成功
redis支持RedisCluster集群
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
单机版Jedis
Jedis jedis = new Jedis("192.168.198.15", 7000);
String value = jedis.get("mm");
System.out.println("value:" + value);
jedis.close();
Redis支持集群版本
public class RedisCluster {
private static JedisCluster jedis;
static {
// 添加集群的服务节点Set集合
Set<HostAndPort> hostAndPortsSet = new HashSet<HostAndPort>();
// 添加节点
hostAndPortsSet.add(new HostAndPort("192.168.198.15", 7000));
hostAndPortsSet.add(new HostAndPort("192.168.198.15", 7001));
hostAndPortsSet.add(new HostAndPort("192.168.198.15", 7002));
hostAndPortsSet.add(new HostAndPort("192.168.198.15", 7003));
hostAndPortsSet.add(new HostAndPort("192.168.198.15", 7004));
hostAndPortsSet.add(new HostAndPort("192.168.198.15", 7005));
// Jedis连接池配置
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大空闲连接数, 默认8个
jedisPoolConfig.setMaxIdle(100);
// 最大连接数, 默认8个
jedisPoolConfig.setMaxTotal(500);
//最小空闲连接数, 默认0
jedisPoolConfig.setMinIdle(0);
// 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, 默认-1 设置2秒
jedisPoolConfig.setMaxWaitMillis(2000);
//对拿到的connection进行validateObject校验
jedisPoolConfig.setTestOnBorrow(true);
jedis = new JedisCluster(hostAndPortsSet, jedisPoolConfig);
}
public String getKye(String key) {
return jedis.get(key);
}
public static void main(String[] args) {
String value = new RedisCluster().getKye("mm");
System.out.println("value:" + value);
}
}
基于布隆过滤器解决缓存穿透问题
缓存穿透
产生的背景:
缓存穿透是指使用不存在的key进行大量的高并发查询,导致缓存无法命中,每次请求都要都要穿透到后端数据库查询,使得数据库的压力非常大,甚至导致数据库服务压死;
解决方案:
4.接口层实现api限流、用户授权、id检查等;
5.从缓存和数据库都取不到数据的话,一样将数据库空值放入缓存中,设置30s有效期避免使用同一个id对数据库攻击压力大;
布隆过滤器基本介绍
布隆过滤器适用于判断某个数据是否在集合中存在,不一定百分百准备, Bloom Filter基本实现原理采用位数组与联合函数一起实现;
基于布隆过滤器解决缓存穿透问题
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>22.0</version>
</dependency>
public class BlongTest {
/**
* 在布隆中存放100万条数据
*/
private static Integer size = 1000000;
public static void main(String[] args) {
BloomFilter<Integer> integerBloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, 0.01);
for (int i = 0; i < size; i++) {
integerBloomFilter.put(i);
}
// 从布隆中查询数据是否存在
ArrayList<Integer> strings = new ArrayList<>();
for (int j = size; j < size + 10000; j++) {
if (integerBloomFilter.mightContain(j)) {
strings.add(j);
}
}
System.out.println("误判数量:" + strings.size());
}
}
基于布隆过滤器解决Redis击穿问题
@RequestMapping("/getOrder")
public OrderEntity getOrder(Integer orderId) {
if (integerBloomFilter != null) {
if (!integerBloomFilter.mightContain(orderId)) {
System.out.println("从布隆过滤器中检测到该key不存在");
return null;
}
}
// 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;
}
@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";
}
布隆过滤器最大的问题:就是可能会存在一个误判的问题,如果向误判概率越低,则二进制数组会越大,同时也会非常占用空间
缓存穿透的问题?
缓存的穿透:指定使用一些不存在的key进行大量的查询Redis,导致无法命中
每次的请求都会要传查询到我们数据库;对我们的数据库的压力非常大;
1.对我们的服务接口api实现限流、用户授权、黑名单和白名单拦截;
2.从缓存和数据库都查询不到结果的话,一样将数据库空值结果缓存到Redis
中;设置30s的有效期避免使用同一个id对数据库攻击。
如果黑客真的在攻击的情况下,随机成id肯定是不一样的。
3.布隆过滤器
什么是布隆过滤器呢?
HashMap? 检测该key是否存在 原理:先计算对应的key存放到数组的
Index? O(1)
布隆过滤器 作用:
布隆过滤器适用于判断一个元素在集合中是否存在,但是可能会存在误判的问题。
实现的原理采用二进制向量数组和随机映射hash函数。
布隆过滤器为什么会产生冲突 ,会根据key计算hash值,可能与布隆过滤器中存放的元素hash产生冲突都是为1,布隆可能会产生误判可能存在。
如何解决这个问题:二进制数组长度设置比较大,可以减少布隆误判的概率。