Redis---事务篇

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis---事务篇相关的知识,希望对你有一定的参考价值。


Redis的事务定义

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

Redis事务的主要作用就是串联多个命令防止别的命令插队


Multi、Exec、discard命令

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行

组队的过程中可以通过discard来放弃组队


案例1:组队成功,提交成功


案例2:组队阶段报错,提交失败


提交失败,一条命令都不会执行


案例3:组队成功,提交有成功有失败情况


事务的错误处理

组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消


如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。


事务冲突的问题

例子

我有10000元

一个请求想给金额减8000

一个请求想给金额减5000

一个请求想给金额减1000


解决办法

悲观锁

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上

锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读

锁,写锁等,都是在做操作之前先上锁。


乐观锁


**乐观锁(**Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时

候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐

量。Redis就是利用这种check-and-set机制实现事务的


乐观锁在Redis中的应用

WATCH key [key …] 命令

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

如果两个终端同时对当前key进行修改,一个终端修改完后,对应key的版本号更新,那么另一个终端修改完之后,需要比对当前key的版本是否已经更新,如果更新了,那么当前终端的操作就失效了,即事务被打断了

演示: a终端先执行exec提交事务指令,b终端后执行

a终端:

b终端:


unwatch 命令

取消 WATCH 命令对所有 key 的监视

如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了


Redis事务三特性

单独的隔离操作

事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

没有隔离级别的概念

队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

不保证原子性

事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚


Redis命令大全

Redis 命令参考


秒杀案例


设置商品的id为0101,库存10件

秒杀页面:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>秒杀页面</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <!-- 引入 layui.css -->
  <link rel="stylesheet" href="//unpkg.com/layui@2.6.8/dist/css/layui.css"/>

</head>
<body>

<div class="layui-carousel" id="test1">
  <div carousel-item>
    <div><img src="ms1.png" width="1696px" height="280" ></div>
    <div><img src="ms2.png" width="1696px" height="280" ></div>
  </div>
</div>
<div style="text-align:center">
  <button type="button" class="layui-btn layui-btn-lg" id="msBtn">秒杀</button>
</div>


<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<!-- 引入 layui.js -->
<script src="//unpkg.com/layui@2.6.8/dist/layui.js"/>
<!-- 条目中可以是任意内容,如:<img src=""> -->
<script src="/static/build/layui.js"></script>
<script>
  layui.use(['layer', 'form'], function(){
    var layer = layui.layer
            ,form = layui.form;
  });

  layui.use('carousel', function(){
    var carousel = layui.carousel;
    //建造实例
    carousel.render({
      elem: '#test1'
      ,width: '100%' //设置容器宽度
      ,arrow: 'always' //始终显示箭头
      //,anim: 'fade' //切换动画方式
    });
  });
  //按钮每次点击一下,那么都会发送请求到/ms
    $("#msBtn").click(function (){
      $.ajax(
              {
               url:"ms",
                type:"get",
              }
      )
    });
</script>
</body>

controller层代码:

@RestController
@RequestMapping("/ms")
public class RedisTestController
{
    @Autowired
    private RedisTemplate redisTemplate;

    @GetMapping
    public String testRedis()
    {
         //每次点击秒杀按钮生成一个随机的四位数字的用户id
            String uid = UUID.randomUUID().toString().substring(0, 3);
      doSecKill("0101",uid);
         return "";
    }
    //秒杀过程: pid秒杀商品的id , uid秒杀商品的用户的id
    public boolean doSecKill(String pid,String  uid)
    {
    //1.uid和pid的非空判断
        if(uid==null||pid==null)
            return false;
        //2.拼接key
        //库存key
        String kcKey="sk:"+pid+":qt";
        //秒杀成功的用户id
        String userKey="sk:"+uid+":qt";
        //4.获取库存,如果库存为空,则秒杀还未开始
        Integer kc= (Integer) redisTemplate.opsForValue().get(kcKey);
        if(kc==null)
        {
            System.out.println("秒杀还未开始,敬请期待...");
            return false;
        }
        //5.判断用户是否重复秒杀---判断set集合中是否已经存在对应的用户Id
       if(redisTemplate.opsForSet().isMember(userKey, uid))
       {
           System.out.println("已经秒杀成功了,不能重复秒杀");
           return false;
       }
     //6.如果商品的数量小于1,那么秒杀结束
        if(kc<1)
        {
            System.out.println("秒杀结束");
            return false;
        }
     //7.秒杀过程: 库存减去1,加入秒杀成功的用户id
        redisTemplate.opsForValue().decrement(kcKey);
        redisTemplate.opsForSet().add(userKey,uid);
        System.out.println("秒杀成功");
        return true;
    }
}


秒杀并发模拟

使用工具ab模拟测试

联网:yum install httpd-tools


ab -help :可以查看ab工具的使用说明

vim postfile 模拟表单提交参数,以&符号结尾;存放当前目录。

内容:prodid=0101&

如果不加注解,那么参数的名字需要和请求体中参数名字相同,才会完成赋值操作,因为spring底层就是通过名字匹配的,要不然就是别名匹配

-n :当前的请求次数

-c :当前的并发次数

-n 2000 -c 200 :2000个请求中有200个并发操作,即有200个操作在同一时刻发生

-T :提交的数据类型,类型设置为 :'application/x-www-form-urlencoded’

-p postfile :用post方式提交,需要把参数放到一个文件中


最终要执行的命令:

ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://172.28.36.243:8080/ms

记住localhost要用主机ip地址替换


测试前设置存储为10


高并发问题出现:



超卖和超时问题解决

连接超时,通过连接池解决

连接池

节省每次连接redis服务带来的消耗,把连接好的实例反复利用。

通过参数管理连接的行为

如果没有连接池,每一次的存取都需要新建一个连接,使用完后再断开,如果是频繁访问的场景,那也太不划算了。有了连接池,就相当于有了一个“大池子”,池子里的连接都是通着的。你如果想连接,自己去池子里找,自助连接。


连接池参数

MaxTotal:控制一个pool可分配多少个jedis实例,通过pool.getResource()来获取;如果赋值为-1,则表示不限制;如果pool已经分配了MaxTotal个jedis实例,则此时pool的状态为exhausted。

maxIdle:控制一个pool最多有多少个状态为idle(空闲)的jedis实例;

MaxWaitMillis:表示当borrow一个jedis实例时,最大的等待毫秒数,如果超过等待时间,则直接抛
JedisConnectionException;

testOnBorrow:获得一个jedis实例的时候是否检查连接可用性(ping());如果为true,则得到的jedis实例均是可用的;

java代码配置:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Created by silivar on 2018/11/3.
 * 连接池
 */
public class RedisUtil {
    private RedisUtil() {
    }

    private static String ip = "localhost";
    private static int port = 6379;
    //向redis要连接池的超时时间
    private static int timeout = 10000;
    //进入redis的密码
    //private static String auth = "root";
    private static JedisPool pool = null;

    static {
        JedisPoolConfig config = new JedisPoolConfig();
        //最大连接数,默认是1万
        config.setMaxTotal(1024);
        //最大空闲实例数
        config.setMaxIdle(200);
        //等连接池给连接的最大时间,毫秒,设成-1表示永远不超时
        config.setMaxWaitMillis(10000);
        //borrow一个实例的时候,是否提前进行validate操作
        config.setTestOnBorrow(true);

        pool = new JedisPool(config, ip, port, timeout);
    }

    //得到redis连接
    public synchronized static Jedis getJedis() {
        if (pool != null) {
            return pool.getResource();
        } else {
            return null;
        }
    }

    //关闭redis连接
    public static void close(final Jedis redis) {
        if (redis != null) {
            redis.close();
        }
    }
}

下面是springboot整合redis的配置:

#Redis服务器地址
spring.redis.host=192.168.112.128
#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

【Redis】连接池的使用


超卖问题

利用乐观锁淘汰用户,解决超卖问题


如果不是整合springboot,而是使用原生的redis,那么增加乐观锁后的代码如下:

//增加乐观锁
jedis.watch(qtkey);
 
//3.判断库存
String qtkeystr = jedis.get(qtkey);
if(qtkeystr==null || "".equals(qtkeystr.trim())) {
System.out.println("未初始化库存");
jedis.close();
return false ;
}
 
int qt = Integer.parseInt(qtkeystr);
if(qt<=0) {
System.err.println("已经秒光");
jedis.close();
return false;
}
 
//增加事务
Transaction multi = jedis.multi();
 
//4.减少库存
//jedis.decr(qtkey);
multi.decr(qtkey);
 
//5.加人
//jedis.sadd(usrkey, uid);
multi.sadd(usrkey, uid);
 
//执行事务
List<Object> list = multi.exec();
 
//判断事务提交是否失败
if(list==null || list.size()==0) {
System.out.println("秒杀失败");
jedis.close();
return false;
}
System.err.println("秒杀成功");
jedis.close();

整合springboot后的代码:

@RestController
@RequestMapping("/ms")
public class RedisTestController
{
    @Autowired
    private RedisTemplate redisTemplate;

    @PostMapping
    public String testRedis(String prodid)
    {
         //每次点击秒杀按钮生成一个随机的四位数字的用户id
            String uid = UUID.randomUUID().toString().substring(0, 3);
      doSecKill(prodid,uid);
         return "";
    }
    //秒杀过程: pid秒杀商品的id , uid秒杀商品的用户的id
    public boolean doSecKill(String pid,String  uid)
    {
    //1.uid和pid的非空判断
        if(uid==null||pid==null)
            return false;
        //2.拼接key
        //库存key
        String kcKey="sk:"+pid+":qt";
        //秒杀成功的用户id
        String userKey="sk:"+uid+":qt";
        //4.获取库存,如果库存为空,则秒杀还未开始
        Integer kc= (Integer) redisTemplate.opsForValue().get(kcKey);
        if(kc==null)
        {
            System.out.println("秒杀还未开始,敬请期待...");
            return false;
        }
        //5.判断用户是否重复秒杀---判断set集合中是否已经存在对应的用户Id
        if(redisTemplate.opsForSet().isMember(userKey, uid))
        {
            System.out.println("已经秒杀成功了,不能重复秒杀");
            return false;
        }
     //6.如果商品的数量小于1,那么秒杀结束
        if以上是关于Redis---事务篇的主要内容,如果未能解决你的问题,请参考以下文章

Redis---事务篇

高级Java程序员必问,Redis事务终极篇

Redis篇:事务和lua脚本的使用

今天来叨唠一下“Redis事务”

redis在php中的应用(Trancation篇)

redis 简单整理——redis 准备篇[一]