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的数据