Redis入门详解

Redis

一、引言

1、什么是Redis?

Redis是一款基于内存键-值型NoSQL数据库。NoSQL - 非关系型数据库

特点:可以进行快速的数据读写,官方给的数据 11W/s 读 8W/s 写。

Memcache

2、Redis在实际开发中的运用场景

1)作为分布式系统的缓存服务器

2)应对数据高速读写的业务

3)作为分布式锁使用(Zookeeper、Redis)

4)数据共享

5)ID自增序列

二、Docker安装Redis

1)在合适位置准备好redis.conf配置文件

./redis/conf/redis.conf

注意:./表示docker-compose.yml所在的路径

2)编写docker-compose.yml

redis:
    image: redis:5
    container_name: redis
    restart: always
    ports:
      - 6379:6379
    volumes:
      - ./redis/conf/redis.conf:/etc/redis/redis.conf
      - ./redis/conf/data:/data
    command:
      ['redis-server', '/etc/redis/redis.conf']

3)修改宿主机中的redis.conf配置文件

把
bind 127.0.0.1
改成
bind 0.0.0.0

4)重启redis并且通过工具连接操作redis

三、Java通过API操作redis

1)添加依赖

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

2)编写代码操作redis

 //通过连接池
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(100);
        poolConfig.setMaxIdle(50);
        poolConfig.setMinIdle(20);

        JedisPool jedisPool = new JedisPool(poolConfig,"192.168.195.188", 6379);
        Jedis jedis = jedisPool.getResource();

        //连接redis
//        Jedis jedis = new Jedis("192.168.195.188", 6379);
        //操作redis
//        jedis.set("money", "10000");
        String value = jedis.get("money");
        System.out.println(value);

        //关闭连接
        jedis.close();

四、Spring通过API操作redis

1)添加依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-redis</artifactId>
        <version>2.2.3.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.2.7.RELEASE</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.0.1</version>
    </dependency>
</dependencies>

2)配置applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置redis连接池对象 -->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!-- 最大空闲数 -->
        <property name="maxIdle" value="50"/>
        <!-- 最大连接数 -->
        <property name="maxTotal" value="100"/>
        <!-- 最大等待时间 -->
        <property name="maxWaitMillis" value="20000"/>
    </bean>

    <!-- 配置redis连接工厂 -->
    <bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!-- 连接池配置 -->
        <property name="poolConfig" ref="poolConfig"/>
        <!-- 连接主机 -->
        <property name="hostName" value="192.168.195.188"/>
        <!-- 端口 -->
        <property name="port" value="6379"/>
    </bean>


    <bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean>
    <bean id="jdkSerializationRedisSerializer" class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"></bean>

    <!-- 配置redis模板对象 -->
    <bean class="org.springframework.data.redis.core.RedisTemplate">
        <!-- 配置连接工厂 -->
        <property name="connectionFactory" ref="connectionFactory"/>
        <property name="keySerializer" ref="stringRedisSerializer"/>
        <property name="valueSerializer" ref="stringRedisSerializer"/>
    </bean>

</beans>

注意:Spring整合Redis提供的RedisTemplate默认对 key 和 value进行jdk的序列化操作。这样做的好处在于可以存放类型复杂的key-value,但是到了redis中会变成序列化的字符串,命令不能直接操作。
建议实际开发过程中,换成字符串的序列化方式,如果需要存放类型复杂的对象,可以手动的转换成JSON字符串。

五、SpringBoot整合Redis

1)添加依赖

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

2)配置application.yml

spring:
  redis:
    host: 192.168.195.188
    port: 6379

3)注入对象操作redis

@Autowired
private RedisTemplate redisTemplate;

@Autowired
private StringRedisTemplate stringRedisTemplate;

....

六、Redis常见的数据类型(https://www.redis.net.cn/)

1)String类型

结构:key - value

常用命令:
set key value
get key
incr key
incrby key xxx
decr key
decrby key xxx

2)Hash类型

结构:key - value(field, value)…

常用命令:
hset key field value
hget key field
hgetall key
hdel key field

运用场景:通常用来保存一个对象,field对应对象中的一个属性。

3)List类型 - 链表

结构:key - value1,value2,value3…

特点:value有多个,可以重复,并且有序,底层是一个双向链表

常用命令:
lpush key value1 value2…
rpush key value1 value2…
llen key
lpop key
rpop key
lrange key beginIndex endIndex (含头含尾)

4)Set类型 - 集合

结构:key - value3,value2,value4,value1

特点:value有多个,无序不可重复,底层是一张哈希表

常用命令:
sadd key value1 value2…
smembers key
scard key

运用场景:去重、判断唯一、判定是否存在

5)Zset类型 - 有序集合

结构:key - (score1, value1) (score2, value2)…

特点:value有多个,每个value都携带一个评分,根据评分排序,元素不能重复,底层是一张跳跃表

常用的命令:
zadd key score1 value1 score2 value2 …
zscan key cursor
zcard key
zrange key startindex endindex
zrevrangebyscore key maxScore minScore

运用场景:热门搜索词 - 搜索词的搜索频率设置score

6)redis中非类型相关的常见命令

keys * - 查看当前redis库中所有的key
type key - 返回key对应的数据类型
del key - 删除指定的key,无论数据类型
select index - 选择指定的数据库(0 ~ 15)
flushdb - 清空当前的数据库的所有key
flushall - 清空所有数据库的所有key

#对应的SpringBoot中的API操作代码
stringRedisTemplate.opsForValue();//操作String类型
stringRedisTemplate.opsForHash();//操作hash类型
stringRedisTemplate.opsForList();//操作list类型
stringRedisTemplate.opsForSet();//操作set类型
stringRedisTemplate.opsForZSet();//操作zset类

stringRedisTemplate.expire("", 5, TimeUnit.MINUTES);
stringRedisTemplate.delete("");       

七、Redis的超时时间

7.1 redis的超时时间有什么用?

redis通常作为缓存服务器存在,数据库中的很多数据都会缓存到redis中,以此减少数据库的访问压力。但是因为存储单位的不同,不可能将所有的数据库数据都缓存到redis中,只能缓存局部数据。实际开发中,肯定希望redis尽可能的缓存热点数据,如何判断哪些数据是热点数据? 通过给key设置超时时间可以帮助redis过滤掉很多的冷门数据。

7.2 超时时间的相关命令
expire key - 给key设置超时时间,单位是秒
persist key - 移除key的超时时间
ttl key - 查看key的剩余超时时间
	-2 表示当前key不存在
	-1 表示当前key永生

注意:redis中,key如果过期的话,并不会立刻从内存中移除,在redis中移除过期key的机制:

1)当客户端需要使用到某个key时
2)后台有个线程间隔的扫描所有过期的key,再进行移除

八、redis的内存淘汰策略

8.1 什么是redis的内存淘汰策略?

简单来说,就是当redis发现自己内存满了之后,怎么办?

8.2 如何配置Redis的内存淘汰策略?
image-20200707162053766

内存淘汰策略的可选项:
volatile-lru -> Evict using approximated LRU among the keys with an expire set.
allkeys-lru -> Evict any key using approximated LRU.
volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
allkeys-lfu -> Evict any key using approximated LFU.
volatile-random -> Remove a random key among the ones with an expire set.
allkeys-random -> Remove a random key, any key.
volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
noeviction -> Don’t evict anything, just return an error on write operations.

可选值:
noeviction -> redis变成只读模式
volatile-lru -> 在所有设置了过期时间的key中,淘汰最近最少被使用的key
allkeys-lru -> 在所有的key中,淘汰最近最少被使用的key
volatile-lfu -> 在所有设置了过期时间的key中,淘汰总共最少被使用的key
allkeys-lfu -> 在所有的key中,淘汰总共最少被使用的key
volatile-random -> 在所有设置了过期时间的key中,随机淘汰key
allkeys-random -> 在所有的key中,随机淘汰key
volatile-ttl -> 在所有设置了过期时间的key中,淘汰存活时间最短的key

前缀后缀:
allkeys - 表示所有的key
volatile - 表示设置了过期时间的key

lru - 最近最少被使用
lfu - 总共最少被使用
random - 随机
ttl - 存活时间最短

注意:redis中的所有淘汰策略,都是近似算法(lru、lfu、ttl)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C5qkHZT7-1594211958242)(img/image-20200707163258753.png)]

九、Redis的lua脚本

9.1 Redis的线程模型

redis是单线程模型的服务。所有的Redis单线程,并不是说Redis服务只有一个线程,确切来说,只有一个主线程在接收执行客户端的命令。

为什么Redis要设计成单线程?

Redis的执行效率瓶颈不在于CPU,Redis的效率瓶颈在于网络,多线程执行命令对于Redis来说,反而要额外的进行线程切换的开销。

单线程的Redis是否意味着线程安全?

Redis内部确实是线程安全的,但是调用Redis的服务不一定线程安全?

往往可以基于 单线程特性 + Lua脚本 实现无锁化的线程安全业务。

注意:
1、Lua脚本中不能执行耗时操作,如果执行超过5S的lua脚本,Redis服务会自动停止lua脚本运行。
2、Redis 6.0之后,开始支持多线程。主线程 + n个IO线程,IO线程主要负责连接的处理,数据的读写,所有IO线程的命令会交给主线程执行。

9.2 Lua脚本的基本语法

Redis执行lua脚本的语法:

eval SCRIPT numkeys [key …] [arg …]

解释:
eval 固定写法,表示执行一个lua脚本
SCRIPT 脚本内容
numkeys 脚本中key变量的数量
[key …] 传入脚本的key变量的实际值
[arg …] 传入脚本中的附加参数的实际值

在lua脚本中,操作redis的语法:

redis.call(’…’, ‘…’, ‘…’);

比如:set name xiaoming -> redis.call(‘set’, ‘name’, ‘xiaoming’)

案例:
eval “return redis.call(‘get’, ‘name’)” 0
eval “return redis.call(‘set’, KEYS[1], ARGV[1])” 1 name xiaohong
eval “redis.call(‘hset’, KEYS[1], ARGV[1], ARGV[2])” 1 person sex man

练习:求取一个链表所有元素的平均值

--获得redis链表的总长度
local agesLength = redis.call('llen', KEYS[1])

--获得redis链表的所有元素放入一个对象(数组)
local ages = redis.call('lrange', KEYS[1], 0, agesLength)

--临时变量
local ageSum = 0

--循环数组,年龄的累加
for i=1,#ages do
    --循环体
    ageSum = ages[i] + ageSum
end

--求取平均值
return ageSum/#ages

lua脚本的缓存

缓存脚本:script load “SCRIPT” 返回一个sha签名字符串
执行缓存脚本:evalsha “脚本签名” numkeys [key …] [arg …]

9.3 SpringBoot中执行lua脚本
/**
 * 执行lua脚本
 */
@Test
void script(){
    
    String str = "--获得redis链表的总长度\n" +
            "local agesLength = redis.call('llen', KEYS[1])\n" +
            "\n" +
            "--获得redis链表的所有元素放入一个对象(数组)\n" +
            "local ages = redis.call('lrange', KEYS[1], 0, agesLength)\n" +
            "\n" +
            "--临时变量\n" +
            "local ageSum = 0\n" +
            "\n" +
            "--循环数组,年龄的累加\n" +
            "for i=1,#ages do\n" +
            "    --循环体\n" +
            "    ageSum = ages[i] + ageSum\n" +
            "end\n" +
            "\n" +
            "--求取平均值\n" +
            "return ageSum/#ages";

    //创建一个脚本对象
    DefaultRedisScript script = new DefaultRedisScript(str, Long.class);

    List<String> keys = new ArrayList<>();
    keys.add("ages");
    //执行脚本
    Long result = (Long) stringRedisTemplate.execute(script, keys);
    System.out.println("脚本执行的结果:" + result);
}

十、使用Redis的lua脚本,实现分布式锁

package com.qf.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.UUID;

@Component
public class LockUtil {

    @Autowired
    private StringRedisTemplate redisTemplate;

    //加锁的lua脚本
    private String lockLua = "--锁的名称\n" +
            "local lockName=KEYS[1]\n" +
            "--锁的value\n" +
            "local lockValue=ARGV[1]\n" +
            "--过期时间 秒\n" +
            "local timeout=tonumber(ARGV[2])\n" +
            "--尝试进行加锁\n" +
            "local flag=redis.call('setnx', lockName, lockValue)\n" +
            "--判断是否获得锁\n" +
            "if flag==1 then\n" +
            "--获得分布式锁,设置过期时间\n" +
            "redis.call('expire', lockName, timeout)\n" +
            "end\n" +
            "--返回标识\n" +
            "return flag ";

    //解锁的lua脚本
    private String unLockLua = "--锁的名称\n" +
            "local lockName=KEYS[1]\n" +
            "--锁的value\n" +
            "local lockValue=ARGV[1]\n" +
            "--判断锁是否存在,以及锁的内容是否为自己加的\n" +
            "local value=redis.call('get', lockName)\n" +
            "--判断是否相同\n" +
            "if value == lockValue then\n" +
            "     redis.call('del', lockName)\n" +
            "     return 1\n" +
            "end\n" +
            "return 0";

    private ThreadLocal<String> tokens = new ThreadLocal<>();

    /**
     * 加锁
     * @return
     */
    public void lock(String lockName){
        lock(lockName, 30);
    }

    public void lock(String lockName, Integer timeout){

        String token = UUID.randomUUID().toString();
        //设置给threadLocal
        tokens.set(token);

        //分布式锁 - 加锁
        Long flag = (Long) redisTemplate.execute(new DefaultRedisScript(lockLua, Long.class),
                Collections.singletonList(lockName),
                token, timeout + ""
        );

        System.out.println("获得锁的结果:" + flag);

        //设置锁的自旋
        if (flag == 0) {
            //未获得锁
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            lock(lockName, timeout);
        }
    }

    /**
     * 解锁
     * @return
     */
    public boolean unlock(String lockName){

        //获得ThreadLocal
        String token = tokens.get();

        //解锁
        Long result = (Long) redisTemplate.execute(new DefaultRedisScript(unLockLua, Long.class),
                Collections.singletonList(lockName),
                token);

        System.out.println("删除锁的结果:" + result);

        return result == 1;
    }
}
       } catch (InterruptedException e) {
            e.printStackTrace();
        }

        lock(lockName, timeout);
    }
}

/**
 * 解锁
 * @return
 */
public boolean unlock(String lockName){

    //获得ThreadLocal
    String token = tokens.get();

    //解锁
    Long result = (Long) redisTemplate.execute(new DefaultRedisScript(unLockLua, Long.class),
            Collections.singletonList(lockName),
            token);

    System.out.println("删除锁的结果:" + result);

    return result == 1;
}

}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值