Redis

一、Redis的安装和启动和关闭

安装:在Linux中使用 wegt 命令下载,或使用 xftp 软件传入Linux中,使用如下命令解压安装

1
2
3
4
5
6
7
8
//使用wegt命令下载或使用xftp传输文件
wegt `下载地址去官网搜索`

//使用解压命令
tar -xvf `压缩包文件名`

//使用如下命令安装
make && make install

启动

命令 作用
redis-benchmark 性能测试工具,可以在自己本子运行,看看自己本子性能如何
redis-check-aof 修复有问题的AOF文件,rdb和aof后面讲
redis-check-dump 修复有问题的dump.rdb文件
redis-sentinel Redis集群使用
redis-server(不建议使用) Redis服务器启动命令
redis-cli 客户端,操作入口
  • 前台启动(不推荐)

    使用 redis-server ,窗口不能关闭,服务器停止

  • 后台启动(推荐)

    • 备份 redis.cong文件,这里拷贝一份文件到 etc目录下面
    • 需要将 redis.conf 文件中的 daemonize no 改成yes
    • 启动命令:redis-server /etc/redis.conf
  • 客户端访问:redis-cli / redis-cli —raw 【 方式一:不能读取中文,方式二,可以读取中文】

关闭

  • 单实例关闭:redis-cli shutdown:

    image-20210526172727665

  • redis-cli 进入终端关闭:

    image-20210526172924480

  • 多实例关闭:指定端口号关闭

    • 方式一:

      1
      redis-cli -p 端口号 shuwdown
    • 方式二:

      1
      2
      3
      4
      ps -ef | grep '进程名字'
      例如:ps -ef | grep tomcat

      kill -9 `通过进程查询id号,通过查到的id删除j`

二、常用五大数据类型

redis数据库常用操作命令:http://www.redis.cn/commands.html

Redis键值(key)

命令 作用
keys 【keys 2】 查询当前库所有key 【查询指定库】
exists key 判断某个key是否存在
type key 查看你的key是什么类型
del key 删除指定的key数据
unlink key 根据value选择非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作。
expire key 10 10秒中:为指定的key设置过期时间
ttl key 查看还有多少秒过期,-1 表示永不过期,-2 表示已过期
select 指定数据库序号 切换数据库
dbsize 查看当前数据库的key的数量
flushdb 清空当前数据库
flushall 通杀全部数据库

Redis 字符串(String)

String是Redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。

String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。

String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M

命令 作用
set 添加键值对
get 查询对应的键值
append 将给定的追加到原值的末尾
strlen 获取值的长度
incr 将key中存储的数字值增1,只能对数字操作,如果为空,新增值为1
decr 将key中存储的数字值减1,只能对数字值操作,如果为空,新增值为-1
incrby / decrby <步长> 将key中存储的数字值增减,自定义步长
mset [ … ] 同时设置一个或多个key-value
mget [ ….. ] 同时获取一个或多个value
msetnx [ … ] 同时设置一个或多个key-value,当所有的key都不存在时将插入,如果有一个key已存在,所有的key将插入失败
getrange <起始位置> <结束位置> 获得值的范围,类似Java中substring,前包,后包
setrange <起始位置> 用覆写所存储的字符串值,从<起始位置>开始 (索引从0开始)
setex <过期时间> 设置键的同时,设置过期时间,单位秒
getset 以旧换新,设置了新值的同时获得旧值

set 命令中的属性值的问题

image-20210526184545861

属性 作用
*EX key的超时秒数
*PX key的超时秒数,与EX互斥
*NX 当数据库中的key不存在时,可以将key-value添加到数据库中
*XX 当数据库中的key存在时,可以将key-value添加到数据库中,与NX参数互斥

Redis列表(List)

在Redis中列表是一个双向链表

命令 作用
lpush / rpush …. 从左边 / 右边插入一个或多个值
lpop / rpop 从左边 / 右边吐出一个值。值在键在,值亡键亡,值取出就没了
rpoplpush 从列表右边吐出一个值,插到列表左边
lrange 按照索引下标获得元素(从左到右)
lrange mylist 0 -1 0 左边第一个,-1 右边第一个 ( 0 -1)表示获取所有
lindex 按照索引下标获得元素(从左到右)
lien 获得列表长度
linsert before 在 的后面插入
lrem 从左边删除第n个value(从左到右)
lset 将列表key下标为index的值替换成为value

Redis集合(Set)

当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择

sadd 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
smembers 取出该集合的所有值。
sismember 判断集合是否为含有该值,有1,没有0
scard 返回该集合的元素个数。
srem …. 删除集合中的某个元素。
spop 随机从该集合中吐出一个值。
srandmember 随机从该集合中取出n个值。不会从集合中删除 。
smove value 把集合中一个值从一个集合移动到另一个集合
sinter 返回两个集合的交集元素。
sunion 返回两个集合的并集元素。
sdiff 返回两个集合的差集元素(key1中的,不包含key2中的)

三、SpringBoot整合Redis

  1. 导入Jar包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  2. 配置文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #Redis服务器地址
    spring.redis.host=192.168.140.136
    #Redis服务器连接端口
    spring.redis.port=6379
    #Redis数据库索引(默认为0)
    spring.redis.database= 0
    #连接超时时间(毫秒)
    spring.redis.timeout=1800000
    #连接池最大连接数(使用负值表示没有限制)
    spring.redis.lettuce.pool.max-active=20
    #最大阻塞等待时间(负数表示没限制)
    spring.redis.lettuce.pool.max-wait=-1
    #连接池中的最大空闲连接
    spring.redis.lettuce.pool.max-idle=5
    #连接池中的最小空闲连接
    spring.redis.lettuce.pool.min-idle=0
    #使用springboot集群需要设置spring.redis.cluster.nodes的值
    spring.redis.cluster.nodes=192.168.162.132:6380
  1. 编写配置类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    package com.lf.config;

    import com.fasterxml.jackson.annotation.JsonAutoDetect;
    import com.fasterxml.jackson.annotation.PropertyAccessor;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.cache.CacheManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializationContext;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;

    import java.time.Duration;


    /**
    * Redis的配置类
    * User: 李飞
    * Date: 2021/5/27
    * Time: 8:28
    */
    @Configuration
    public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    RedisSerializer<String> redisSerializer = new StringRedisSerializer();
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(om);
    template.setConnectionFactory(factory);
    //key序列化方式
    template.setKeySerializer(redisSerializer);
    //value序列化
    template.setValueSerializer(jackson2JsonRedisSerializer);
    //value hashmap序列化
    template.setHashValueSerializer(jackson2JsonRedisSerializer);
    return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
    RedisSerializer<String> redisSerializer = new StringRedisSerializer();
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    //解决查询缓存转换异常的问题
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(om);
    // 配置序列化(解决乱码的问题),过期时间600秒
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
    .entryTtl(Duration.ofSeconds(600))
    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
    .disableCachingNullValues();
    RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
    .cacheDefaults(config)
    .build();
    return cacheManager;
    }
    }
  2. 测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package com.lf.controller;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    public class RedisController {
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping("/")
    public String testRedis() {
    //设置值到redis
    redisTemplate.opsForValue().set("name", "lucy");
    //从redis获取值
    String name = (String) redisTemplate.opsForValue().get("name");
    return name;
    }

    }

四、Redis_事务

五、秒杀问题

1、每次只有一个用户进行秒杀

  1. 导包:看使用SpringMVC还是SpringBoot再导其他包

    1
    2
    3
    4
    <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    </dependency>
  2. Java代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    public boolean doSecKill( String prodId,  String userId) {
    //1、userId和prodId非空判断
    if (userId == null || prodId == null) {
    return false;
    }

    //2、连接jedis
    Jedis jedis = new Jedis("192.168.162.132", 6379);


    //3 拼接key
    // 3.1 库存key
    String kcKey = "sk:" + prodId + ":qt";
    // 3.2 秒杀成功用户key
    String userKey = "sk:" + prodId + ":user";

    //监视库存
    jedis.watch(kcKey);

    Object key = jedis.get(kcKey);
    //4、获取库存,如果库存null,秒杀还没有开始
    if (key == null) {
    System.out.println("秒杀还没有开始,请等待");
    jedis.close();
    return false;
    }

    //5、判断用户是否重复秒杀操作,判断用户的key是否已经存在了,存在则不能秒杀了
    if (jedis.sismember(userKey, userId)) {
    System.out.println("已经秒杀成功,不能重复秒杀");
    jedis.close();
    return false;
    }

    //6、判断如果剩余商品数量,库存数量小于1,秒杀结束
    if (Integer.parseInt(jedis.get(kcKey)) < 1) {
    System.out.println("秒杀已结束");
    jedis.close();
    return false;
    }

    //7、秒杀过程,将用户的key存入set集合中,set集合中的值不能有重复的
    jedis.sadd(userKey, userId);
    System.out.println("秒杀成功");

    //7.1、库存-1,decr:使数字类型的数据减 1
    jedis.decr(kcKey);
    jedis.close();

    //7.2、把秒杀成功用户添加到清单里面

    return true;
    }

2、模拟高并发下的场景进行秒杀

模拟多个用户同时进行秒杀

在高并发的场景下面可能会出现的问题和解决方案

  • 模拟高并发:在Linux中ab模拟测试工具:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #选择安装
    yum install httpd-tools

    #使用:
    ab
    -n 填写请求数量
    -c 填写并发数量
    -p 填写提交的参数,可以传入文件
    -T 提交的参数的类型,就是表单中enctype的值
    最后再填写请求的路径

    #例:
    ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://http://192.168.58.1/:8080/
    ab -n 1000 -c 100 http://192.168.58.1:8080/spike/1/1
  • 解决库存超卖问题,这种方法虽然解决了库存超卖问题,但是还是会出现库存遗留问题,还是需要使用Lua脚本来解决库存遗留问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #使用监听和事务,事务监听在事务之前就要使用

    #进行键监听
    jedis.watch('需要监听的key');

    #开启乐观锁,在需要进行事务操作的部分将使用原来的jedis使用为Transaction来进行操作
    Transaction multi = jedis.multi();
    #使用返回的multi来使用乐观锁
    multi.sadd(key,value);
    #没有问题之后执行exec方法提交执行的语句
    List<Object> results = multi.exec();
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    /**
    * 该方法会出现库存遗留问题
    */
    @GetMapping("/spike/{userId}/{prodId}")
    public boolean doSecKill(@PathVariable String prodId, @PathVariable String userId) {
    //userId和prodId非空判断
    if (userId == null || prodId == null) {
    return false;
    }
    userId = String.valueOf(new Random().nextInt());

    //连接jedis
    //Jedis jedis = new Jedis("192.168.162.132", 6379);

    //获取连接池
    JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
    //通过连接池获取jedis对象
    Jedis jedis = jedisPool.getResource();


    //3 拼接key。 3.1 库存key
    String kcKey = "sk:" + prodId + ":qt";
    // 3.2 秒杀成功用户key
    String userKey = "sk:" + prodId + ":user";

    //增加乐观锁,监视库存
    jedis.watch(kcKey);

    Object key = jedis.get(kcKey);
    //4、获取库存,如果库存null,秒杀还没有开始
    if (key == null) {
    System.out.println("秒杀还没有开始,请等待");
    jedis.close();
    return false;
    }

    //5、判断用户是否重复秒杀操作
    if (jedis.sismember(userKey, userId)) {
    System.out.println("已经秒杀成功,不能重复秒杀");
    jedis.close();
    return false;
    }

    //6、判断如果商品数量,库存数量小于1,秒杀结束
    if (Integer.parseInt(jedis.get(kcKey)) < 1) {
    System.out.println("秒杀已结束");
    jedis.close();
    return false;
    }

    //7、秒杀过程

    //使用事务
    Transaction multi = jedis.multi();
    //组队操作
    multi.decr(kcKey);
    multi.sadd(userKey, userId);
    //执行
    List<Object> results = multi.exec();
    if (results == null || results.size() == 0) {
    System.out.println("秒杀失败了 ..... ");
    jedis.close();
    return false;
    }

    //秒杀成功
    System.out.println("秒杀成功");
    jedis.close();

    return true;
    }
  • 解决高并发下的库存遗留问题,用户的数量超过库存的数量,结果因为乐观锁导致库存没有卖完,使用 Lua脚本语言来解决

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    import org.slf4j.LoggerFactory;
    import redis.clients.jedis.HostAndPort;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    import java.io.IOException;
    import java.util.HashSet;
    import java.util.Set;

    public class SpikeRedisByScript {
    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(SpikeRedisByScript.class);

    //这是一段Lua脚本
    static String secKillScript ="local userid=KEYS[1];\r\n" +
    "local prodid=KEYS[2];\r\n" +
    "local qtkey='sk:'..prodid..\":qt\";\r\n" +
    "local usersKey='sk:'..prodid..\":usr\";\r\n" +
    "local userExists=redis.call(\"sismember\",usersKey,userid);\r\n" +
    "if tonumber(userExists)==1 then \r\n" +
    " return 2;\r\n" +
    "end\r\n" +
    "local num= redis.call(\"get\" ,qtkey);\r\n" +
    "if tonumber(num)<=0 then \r\n" +
    " return 0;\r\n" +
    "else \r\n" +
    " redis.call(\"decr\",qtkey);\r\n" +
    " redis.call(\"sadd\",usersKey,userid);\r\n" +
    "end\r\n" +
    "return 1" ;

    static String secKillScript2 =
    "local userExists=redis.call(\"sismember\",\"{sk}:0101:usr\",userid);\r\n" +
    " return 1";

    public static boolean doSecKill(String uid, String prodid) throws IOException {

    JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance();
    Jedis jedis = jedispool.getResource();

    String sha1 = jedis.scriptLoad(secKillScript);
    Object result = jedis.evalsha(sha1, 2, uid, prodid);

    String reString = String.valueOf(result);
    if ("0".equals(reString)) {
    System.err.println("已抢空!!");
    } else if ("1".equals(reString)) {
    System.out.println("抢购成功!!!!");
    } else if ("2".equals(reString)) {
    System.err.println("该用户已抢过!!");
    } else {
    System.err.println("抢购异常!!");
    }
    jedis.close();
    return true;
    }
    }

持久化-RDB

持久化-AOF

AOF(Append Only File):追加文件,不允许改写文件,记录编写的指令和数据

  • RDB默认开启的,AOF默认是不开启的,需要在配置文件中开启,将appendonly的值设置为 yes,
  • AOF的文件生成的路径是和RDB文件的生成路径是一致的
  • 如果AOF和RDB同时开启,系统默认读取AOP的数据