最详细的Redis学习教程

一.安装步骤

方式一:

1.先将你下载好的文件放入到linux虚拟机

2.tar -zxvf redis-7.0.8.tar :解压文件

3.安装 gcc,(编译时会用到 yum install gcc)/apt-get install gcc

4.进入到解压目录中 make distclean && make

方式二:

step01-安装 gcc,编译时会用到 yum install gcc(centos的安装)/apt-get install gcc(ubantu的安装)

step02-远程下载redis压缩包或者在官网上下载好压缩包,进行解压下载
yum install wget    :安装wget
wget https://github.com.redis/redis/archive/ref/tags/7.0.5.tar.gz    :通过wget远程安装redis
tar -zxvf redis压缩包    :解压缩

step03-进入到redis解压缩好的文件夹中,进行编译运行
make distclean:清除上次的,重新编译
make:编译

step04-安装
make install

step05-安装完成之后,修改 redis.conf ,设置daemonzie yes ,这个表示让 redis 在后台运行

 step06-启动redis服务端

src/redis-server redis.conf

step07-进入redis的客户端

src/redis-cli

step08-Redis 开启远程连接,修改 redis.conf

保护模式(建议不修改,就修改下面两个指令):protectedmode no
#bind 127.0.0.1
requirepass 123

如果设置了密码了,再次进入到src/redis-cli时,需要去输入密码
auth 123(建议这种)
src/redis -a 123(不安全)

step09-如果远程还是不行,看看关防火墙没

systemctl stop firewalld.service
systemctl disable firewalld.service

二.安装完后使用命令进入redis

第一步:src/redis-server redis-conf        :设置redis-conf的配置,通过此配置文件进入到redis服务中去

第二步:src/redis-cli        :进入到redis的脚手架

注意点:redis的远程连接需要设定密码,安全校验
保护模式(建议不修改,就修改下面两个指令):protectedmode no
#bind 127.0.0.1
requirepass 123

如果设置了密码了,再次进入到src/redis-cli时,需要去输入密码
auth 123(建议这种)
src/redis -a 123(不安全)

三.redis五种数据类型命令

redis中,key都是字符串,我们说的不同的数据类型都是value
常用的5种数据结构:
- key-string:一个key对应一个值。
- key-hash:一个key对应一个Map。
- key-list:一个key对应一个列表。
- key-set:一个key对应一个集合。
- key-zset:一个key对应一个有序的集合。

1.String

key指的是参数名称
#1.  添加值,如果value中间有空格的,就需要加''来包裹
set key value

#2. 取值
get key

#3. 如果没有设置key,在key对应的value后,追加内容
append key value

#4. 自减命令(自减1),value值要是int
decr key

#5. 自增命令(自增1),value值要是int
incr key 

#6. 自增或自减指定数量,value值要是int
incrby key increment
decrby key increment

#7.自增小数/自减小数
incrbyfloat key increment
decrbyfloat key increment

#8.查看并删除key
getdel key

#9.查看剩下还有多少时间就过期,-1表示不会过期,-2表示过期了
ttl key

#10.获取并设置key的过期时间
getex key ex 时间
ex:表示秒
px:表示毫秒

#11.获取截取value字符串
getrange key start end
end如果是-1就是拿完整的

#12.查找两个key公共部分
lcs key1 key2

#13. 批量操作,批量设置和批量获取
mset key value [key value...]
mget key [key...]

#14. 设置值,如果当前key存在的话,就不覆盖,没变化(如果这个key不存在,和set命令一样,设置值)
setnx key value

#15. 设置值的同时,指定生存时间(每次向Redis中添加数据时,尽量都设置上生存时间)
setex key second value


#16.如果值开始就设置了,就只需要设置时间
setex key ex second
ex:表示秒
px:表示毫秒

#16. 查看value字符串的长度
strlen key

#17.获取当前的key的值,设置新的
getset key value

2.Hash

hash的value又是一个key-value形式的,key相当于存的是对象,value存的是对象的属性+值

 #1. 存储数据,key相当于存的是对象,field存的是对象的属性,value是属性的值,key可以有多个属性field
hset key field value

#2. 获取数据
hget key field

#3. 批量操作
hmset key field value [field value ...]
hmget key field [field ...]

#4. 自增(指定自增的值),当时设定field的值要是int
hincrby key field increment

#5. 设置值(如果key-field不存在,那么就正常添加,如果存在,什么事都不做)
hsetnx key field value

#6. 检查field是否存在
hexists key field 

#7. 删除key对应的field,可以删除多个,不能删除key
hdel key field [field ...]

#8. 获取当前hash结构中的全部field和value
hgetall key

#9. 获取当前hash结构中的全部field
hkeys key

#10. 获取当前hash结构中的全部value
hvals key

#11. 获取当前hash结构中field的数量
hlen key

#12.随机获取key的属性,count表示随机获取几个
hrandfield key count

3.List

像队列一样,可以进行左边进还是右边进,是左边出还是右边出,有序可重复

#1. 存储数据(从左侧插入数据,从右侧插入数据)
lpush key value [value ...]
rpush key value [value ...]

#2. 弹栈方式获取数据(左侧弹出数据,从右侧弹出数据)
lpop key
rpop key

#3. 追加存储数据(如果key不存在,什么事都不做,如果key存在,但是不是list结构,什么都不做)
lpushx key value
rpushx key value

#4. 修改数据(在存储数据时,指定好你的索引位置,覆盖之前索引位置的数据,index超出整个列表的长度,也会失败)
lset key index value

#5. 获取指定索引范围的数据(start从0开始,stop输入-1,代表最后一个,-2代表倒数第二个)
lrange key start stop

#6. 获取指定索引位置的数据,不会出队列
lindex key index

#7. 获取整个列表的长度
llen key

#8. 删除列表中的数据(他是删除当前列表中的count个value值,count > 0从左侧向右侧删除,count < 0从右侧向左侧删除,count == 0删除列表中全部的value)
lrem key count value

#9. 保留列表中的数据(保留你指定索引范围内的数据,超过整个索引范围被移除掉)
ltrim key start stop

#10. 将一个列表中最后的一个数据,插入到另外一个列表的头部位置
rpoplpush list1 list2

#11.插入一个元素,pivot是在哪个元素,
linsert key before|after pivot value

#12.获取范围内的所有元素
lrange key start stop

#13.移动key到另一个key,left和right的意思是从哪边出,从哪边进。移出只是一个元素
lmove key1 key2 left|right left |right

#14.移除几个key,left和right的意思是从哪边出,从哪边进,Count number是几个,Count必须写
lmpop keynumber key1 left|right Count number key2  left |right 

#15.查询指定元素的下边
lpos key element Count number

4.set

set:无序的,不可重复

#1. 存储数据,member是你的元素,可以有多个
sadd key member [member ...]

#2. 获取数据(获取全部数据)
smembers key
scard key :获取key的长度

#3. 随机获取一个数据(获取的同时,移除数据,count默认为1,代表弹出数据的数量)
spop key [count]
srandmember key [count] :随机获取多个

#4. 交集(取多个set集合交集)
sinter set1 set2 ...
sintercard numberkeys set1 set2 :交集的数量

#5. 并集(获取全部集合中的数据)
sunion set1 set2 ...

#6. 差集(获取多个集合中不一样的数据)从第一个key里面除去第二个key公共的部分
sdiff set1 set2 ...

# 7. 删除数据
srem key member [member ...]

# 8. 查看当前的set集合中是否包含这个值
sismember key member
smismember key members

# 9.除去两个key的公共部分,然后将剩下的存到第三个key上面去
sdiffstore key3 key1 key2

# 10.从一个key移动到另一个key
smove key1 key2 element

5.zset

有序的,可以用来做限流

#1. 添加数据(score必须是数值。member不允许重复的。)
zadd key score member [score member ...]

#2. 修改member的分数(如果member是存在于key中的,正常增加分数,如果memeber不存在,这个命令就相当于zadd)
zincrby key increment member

#3. 查看指定的member的分数
zscore key member

#4. 获取zset中数据的数量
zcard key

#5. 根据score的范围查询member数量
zcount key min max

#6. 删除zset中的成员
zrem key member [member...]

#7. 根据分数从小到大排序,获取指定范围内的数据(withscores如果添加这个参数,那么会返回member对应的分数)
zrange key start stop [withscores]

#8. 根据分数从大到小排序,获取指定范围内的数据(withscores如果添加这个参数,那么会返回member对应的分数)
zrevrange key start stop [withscores]

#9. 根据分数的返回去获取member(withscores代表同时返回score,添加limit,就和MySQL中一样,如果不希望等于min或者max的值被查询出来可以采用 ‘(分数’ 相当于 < 但是不等于的方式,最大值和最小值使用+inf和-inf来标识)
zrangebyscore key min max [withscores] [limit offset count]

#10. 根据分数的返回去获取member(withscores代表同时返回score,添加limit,就和MySQL中一样)
zrangebyscore key max min [withscores] [limit offset count]

四.Java代码实现redis

前言:Jedis,Resdisson(分布式锁),lettuce

redis的通信协议是:RESP(Redis Serialization Protocol)协议,java代码和redis直接的通信规则

1.Jedis方式操作redis

step01-依赖

<dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>



step02-连接测试

public class JedisDemo01 {
    public static void main(String[] args) {
        //创造连接
        Jedis jedis = new Jedis("139.159.210.107", 6379);
        //进行密码校验
        jedis.auth("123");
        String set = jedis.set("k1", "欧克");
        System.out.println(set);
        String s = jedis.get("k1");
        System.out.println(s);
        //关闭连接
        jedis.close();
    }
}

2.连接池测试

public class JedisDemo02 {
    public static void main(String[] args) {
        //配置Jedis连接池属性
        GenericObjectPoolConfig<Jedis> poolConfig = new GenericObjectPoolConfig<>();
        //最大连接数
        poolConfig.setMaxTotal(50);
        //最大空闲数
        poolConfig.setMaxIdle(30);
        //最小空闲数
        poolConfig.setMinIdle(10);
        //创建Jedis连接池
        JedisPool jedisPool = new JedisPool(poolConfig, "139.159.210.107", 6379, null, "123");
        //获取一个连接
        Jedis jedis = jedisPool.getResource();
        String set = jedis.set("k1", "欧克");
        System.out.println("set = " + set);
        String get = jedis.get("k1");
        System.out.println("get = " + get);
        //回收资源
        jedisPool.returnResource(jedis);
    }
}

通过try-with-resources来自动回收资源

public class JedisDemo02 {
    public static void main(String[] args) {
        //配置Jedis连接池属性
        GenericObjectPoolConfig<Jedis> poolConfig = new GenericObjectPoolConfig<>();
        //最大连接数
        poolConfig.setMaxTotal(50);
        //最大空闲数
        poolConfig.setMaxIdle(30);
        //最小空闲数
        poolConfig.setMinIdle(10);
        //创建Jedis连接池
        JedisPool jedisPool = new JedisPool(poolConfig, "139.159.210.107", 6379, null, "123");
        //获取一个连接
        try(Jedis jedis = jedisPool.getResource()){
             String set = jedis.set("k1", "欧克");
        	System.out.println("set = " + set);
        	String get = jedis.get("k1");
        	System.out.println("get = " + get);
        }
    }
}

3.封装连接池

1.接口类

public interface RedisCallBack {
    void run(Jedis jedis);
}

 2.封装类

public class Redis {
    JedisPool jedisPool;

    //初始化连接池
    public Redis() {
        GenericObjectPoolConfig<Jedis> poolConfig = new GenericObjectPoolConfig<>();

        poolConfig.setMaxTotal(50);
        poolConfig.setMaxIdle(30);
        poolConfig.setMinIdle(10);
        //创建Jedis连接池
        jedisPool = new JedisPool(poolConfig, "139.159.210.107", 6379, null, "123");

    }

    //连接一个jedis,执行操作,释放资源
    public void execute(RedisCallBack redisCallBack){
        Jedis jedis = jedisPool.getResource();
        redisCallBack.run(jedis);
        jedisPool.returnResource(jedis);
    }
}

3.函数型接口测试

public class Jedis_demo03 {
    public static void main(String[] args) {
        new Redis().execute(new RedisCallBack() {
            @Override
            public void run(Jedis jedis) {
                jedis.set("k1","欧克");
                System.out.println("jedis.get(\"k1\") = " + jedis.get("k1"));
            }
        });
    }
}

4.springboot方式操作redis

底层默认的是使用lettuce,被spring-data-redis封装了

 springboot提供了RedisAutoConfiguration这个自动配置类,当你配置文件中的信息写好了之后,提供Template类给你,当然他也要注入依赖

step01-依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

step02-配置文件信息

#redis服务地址
spring.redis.host=139.159.210.107
#redis密码
spring.redis.password=123
#redis端口号,默认为6379
spring.redis.port=6379
#redis数据库,默认为0
spring.redis.database=0

 step03-测试

可以自定义RedisTemplate,进行格式的转变

 

@SpringBootTest
class SpringDataRedisApplicationTests {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        //设置redis的类型
        ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
        ops.set("k1","我好想你");
        String s = ops.get("k1");
        System.out.println("s = " + s);
    }

        /**
     * RedisTemplate的key和value默认都是对象,即使存入的是字符串,也是当成对象。当对象序列化之后,才可以存入到Redis,所以这里k2最终存储完成后,不仅仅k2了,前面还有其他字符
     */
    @Test
    void test01(){
        ValueOperations ops = redisTemplate.opsForValue();
        Book book = new Book();
        book.setId(1);
        book.setName("潘权荣");
        book.setAge(18);
        ops.set("k2",book);
        Object k1 = ops.get("k2");
        System.out.println(k1);
    }

    //不会有前缀字符串,而是单纯的key
    @Test
    void test02(){
        ValueOperations ops = redisTemplate.opsForValue();
        ops.set("k1","欧克");
        Object k1 = ops.get("k1");
        System.out.println(k1);
    }
 
    /**
     * 之所以StringredisTemplate的key没有多余的前缀,是因为在redisTemplate的基础上修改了序列化方案
     * 我们可以输出以json的形式,方便我们阅读
     */
    @Test
    void test03(){
        //也可以自定义一个redisTemplate的配置类,因为当提供了redisTemplate就不会用系统自带的类
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        ValueOperations ops = redisTemplate.opsForValue();
        Book book = new Book();
        book.setId(1);
        book.setName("潘权荣");
        book.setAge(18);
        ops.set("k2",book);
        Object k1 = ops.get("k2");
    }
}

如果出现找不到类redisConnectionFactory,加依赖

 

五.缓存工具Spring Cache操作redis

作用:通过cache工具存入到数据库的同时将数据存到redis中而不是需要又调用redis的代码进行存入

step01-依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

step02-配置文件+开启缓存

#只需要写这些配置就行了,springcache自动帮你弄好了连接对应的缓存工具
spring.redis.host=139.159.210.107
spring.redis.password=123
spring.redis.port=6379

spring.cache.cache-names=c1
@SpringBootApplication
//开启缓存
@EnableCaching
public class SpringCacheApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringCacheApplication.class, args);
    }

}

step03-实体类User

public class User implements Serializable {
    private Integer id;
    private String username;
    private Integer age;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", age=" + age +
                '}';
    }}

step04-service

@Service
//如果写了CacheConfig就不需要在下面每个方法都配置这个@Cacheable(cacheNames = "c1")了
//@CacheConfig(cacheNames = "c1")
public class UserService {

    /**
     * @Cacheable:表示将当前方法的返回值缓存起来,缓存的key是cacheNames+'::'+方法的参数,缓存的value是方法的返回值
     */
    @Cacheable(cacheNames = "c1")
    public User getUserById(Integer id){
        System.out.println("getUserById>>>"+id);
        User user = new User();
        user.setId(id);
        return user;
    }

    @Cacheable(cacheNames = "c1")
    public User getUserById(Integer id,String username){
        System.out.println("getUserById>>>"+id+">>>"+username);
        User user = new User();
        user.setId(id);
        user.setUsername("username");
        return user;
    }

    /**
     * key = "#id":意思是使用id来做缓存的key,缺陷是可能造成数据的错误,因为id可能重复
     */
    @Cacheable(cacheNames = "c1",key = "#id")
//    @Cacheable(cacheNames = "c1",keyGenerator = "myKeyGenerator")
    public User getUser(Integer id,String username){
        System.out.println("getUserById>>>"+id+">>>"+username);
        User user = new User();
        user.setId(id);
        user.setUsername(username);
        return user;
    }

    /**
     * 对于更新来说,假设:
     * 1.首先执行getUserById方法,查询id为99 的用户
     * 2.执行updateUserById更新id为99的用户
     * 3.再去执行getUserById方法,查询的是id99的用户,由于第一步已经调用了该方法,id为99的用户被缓存起来了,所以拿到的还是旧数据
     * 
     *更新规则:需要保证这里缓存的key要和之前添加的key保持一致
     * 为了在更新数据的时候,也更新缓存我们使用CachePut注解
     * 和Cacheable的区别就是:如果有该key和value了,就会覆盖前面的
     使用这个注解,意味着更新方法的返回值不能是void,必须是刚刚更新过的对象
     * @param user
     * @return
     */
    @CachePut(cacheNames = "c1",key = "#user.id")
    public User updateUser(User user){
        System.out.println("updateUser");
        return user;
    }

    /**
     * @CacheEvict:一般加在删除方法上
     * allEntries = false:这个表示是否清除所有缓存
     * beforeInvocation = false:这个表示是否在删除数据库之前,先删除缓存
     * @param id
     
    使用场景,是因为我们在更新数据的时候,会比较慢,所以不如直接将缓存删了,等你下次来了,我在进行添加
     */
    @CacheEvict(cacheNames = "c1",allEntries = false,beforeInvocation = false)
    public void delUserById(Integer id){
        System.out.println("删除");
    }


}

step05-自定义key

当你不想使用默认的cacheNames+'::'+方法的参数为key时,可以通过实现KeyGenerator接口来自定义key

/**
 * 自定义key的生成类
 */
@Component
public class MyKeyGenerator implements KeyGenerator {
    /**
     * generate:当需要用来key的时候就会自动的调用该方法
     * @param target  当前对象
     * @param method  当前方法
     * @param params  当前方法的参数
     * @return  返回值就是缓存的key
     */
    @Override
    public Object generate(Object target, Method method, Object... params) {
        String name = method.getName();
        String s = Arrays.toString(params);
        return name+s;
    }
}

step06-测试

@SpringBootTest
class SpringCacheApplicationTests<test> {
    @Autowired
    UserService userService;

    @Test
    void contextLoads() {
        User user = userService.getUserById(3);
        User user2 = userService.getUserById(4);
        System.out.println("user = " + user);
        System.out.println("user2 = " + user2);
    }

    @Test
    void test2() {
        User user = userService.getUserById(3,"ok");
        User user2 = userService.getUserById(3,"欧克");
        System.out.println("user = " + user);
        System.out.println("user2 = " + user2);
    }

    @Test
    void test3() {
        User user = userService.getUser(5, "潘权荣");
        System.out.println("user = " + user);
        User user1 = new User();
        user1.setId(5);
        user1.setUsername("欧克");
        userService.updateUser(user1);
        userService.getUser(5, "潘权荣");
    }

    @Test
    void test04(){
        //删除缓存
        userService.delUserById(5);
        
        
        
    }



}

六.Session共享

前言:用来解决一个系统分配在多个服务器上的时候,可以缓解一台服务器的压力,一般使用在redis集群情况下。其实就是你加了session依赖之后,session自动帮你存在了redis中去,不需要做任何的配置了,到时你多个项目的时候,依然可以在redis中获取同一个session

step01-配置文件+依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
//当你要获取session的时候,自动帮你去redis中获取,让你发送请求,然后设置session的时候,自动帮你存
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
spring.redis.host=139.159.210.107
spring.redis.password=123
spring.redis.port=6379

server.port=8080

step02-写一个接口验证

 

 step03-为了统一端口,只需要访问接口就行了,提高用户体验

部署两个服务器

 后面的8080.log和8081.log是执行的日志写在这些日志文件里头

使用nginx来做反向代理,在default.conf中配置这两个springboot项目的上游服务器

 

step04-端口统一了

 七.Redis处理接口幂等性

前言:同一个接口,相同的参数,执行多次和执行一次的结果是一样的(主要针对增删改)

方式一:通过拦截器的方式

step01-依赖+配置文件

<groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
spring.redis.host=139.159.210.107
spring.redis.password=123
spring.redis.port=6379
step02-自定义注解
/**
 * 注解一般都是要通过aop来进行调用,但是在controller层中我们可以通过另一种方式,使用spring提供的handlMethod
 */
//运行时发生
@Retention(RetentionPolicy.RUNTIME)
//使用在方法上
@Target(ElementType.METHOD)
public @interface Idempontent {

}
step03-controller
@RestController
public class HellController {
    @Autowired
    TokenService tokenService;

    //发送请求进行提交的时候
    @PostMapping("/hello")
    //每次这个接口都是用幂等性处理
    @Idempontent
    public String helloPost(){
        return "hello Post";
    }

    @GetMapping("/hello")
    public String helloGet(){
        return "hello Get";
    }
    //当前端点击按钮的时候,生成令牌,然后携带该令牌去redis中查看是否有该令牌
    @GetMapping("/generate")
    public String generateToken(){
        return tokenService.generateToken();
    }
}
step04-拦截器处理@Idempotent注解
/**
 * 自定义拦截器,如果哪个接口需要处理幂等性,就在哪个接口上添加@Idempotent注解,如果有就检查请求的token是否合法,如果没有,这个请求就直接放行
 */
@Component
public class IdempontentInterceptor implements HandlerInterceptor {

    @Autowired
    TokenService tokenService;
    /**
     * 在controller之前执行
     * @param request
     * @param response
     * @param handler  :如果handle不是注解定义的,像之前springmvc那样,这个handler就是每一个controller对象,但是我们是通过注解的方式进行的,所以此时这个handler是你的处理器
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        HandlerMethod是spring提供在Controller层中用来将每一个接口方法封装成一个HandlerMethod对象,
        if(handler instanceof HandlerMethod){
            //这个实际上就是具体的接口方法
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            //获取写在方法上的注解
            Idempontent methodAnnotation = handlerMethod.getMethodAnnotation(Idempontent.class);
            //说明这个接口需要进行幂等性出来
            if(methodAnnotation!=null){
                //检查是否携带令牌,令牌是存放在request中的
                boolean result=tokenService.checkToken(request);
                if(!result){
                    RespBean respBean = new RespBean();
                    respBean.setMsg("请求重复");
                    respBean.setStatus(500);
                    response.setContentType("application/json;charset=utf-8");
                    response.getWriter().write(new ObjectMapper().writeValueAsString(respBean));
                    //也可以返回自定义的幂等性异常
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

将拦截器注入到配置中

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    IdempontentInterceptor idempontentInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(idempontentInterceptor)
                .addPathPatterns("/**");
    }
}
step05-检查令牌
@Service
public class TokenService {

    //设置给予redis的令牌
    @Autowired
    RedisService redisService;

    public boolean checkToken(HttpServletRequest request) {
        //从请求头中获取token
        String token = request.getHeader("token");
        //如果请求头没有,就从请求参数中获取
        if (token == null || "".equals(token)) {
            token = request.getParameter("token");
            if (token == null || "".equals(token)) {
                //说明请求没有携带令牌
                return false;
            }
        }
        //查看redis中是否存在
        boolean exists = redisService.exists(token);
        //如果redis中存在令牌
        if (exists) {

            boolean delResult = redisService.deleteToken(token);
            return delResult;
        }

        return false;
    }

    //生成令牌,并存令牌在redis中
    public String generateToken() {
        String s = UUID.randomUUID().toString();
        redisService.saveToken(s);
        return s;

    }
}
step06-检查redis中是否有令牌
@Service
public class RedisService {
    @Autowired
    StringRedisTemplate redisTemplate;
    public boolean exists(String token) {
        //判断redis中是否存在token参数
        return redisTemplate.hasKey(token);
    }
    //担心并发的时候,同时删除了,但还是要在操作一次,会报错,所以我们在删除的时候需要在先查一次,看看token是否存在
    public boolean deleteToken(String token) {
        if(exists(token)){
            return redisTemplate.delete(token);
        }
        return false;
    }

    //在redis中存储令牌
    public void saveToken(String token) {
        redisTemplate.opsForValue().set(token,token);
    }
}
step07-自定义幂等异常
/**
 * 自定义的幂等异常
 */
public class IdempontentException extends RuntimeException {
    public IdempontentException(String message) {
        super(message);
    }
}
/**
 * 配置全局捕获异常
 */
@RestControllerAdvice
public class GlobalException {
    @ExceptionHandler(IdempontentException.class)
    public RespBean idempontentException(IdempontentException e){
        RespBean respBean = new RespBean();
        respBean.setMsg(e.getMessage());
        respBean.setStatus(500);
        return respBean;
    }
}

方式二:通过Aop

step01-依赖+配置
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
step02-注解
//运行时发生
@Retention(RetentionPolicy.RUNTIME)
//使用在方法上
@Target(ElementType.METHOD)
public @interface Idempontent {

}
step03-Aop切面
@Component
@Aspect
@EnableAspectJAutoProxy
public class IdempontentAop {
    @Autowired
    TokenService tokenService;
    
    //拦截该注解
    @Pointcut("@annotation(com.pan.idempontent.annottion.Idempontent)")
    public void pointcut(){

    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        //获得当前请求
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        HttpServletResponse response = requestAttributes.getResponse();
        boolean checkToken = tokenService.checkToken(request);

        //方式一,直接返回一个json字符串
//        if(checkToken){
        //继续执行
//            return pjp.proceed();
//
//        }else {
//            RespBean respBean = new RespBean();
//            respBean.setMsg("请求重复");
//            respBean.setStatus(500);
//            return new ObjectMapper().writeValueAsString(respBean);
//        }

        //方式二
        if(checkToken){
            //继续执行
            return pjp.proceed();
        }else {
            throw new IdempontentException("请求重复");
        }


    }
}
step04-自定义幂等异常
/**
 * 自定义的幂等异常
 */
public class IdempontentException extends RuntimeException {
    public IdempontentException(String message) {
        super(message);
    }
}
/**
 * 配置全局捕获异常
 */
@RestControllerAdvice
public class GlobalException {
    @ExceptionHandler(IdempontentException.class)
    public RespBean idempontentException(IdempontentException e){
        RespBean respBean = new RespBean();
        respBean.setMsg(e.getMessage());
        respBean.setStatus(500);
        return respBean;
    }
}
step05-检查令牌
@Service
public class TokenService {

    //设置给予redis的令牌
    @Autowired
    RedisService redisService;

    public boolean checkToken(HttpServletRequest request) {
        //从请求头中获取token
        String token = request.getHeader("token");
        //如果请求头没有,就从请求参数中获取
        if (token == null || "".equals(token)) {
            token = request.getParameter("token");
            if (token == null || "".equals(token)) {
                //说明请求没有携带令牌
                return false;
            }
        }
        //查看redis中是否存在
        boolean exists = redisService.exists(token);
        //如果redis中存在令牌
        if (exists) {

            boolean delResult = redisService.deleteToken(token);
            return delResult;
        }

        return false;
    }

    //生成令牌,并存令牌在redis中
    public String generateToken() {
        String s = UUID.randomUUID().toString();
        redisService.saveToken(s);
        return s;

    }
}
step06-检查redis中是否有令牌
@Service
public class RedisService {
    @Autowired
    StringRedisTemplate redisTemplate;
    public boolean exists(String token) {
        //判断redis中是否存在token参数
        return redisTemplate.hasKey(token);
    }
    //担心并发的时候,同时删除了,但还是要在操作一次,会报错,所以我们在删除的时候需要在先查一次,看看token是否存在
    public boolean deleteToken(String token) {
        if(exists(token)){
            return redisTemplate.delete(token);
        }
        return false;
    }

    //在redis中存储令牌
    public void saveToken(String token) {
        redisTemplate.opsForValue().set(token,token);
    }
}
step07-controller
@RestController
public class HellController {
    @Autowired
    TokenService tokenService;

    //发送请求进行提交的时候
    @PostMapping("/hello")
    //每次这个接口都是用幂等性处理
    @Idempontent
    public String helloPost(){
        return "hello Post";
    }

    @GetMapping("/hello")
    public String helloGet(){
        return "hello Get";
    }
    //当前端点击按钮的时候,生成令牌,然后携带该令牌去redis中查看是否有该令牌
    @GetMapping("/generate")
    public String generateToken(){
        return tokenService.generateToken();
    }
}

八.redis事务

- 开启事务:multi
- 输入要执行的命令:被放入到一个队列中
- 执行事务:exec
- 取消事务:discard

 

redis的事务和mysql的事务不太一样,redis中的事务,只有编译的错误他才会回滚,如果是类型错误的incr是不会回滚,只是不会执行错的,其他没错的照常执行。

为了解决这个弊端,我们得搭配watch来使用
在开启事务之前,先通过watch命令去监听一个或多个key,在开启事务之后,如果有其他客户端修改了我监听的key,事务会自动取消。
如果执行了事务,或者取消了事务,watch监听自动消除,一般不需要手动执行unwatch。

 

九.Redis持久化机制

1.RDB(快照,二进制文件)

可以在redis.conf文件中修改配置

RDB是Redis默认的持久化机制

 - RDB持久化文件,速度比较快,而且存储的是一个二进制的文件,传输起来很方便。

 - RDB持久化的时机:

save 900 1:在900秒内,有1个key改变了,就执行RDB持久化。

save 300 10:在300秒内,有10个key改变了,就执行RDB持久化。

save 60 10000:在60秒内,有10000个key改变了,就执行RDB持久化。

当满足要求,或者shutdown或者save/bgsave指令的时候,数据存储到redis配置文件的同一目录:dump.rdb,当你重新启动的时候,会从该磁盘文件中,获取数据。

- RDB无法保证数据的绝对安全。,因为是达到要求才能够进行存储数据到rdb文件

关闭RDB的操作:
1.进去到redis的配置文件中去
vi redis.conf
2.配置文件中搜/rdb,找到save "",取消注释关闭RDB备份

 

2.AOF

AOF持久化机制默认是关闭的,Redis官方推荐同时开启RDB和AOF持久化,更安全,避免数据丢失。

- AOF持久化的速度,相对RDB较慢的,存储的是一个文本文件,到了后期文件会比较大,传输困难。

- AOF持久化时机。

appendfsync always:每执行一个写操作,立即持久化到AOF文件中,性能比较低。
appendfsync everysec:每秒执行一次持久化。(默认)
appendfsync no:会根据你的操作系统不同,环境的不同,在一定时间内执行一次持久化。

- AOF相对RDB更安全,推荐同时开启AOF和RDB。

开启AOF机制:
1.在redis.conf配置文件中搜/appendonly
2.将appendonly开启
生成的备份文件名时:存在/appendonlydir/appendonly.aof

 

十.Redis一主两从

前言:使用主机来增删改,从机来读

1.修改redis.conf配置文件,因为需要弄多个redis实例才能实现一主二从

使用AOF的时候,我们在设置一主二从的时候,需要添加多个redis实例,所以要修改备份文件的名字,一个实例对应一个备份文件名
appenddirname "appendonlydir6379"

使用RDB备份的话,就要修改这个备份文件名
logfile "6379.log"

还要修改pid进程文件
pidfile /var/run/redis_6379.pid

 2.在修改配置文件的时候,可以通过命令快速修改内容

3.我们需要将6380和6381当作从机,所有在6380和6381配置文件中还需要修改多一个配置

replicaof masterip 6379        :告诉从机,主机是ip和端口号6379
masterauth 主机密码 

 

4.查看副本信息:info replication

十一.哨兵模式

step01-修改sentinel.conf里面的配置

sentinel monitor mymaster 127.0.0.1 6379 1
sentinel auth-pass mymaster 123
其中mymaster是给要监控的主机取的名字,随意取,后面是主机地址,最后面的1表示有多少个sentinel认为主机挂掉了,就进行切换(我这里只有一个,因此设置为1)

 

step02-启动哨兵

redis-sentinel sentinel.conf

 

十二.集群

前言:当创建好了集群,只需要登录到其中一个节点上,数据会随机分配到任意的节点上,进行切换

step01-在redis.conf中配置

# 端口号
port 7001
# 本来这个配置是只允许本地登录,注释掉之后就允许远程登录了
#bind 127.0.0.1
# 开启集群模式
cluster-enabled yes
# 集群的配置,每一个节点都对应一个配置
cluster-config-file nodes-7001.conf
# 关闭保护模式
protected no
# 后台运行
daemonize yes
# 主机密码(这个是给从机配置的)
masterauth 123
# 配置主机密码
requirepass 123
# 日志文件
logfile 7001.log
# 每个节点的进程文件
pidfile /var/run/redis_7001.pid
# 关闭 rdb 持久化(aof持久化默认就是关闭的)
save ""

快速的将复制的配置文件进行修改

 

step02-这个表示各个节点都启动成功了。接下来我们就可以进行集群的创建了

./src/redis-cli --cluster create --cluster-replicas 1 192.168.91.130:7001 192.168.91.130:7002 192.168.91.130:7003 192.168.91.130:7004 192.168.91.130:7005 192.168.91.130:7006 -a 123

注意:创建的redis一定要为空,否则搭建不了集群,所以要删除缓存文件等

 

step03-集群创建成功后,我们可以登录到 Redis 控制台查看集群信息,注意登录时要添加 -c 参数,表示以集群方式连接

 step04-可以在上面的集群基础上添加新的主节点

src/redis-cli -a 123 -p 7001 --cluster add-node 127.0.0.1:7007 127.0.0.1:7001

但是通过cluster nodes命令查看的时候7007并没有任何的槽

step05-重新分配槽

src/redis-cli -a 123 -p 7001 --cluster reshard 127.0.0.1:7001

因为 hash 槽目前已经全部分配完毕,要重新从已经分好的节点中拿出来一部分给 7007,必然要让另外三个节点把吃进去的吐出来,这里我们可以输入多个节点的编号,每次输完一个点击回车,输完所有的输入 done 表示输入完成,这样就让这几个节点让出部分 slot,如果要让所有具有 slot 的节点都参与到此次 slot 重新分配的活动中,那么这里直接输入 all 即可

step06-也可以添加从节点

src/redis-cli -a 123 -p 7001 --cluster add-node 127.0.0.1:7008 127.0.0.1:7001 --cluster-slave --cluster-master-id 8e8eedb372b9dc36a57ff81d4b4b4634f3f2bffa

8e8eedb372b9dc36a57ff81d4b4b4634f3f2bffa是你要将7008定义为谁的从机

删除槽,在删除之前,要将槽分出去,确保集群的完整性

src/redis-cli -a 123 -p 7001 --cluster del-node 127.0.0.1:7001 ce53609f0fc6b0af5daf2ce37d6c4ec5798c7d98

Jedis 操作 RedisCluster

需要的是jedis的依赖

public class RedisCluster {
    public static void main(String[] args) {
        Set<HostAndPort> nodes = new HashSet<>();
        nodes.add(new HostAndPort("192.168.91.128", 7001));
        nodes.add(new HostAndPort("192.168.91.128", 7002));
        nodes.add(new HostAndPort("192.168.91.128", 7003));
        nodes.add(new HostAndPort("192.168.91.128", 7004));
        nodes.add(new HostAndPort("192.168.91.128", 7005));
        nodes.add(new HostAndPort("192.168.91.128", 7006));
        nodes.add(new HostAndPort("192.168.91.128", 7007));
        GenericObjectPoolConfig config = new JedisPoolConfig();
        //连接池最大空闲数
        config.setMaxIdle(300);
        //最大连接数
        config.setMaxTotal(1000);
        //连接最大等待时间,如果是 -1 表示没有限制
        config.setMaxWaitMillis(30000);
        //在空闲时检查有效性
        config.setTestOnBorrow(true);
        JedisCluster cluster = new JedisCluster(nodes, 15000, 15000, 5, "123", config);
        String set = cluster.set("k1", "v1");
        System.out.println(set);
        String k1 = cluster.get("k1");
        System.out.println(k1);
    }
}

十三.布隆过滤器

每一个布隆过滤器,在 Redis 中都对应了一个大型的位数组以及几个不同的

hash 函数。

前言:1.Bloom Filter专门是用来解决去重问题的

2.底层是根据hash函数进行运算的

3.再对应的位置查看有没有对应的数,虽说有不一定真的有,需要去数据库查找,说没有的话,就一定是没有的

1.安装

1.docker安装

docker run -p 6379:6379 --name redis-redisbloom
redislabs/rebloom:latest

2.自己编译安装

cd redis-5.0.7
//方式一
git clone https://github.com/RedisBloom/RedisBloom.git
//方式二
wget http://github.com/RedisBloom/RedisBloom/archive/refs/tags/v2.2.18.tar.gz
cd RedisBloom/
//使用gcc的指令
make
cd ..
//记得redis.conf文件需要修改,和之前创建好的redis一样的修改就行了
redis-server redis.conf --loadmodule ./RedisBloom/redisbloom.so

安装完成后,执行 bf.add 命令,测试安装是否成功。

每次启动时都输入 redis-server redis.conf --loadmodule ./RedisBloom/redisbloom.so 比较麻烦,我们可以将要加载的模块在

redis.conf 中提前配置好。

##MODULES 
\# Load modules at startup. If the server is not able to load modules 
\# it will abort. It is possible to use multiple loadmodule 

directives. 
\# 
\# loadmodule /path/to/my_module.so 
\# loadmodule /path/to/other_module.so 

loadmodule /root/redis-5.0.7/RedisBloom/redisbloom.so 

最下面这一句,配置完成后,以后只需要 redis-server redis.conf 来启动 Redis 即可。

2.使用

默认情况下,我们使用的布隆过滤器它的错误率是 0.01 ,默认的元素大小是 100。但是这两个参数也是可以配置的。

我们可以调用 bf.reserve 方法进行配置。

BF.RESERVE k1 0.0001 1000000 

第一个参数是 key,第二个参数是错误率,错误率越低(意味着位数组越长),占用的空间越大,第三个参数预计存储的数量,当实际数量超出预计数量时,错误率会上升。

例子:

 

3.使用java代码使用布隆过滤器

方式一:直接使用布隆过滤器的依赖

step01-i加入布隆过滤器的依赖,但是呢,它有兼容性的问题,需要搭配低版本的jedis

<dependency>
	<groupId>com.redislabs</groupId>
	<artifactId>jrebloom</artifactId>
	<version>1.2.0</version>
</dependency>
//搭配布隆过滤器的jedis这个低版本
<dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
     <version>3.7.0</version>
</dependency>

step02-代码

public class BloomFilter {
    public static void main(String[] args) {
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        config.setMaxIdle(300);
        config.setMaxTotal(1000);
        config.setMaxWaitMillis(30000);
        config.setTestOnBorrow(true);
        JedisPool pool = new JedisPool(config, "192.168.91.128",6379, 30000, "javaboy");
        //布隆过滤器提供的方法
        Client client = new Client(pool);
        //存入数据
        for (int i = 0; i < 100000; i++) {
            client.add("name", "javaboy-" + i);
        }
        //检查数据是否存在
        boolean exists = client.exists("name", "javaboy-9999999");
        System.out.println(exists);
        }
}

方式二:使用lettuce提供的工具,自定义

step01-依赖

<dependency>
	<groupId>com.redislabs</groupId>
	<artifactId>jrebloom</artifactId>
	<version>1.2.0</version>
</dependency>

<dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
</dependency>

还需要加上lettuce依赖

 step02-代码

 

 

十四.分布式锁

方式一:自己写锁

需要引入lua脚本,它附有原子性,再java代码执行的过程中,主要lua脚本没有执行完之前,其他java代码的命令是一概不处理的

 

 

 

方式二:使用Redisson锁,直接调用它提供的方法

相对于 Jedis 这种原生态的应用,Redisson 对 Redis 请求做了较多的封装,对于锁,也提供了对应的方

法可以直接使用:

step01-依赖

 

 

step02-java代码

Config config = new Config(); 
//配置 Redis 基本连接信息 
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123"); 
//获取一个 RedissonClient 对象 
RedissonClient redisson = Redisson.create(config); 
//获取一个锁对象实例 
RLock lock = redisson.getLock("lock"); 
try { 
    //获取锁 
    boolean b = lock.tryLock(500, 1000, TimeUnit.MILLISECONDS); 
    if (b) { 
        //获取到锁了,开始写业务,getBucket相当于设置一个key
        RBucket<Object> bucket = redisson.getBucket("k1"); 
        //设置value值
        bucket.set("www.javaboy.org"); 
        Object o = bucket.get(); 
        System.out.println(o); 
    }else{ 
    	System.out.println("没拿到锁"); 
    }
} catch (InterruptedException e) { 
	e.printStackTrace(); 
} finally { 
    //释放锁 
    lock.unlock(); 
} 

在这段代码中,核心的就是 lock.tryLock(500, 1000, TimeUnit.MILLISECONDS); ,第一个参数是 尝试加锁的等待时间为 500 毫秒,第二个参数表示锁的超时时间为 1000 毫秒,也就是这个锁在 1000毫秒后会自动失效。(如果第二个参数不写的话,那么redisson会有watchdog来查看你的业务是否完成,如果没有完成自动续期) redisson中的加锁和设置过期时间等操作都是基于lua脚本完成的,底层是setnx+lua脚本(保证原子性)

十五.redis限流(令牌桶限流)

 

wget https://github.com/brandur/redis-cell/releases/download/v0.2.4/redis-cellv0.2.4-x86_64-unknown-linux-gnu.tar.gz
tar -zxvf redis-cell-v0.2.4-x86_64-unknown-linux-gnu.tar.gz
mkdir redis-cell
mv libredis_cell.d ./redis-cell
mv libredis_cell.so ./redis-cell

接下来修改 redis.conf 文件,加载额外的模块:
loadmodule /root/redis-5.0.7/redis-cell/libredis_cell.so

启动 Redis
redis-server redis.conf

step01-解压缩redis-cell压缩包

 step02-再配置文件中加入redis-cell的工具的路径

 step03-启动redis,设置令牌通的参数

 

redis 启动成功后,如果存在 CL.THROTTLE 命令,说明 redis-cell 已经安装成功了

CL.THROTTLE 命令一共有五个参数

\1. 第一个参数是 key 

\2. 第二个参数是**令牌桶容量** 

\3. **令牌产生个数** 

\4. **令牌产生时间** 

\5. **本次取走的令牌数** 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值