Redis应如何使用?

 Redis列表(List)

  • Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
  • 它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
  • 有序,元素可以重复
  • lpush/rpush  <key>  <value1>  <value2>  <value3> ....     从左边/右边插入一个或多个值。
  • lpop/rpop  <key>              从左边/右边吐出一个值。值在键在,值光键亡
  • rpoplpush  <key1>  <key2>    从<key1>列表右边吐出一个值,插到<key2>列表左边。
  • lrange <key> <start> <stop>
    1. lrange mylist 0 -1              从0开始,-1表示获取所有
    2. 按照索引下标获得元素(从左到右)
  • lindex <key> <index>          按照索引下标获得元素(从左到右)
  • llen <key>                    获得列表长度
  • linsert <key>  before/after <value>  <newvalue>      在<value>的前面/后面插入<newvalue>
  • lrem <key> <n>  <value>                     从左边删除n个value(从左到右)

Redis集合(Set)

  • 自动去重,无序,底层是value值为null的hash 表, 应用场景: 秒杀成功的用户列表
  • 添加,删除,查找的复杂度都是O(1)
  • 一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变
  • sadd <key>  <value1>  <value2> .....
    1. 将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略 
  • smembers <key>                  取出该集合的所有值。
  • sismember <key>  <value>         判断集合<key>是否为含有该<value>值,有1,没有0
  • scard <key>                      返回该集合的元素个数。
  • srem <key> <value1> <value2> ....    删除集合中的某个元素。
  • spop <key>                       随机从该集合中吐出一个值。(吐完以后集合中没有该元素)
  • srandmember <key> <n>            随机从该集合中取出n个值。不会从集合中删除 
  • sinter <key1> <key2>              返回两个集合的交集元素。
  • sunion <key1> <key2>             返回两个集合的并集元素。
  • sdiff <key1> <key2>               返回两个集合的差集元素(key1中的,不包含key2中的)

Redis哈希(Hash)

  • Redis hash 是一个键值对集合。
  • Redis hash是一个string类型的fieldvalue的映射表,hash特别适合用于存储对象。
  • 类似Java里面的Map<String,Object>
  • 主要有以下2种存储方式: 
    1.    每次修改用户的某个属性需要,先反序列化改好后再序列化回去。开销较大。
    2. 通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题
  • hset <key>  <field>  <value>       给<key>集合中的  <field>键赋值<value>
  • hget <key1>  <field>              从<key1>集合<field> 取出 value
  • hmget <key>  <field1> <field2> ...    获取hash结构的多个属性的属性值
  • hmset <key1>  <field1> <value1> <field2> <value2>...     批量设置hash的值
  • hexists <key1>  <field>             查看哈希表 key 中,给定域 field 是否存在。 存在1,不存在0
  • hkeys <key>                       列出该hash集合的所有field
  • hvals <key>                       列出该hash集合的所有value
  • hincrby <key> <field>  <increment>   为哈希表 key 中的域 field 的值加上增量 1   -1   increment表示增量多少,可为负数
  • hsetnx <key>  <field> <value>       将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .
  • java代码操作redis时,一般使用 string存储java对象,gson、fastjson 可以将java对象转为json字符串存入到redis中,获取时再还原为java对象

    Redis有序集合Zset(sorted set)

  • 与普通集合set非常相似,是一个没有重复元素的字符串集合。

  • 不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了
  • 因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
  • 访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
  • zadd  <key> <score1> <value1>  <score2> <value2>…
    1. 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
  • zrange <key>  <start> <stop>  [WITHSCORES]  
    1. 返回有序集 key 中,下标在<start> <stop>之间的元素
    2. 带WITHSCORES,可以让分数一起和值返回到结果集。
  • zrangebyscore key min max [withscores] [limit offset count]
    1. 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。
  • zrevrangebyscore key max min [withscores] [limit offset count]              
    1. 同上,改为从大到小排列。
  • zincrby <key> <increment> <value>      为元素的score加上增量
  • zrem  <key>  <value>                删除该集合下,指定值的元素
  • zcount <key>  <min>  <max>          统计该集合,分数区间内的元素个数
  • zrank <key>  <value>                 返回该值在集合中的排名,从0开始。

Redis配置文件介绍

  • 自定义目录:/myredis/redis.conf  使用vim 进行编辑查看

Units单位

  • 配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit
  • 大小写不敏感

includes包含

  • 类似jsp中的include,多实例的情况可以把公用的配置文件提取出来

网络相关配置

bind

  • 默认情况bind=127.0.0.1只能接受本机的访问请求
  • 不写的情况下,无限制接受任何ip地址的访问
  • 生产环境肯定要写你应用服务器的地址;服务器是需要远程访问的,所以需要将其注释掉
  • 如果开启了protected-mode,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的相应
  • 保存配置,停止服务,重启启动查看进程,不再是本机访问了。

protected-mode

  • 将本机访问保护模式设置no
  • 保护模式,和bind一起使用

Port

  • 端口号,默认 6379

tcp-backlog

  • 设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列 + 已经完成三次握手队列。
  • 在高并发环境下你需要一个高backlog值来避免慢客户端连接问题。注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值(128),所以需要确认增大/proc/sys/net/core/somaxconn和/proc/sys/net/ipv4/tcp_max_syn_backlog(128)两个值来达到想要的效果

timeout

  • 一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭

tcp-keepalive

  • 对访问客户端的一种心跳检测,每过n秒检测一次。
  • 单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60

GENERAL通用

daemonize

  • 是否为后台进程,设置为yes
  • 守护进程,后台启动

pidfile

  • 存放pid文件的位置,每个实例会产生一个不同的pid文件

loglevel

  • 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为notice
  • 四个级别根据使用阶段来选择,生产环境选择notice 或者warning

logfile

  • logfile "" 代表不持久化保存日志
  • 日志文件名称

databases 16

  • 设定库的数量 默认16,默认数据库为0,可以使用SELECT <dbid>命令在连接上指定数据库id

SECURITY安全

设置密码

  • 访问密码的查看、设置和取消
    1. 在命令中设置密码,只是临时的。重启redis服务器,密码就还原了。
    2. 永久设置,需要再配置文件中进行设置。

LIMITS限制

maxclients

  • 设置redis同时可以与多少个客户端进行连接。
  • 默认情况下为10000个客户端。
  • 如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应。

maxmemory

  • 建议必须设置,否则,将内存占满,造成服务器宕机
  • 设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。
  • 如果redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。
  • 但是对于无内存申请的指令,仍然会正常响应,比如GET等。如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素。

maxmemory-policy

  • volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)
  • allkeys-lru:在所有集合key中,使用LRU算法移除key
  • volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键
  • allkeys-random:在所有集合key中,移除随机的key
  • volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key
  • noeviction:不进行移除。针对写操作,只是返回错误信息
  • # volatile-lru -> Evict using approximated LRU among the keys with      an expire set.
    		-  对所有的有过期时间的数据按照LRU算法移除
     565 # allkeys-lru -> Evict any key using approximated LRU.
     		- 使用LRU的策略对所有的键进行移除
     566 # volatile-lfu -> Evict using approximated LFU among the keys with      an expire set.
     		- 对有过期时间的数据按照LFU的算法移除
     567 # allkeys-lfu -> Evict any key using approximated LFU.
     568 # volatile-random -> Remove a random key among the ones with an ex     pire set.
     		- 对有过期时间的键使用随机算法进行移除
     569 # allkeys-random -> Remove a random key, any key.
     		
     570 # volatile-ttl -> Remove the key with the nearest expire time (min     or TTL)
     		- 对有过期时间的键,移除即将要过期的键
     571 # noeviction -> Don't evict anything, just return an error on writ     e operations.
     		- 不做任何操作,当内存满了还有写操作时,直接返回一个错误
     572 #
     573 # LRU means Least Recently Used
     		最近最少使用(按时间)  离最近时间最远
     574 # LFU means Least Frequently Used
     		使用次数最少(按次数)   最不常使用

    maxmemory-samples

  • 设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key并选择其中LRU的那个。
  • 一般设置3到7的数字,数值越小样本越不准确,但性能消耗越小。

Redis_Jedis_测试

Jedis所需要的jar包

  • commons-pool2-2.4.2.jar
  • jedis-2.8.1.jar

连接Redis注意事项

  • 禁用Linux的防火墙:Linux(CentOS7)里执行命令:systemctl stop/disable firewalld.service  
  • redis.conf中注释掉bind 127.0.0.1 ,然后 protected-mode no
  1. 创建maven项目
  2. 在pom中引入maven的依赖
<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <version>2.6.1</version>
    </dependency>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>
</dependencies>

3. 创建java类,在main中测试redis的连接

package com.atguigu.jedis;

import redis.clients.jedis.Jedis;

public class JedisTest {
    /**
     * JedisConnectionException: java.net.SocketTimeoutException:
     *      错误排查:
     *             1.1  检查reids是否启动
     *                  xhsell中: ps -aux|grep redis
     *             1.2 检查虚拟机防火墙是否关闭
     *             1.3 redis保护模式必须关闭
     *                  vim  /myredis/redis.conf
     *                     69行 , 注释bind 127.0.0.1
     *                      88行,protected-mode yes改为no
     *                      重启redis服务
     */
    public static void main(String[] args) {
        //java代码连接redis步骤
        //1、获取连接
        Jedis jedis = new Jedis("192.168.1.130", 6379);
        //2、使用连接对象发送命名操作redis
        String ping = jedis.ping();
        System.out.println("ping = " + ping);
        //3、关闭连接
        jedis.close();

    }
}

Redis_事务_锁机制_秒杀

Redis的事务定义

  • Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • Redis事务的主要作用就是串联多个命令防止别的命令插队。

Multi、Exec、discard

  • 从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
  • 组队的过程中可以通过discard来放弃组队。 
  • 组队成功,提交成功

  • 组队阶段保存,提交失败

  • 组队成功,提交有成功,有失败

事务的错误处理

  • 组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
  • 如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

悲观锁

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁表锁等,读锁写锁等,都是在做操作之前先上锁。

  • 悲观锁:请求获取到数据时会加锁,其他请求等待锁释放才可以争抢锁使用[高并发写操作为了保证数据安全可以使用, R数据库]

乐观锁

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量Redis就是利用这种check-and-set机制实现事务的。

  • 乐观锁:对数据加版本号,当请求获取数据时会一起获取到版本号,其他请求读取数据也没有问题,当任意一个请求修改数据时,乐观锁会检查数据的版本号,如果请求数据的版本号和存储数据的版本号一致,可以修改,并提升版本号。如果获取了修改前数据版本号的其他请求再修改数据一定会失败[高并发读操作]

WATCH key[key...]

  • 在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

unwatch

  • 取消 WATCH 命令对所有 key 的监视。
  • 如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。

Redis事务三特性

  • 单独的隔离操作
    1. 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 
  • 没有隔离级别的概念
    1. 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
  • 不保证原子性
    1. 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 

案例分析

  • java程序可以处理用户从程序的页面中提交的秒杀请求
  • 项目中需要存储秒杀商品的数据,用户秒杀成功后,项目中也需要保存秒杀成功的用户

1、在redis中初始化秒杀的库存

在redis中:set sk:10001:qt 5

2、开发秒杀项目:

2.1 创建maven web工程[引入springmvc+spring依赖+commons-pool2+jedis]

 <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
    <!-- 基础框架 -->
    <spring.version>5.2.5.RELEASE</spring.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
      <version>2.6.1</version>
    </dependency>
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.9.0</version>
    </dependency>
    <!-- 基础框架 -->
    <!-- Spring配置 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>${spring.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>${spring.version}</version>
    </dependency>
  </dependencies>

2.2 web.xml中配置

 <!-- DispatcherServlet -->
  <servlet>
    <servlet-name>DispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>
        classpath*:spring/springmvc.xml
      </param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>DispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

2.3 在resources下创建spring/springmvc.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"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- controller组件的扫描 -->
    <context:component-scan base-package="com.atguigu.sk">
    </context:component-scan>
    <!-- 视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>
    <!-- 静态资源放行的Servlet-->
    <mvc:default-servlet-handler></mvc:default-servlet-handler>
    <!-- 注解驱动 -->
    <mvc:annotation-driven></mvc:annotation-driven>
</beans>

2.4 创建秒杀页面

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>尚筹网-秒杀页面</title>
    <script src="jquery/jquery-2.1.1.min.js" type="text/javascript"></script>
</head>
<body>
<form action="sk/doSecondKill" method="post">
    <input type="hidden" name="id" value="10001">
    <a  href="#">点击参与1元秒杀Iphone11promax</a>
</form>
<script type="text/javascript">
    $("a").click(function () {

        $.ajax({
            type:"post",
            url:$("form").prop("action"),
            data:$("form").serialize(),
            success: function (res) {
                if(res=="ok"){
                    alert("秒杀成功");
                }else{
                    alert(res);
                    $("a").prop("disabled" , true);
                }
            }
        });
        return false;
    });
</script>
</body>
</html>

2.5 创建COntroller处理秒杀请求

@RestController //等价于  Controller+方法上的 ResponseBody
public class SecondKillController {

//sk/doSecondKill
    @PostMapping(value = "/sk/doSecondKill",produces = "text/html;charset=UTF-8")
    public String doSk(Integer id){
        //随机生成用户id
        Integer usrid = (int)(10000*Math.random());
        //秒杀商品的id
        Integer pid = id;
        //秒杀业务
        //拼接商品库存的key和用户列表集合的key
        String qtKey = "sk:"+pid+":qt";
        String usrsKey = "sk:"+pid+":usr";

        Jedis jedis = new Jedis("192.168.1.130", 6379);
        //1、判断该用户是否已经秒杀过
        if(jedis.sismember(usrsKey, usrid + "")){
            System.err.println("重复秒杀:"+usrid);
            return "该用户已经秒杀过,请勿重复秒杀";
        }
        //2、获取redis中的库存,判断是否足够
        String qtStr = jedis.get(qtKey);
        if(StringUtils.isEmpty(qtStr)){
            System.err.println("秒杀尚未开始");
            return "秒杀尚未开始";
        }
        int qtNum = Integer.parseInt(qtStr);
        if(qtNum<=0){
            System.err.println("库存不足");
            return "库存不足";
        }
        //3、库存足够,秒杀的业务
        //减库存
        jedis.decr(qtKey);
        //将用户加入到秒杀成功的列表中
        jedis.sadd(usrsKey , usrid+"");
        System.out.println("秒杀成功:"+ usrid);
        return "ok";
    }
}

使用ab模拟高并发测试秒杀项目

1、初始化库存 :set sk:10001:qt 100

2、在xshell中使用ab高并发测试:http://主机ip地址:8888/SecondKill_war/sk/doSecondKill

http://192.168.1.1:8888/SecondKill_war/sk/doSecondKill

先创建postfile.txt文件:文件中保存要提交的请求参数

id=10001&

ab -n8000 -c500 -p postfile.txt -T application/x-www-form-urlencoded http://192.168.1.1:8888/SecondKill_war/sk/doSecondKill

高并发时多线程请求,有可能会导致超卖

利用事务解决超卖问题

每个请求再controller中被处理秒杀请求时,将多个命令组成队列,再对要使用的库存添加乐观锁

在使用库存前,对库存的键添加乐观锁,高并发访问时,每个请求使用数据都会携带版本。多个请求同时获取相同版本的数据,最多只能有一个修改。

 @PostMapping(value = "/sk/doSecondKill",produces = "text/html;charset=UTF-8")
    public String doSk(Integer id){
        //随机生成用户id
        Integer usrid = (int)(10000*Math.random());
        //秒杀商品的id
        Integer pid = id;
        //秒杀业务
        //拼接商品库存的key和用户列表集合的key
        String qtKey = "sk:"+pid+":qt";
        String usrsKey = "sk:"+pid+":usr";

        Jedis jedis = new Jedis("192.168.1.130", 6379);
        //1、判断该用户是否已经秒杀过
        if(jedis.sismember(usrsKey, usrid + "")){
            System.err.println("重复秒杀:"+usrid);
            return "该用户已经秒杀过,请勿重复秒杀";
        }
        //2、获取redis中的库存,判断是否足够
        //对库存的key 进行watch
        //==================================================
        jedis.watch(qtKey);
        String qtStr = jedis.get(qtKey);
        if(StringUtils.isEmpty(qtStr)){
            System.err.println("秒杀尚未开始");
            return "秒杀尚未开始";
        }
        int qtNum = Integer.parseInt(qtStr);
        System.out.println("库存 = " + qtNum);
        if(qtNum<=0){
            System.err.println("库存不足");
            return "库存不足";
        }
        //3、库存足够,秒杀的业务
        //减库存
        //======================================================
        Transaction multi = jedis.multi();//开启redis的组队
        multi.decr(qtKey);
        //将用户加入到秒杀成功的列表中
        multi.sadd(usrsKey , usrid+"");
        multi.exec();
        System.out.println("秒杀成功:"+ usrid);

        jedis.close();
        return "ok";
    }

乐观锁导致的问题:

库存剩余的问题。多个并发的线程同时修改数据时,只能有一个修改成功

公平性的问题,先来的线程由于乐观锁可能会秒杀失败

利用lua脚本解决库存剩余问题

LUA脚本:小脚本语言

redis支持解析lua脚本,lua脚本可以将java代码中的业务逻辑和对redis的操作封装成一个lua脚本,一次性交给redis执行 ,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";

@PostMapping(value = "/sk/doSecondKill",produces = "text/html;charset=UTF-8")
public String doSkByLUA(Integer id){
    //随机生成用户id
    Integer usrid = (int)(10000*Math.random());
    Jedis jedis = new Jedis("192.168.1.130", 6379);
    //加载LUA脚本
    String sha1 = jedis.scriptLoad(secKillScript);
    //将LUA脚本和LUA脚本需要的参数传给redis执行:keyCount:lua脚本需要的参数数量,params:参数列表
    Object obj = jedis.evalsha(sha1, 2, usrid + "", id + "");
    // Long 强转为Integer会报错  ,Lange和Integer没有父类和子类的关系
    int result = (int)((long)obj);
    if(result==1){
        System.out.println("秒杀成功");
        return "ok";
    }else if(result==2){
        System.out.println("重复秒杀");
        return "重复秒杀";
    }else{
        System.out.println("库存不足");
        return "库存不足";
    }
}

//jedis连接池:

public class JedisPoolUtil {
    private static volatile JedisPool jedisPool = null;
    //私有构造器
    private JedisPoolUtil(){}

    public static JedisPool getJedisPoolInstance() {
        if (null == jedisPool) {
            synchronized (JedisPoolUtil.class) {
                if (null == jedisPool) {
                    JedisPoolConfig poolConfig = new JedisPoolConfig();
                    poolConfig.setMaxTotal(200);
                    poolConfig.setMaxIdle(32);
                    poolConfig.setMaxWaitMillis(1 * 1000);
                    poolConfig.setBlockWhenExhausted(true);
                    poolConfig.setTestOnBorrow(true);
                    jedisPool = new JedisPool(poolConfig, "192.168.1.130", 6379, 60000);
                }
            }
        }
        return jedisPool;
    }

    public static void release(JedisPool jedisPool, Jedis jedis) {
        if (null != jedis) {
            jedisPool.returnResource(jedis);
        }
    }

}
//使用
Jedis jedis = JedisPoolUtil.getJedisPoolInstance().getResource();

redis持久化

以快照集的形式 在一定时间间隔后保存到本地硬盘上

当redis服务端需要加载数据时直接可以加载到内存中

 

在redis.conf配置文件中,253行可以修改rdb持久化文件的名称

在263行可以修改redis自己创建文件的基准地址

设置持久化备份文件的存放位置
 263 dir /myredis/
rdb备份的保存策略
 218 save 900 1
 219 save 300 10
 220 save 60 10000

redis中执行命令时,如果执行save(同步)或者bgsave(异步),也可以触发redis使用rbd的方式将内存的数据写入到rdb文件

redis正常关闭时,也会自己调用bgsave完成数据的备份

flushdb也是写指令,如果触发redis的保存策略,也会覆盖dump.rdb文件

 

使用rbd文件防止误删除操作:

1、每隔一段时间对dump.rdb文件进行备份

cp dump.rdb dump-20-5-27.rdb.bak

2、如果redis中有误操作删除了redis中所有的数据

flushdb

3、借助备份rdb文件恢复误删除的数据

先停止redis服务

cp dump-20-5-27.rdb.bak dump.rdb

4、重启redis服务

 

rdb压缩保存,节省磁盘空间,文件不可读

235 stop-writes-on-bgsave-error yes  当bgsave执行时如果有错误停止写操作
 241 rdbcompression yes  压缩保存数据
 250 rdbchecksum no   检查数据完整性,比较消耗性能

rdb优点:

Ø 适合大规模的数据恢复

Ø 对数据完整性和一致性要求不高更适合使用

Ø 节省磁盘空间

Ø 恢复速度快

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

destiny- freedom

感觉怎么样呐?

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值