Redis事务_锁机制——秒杀★★★

Redis6总纲

https://www.jianshu.com/p/901dc5a0c683

目录

一、基本操作(Multi、Exec、discard)
二、乐观锁和悲观锁
三、秒杀案例

一、基本操作

1、Redis的事务定义

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

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

2、Multi、Exec、discard

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

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

image.png

(1)组队案例

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK

(2)放弃组队

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> discard
1) OK

(3)任何一个组队失败,都不会执行

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.

3、事务的错误处理

(1)组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。(就是上面3的操作)


image.png

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


image.png

案例:字符串+1

127.0.0.1:6379> multi 
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range

二、乐观锁和悲观锁

1、事务冲突问题

一个请求想给金额减8000
一个请求想给金额减5000
一个请求想给金额减1000


image.png

2、悲观锁

image.png

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

3、乐观锁

image.png

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

4、WATCH key [key ...]

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

image.png

5、unwatch

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

6、Redis事务三特性

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

三、秒杀案例

1、案例:计数器和人员记录

image.png

初版秒杀代码(没有事务)

@RestController
@RequestMapping("/test")
public class TestController {
    // 秒杀过程
    @RequestMapping("/doSecKill")
    public static boolean doSecKill(String uid, String prodid) throws Exception {

        uid = String.valueOf ((int) (Math.random ( ) * 10000));
        prodid = "0101";
        // 1、入参的判空
        if (uid == null || prodid == null) {
            return false;
        }

        // 2.连接redis
        Jedis jedis = new Jedis ("106.52.23.202", 6379);

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

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

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

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

        //7 秒杀过程
        //7.1 库存-1
        jedis.decr (kcKey);
        //7.2 把秒杀成功用户添加清单里面
        jedis.sadd (userKey, uid);
        System.out.println ("秒杀成功了..");
        jedis.close ( );
        return true;
    }
}

redis 添加测试数据

[root@VM-0-13-centos redis]# docker exec -it redis redis-cli
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set sk:0101:qt 10
OK
127.0.0.1:6379> keys *
1) "sk:0101:qt"
127.0.0.1:6379> keys *
1) "sk:0101:user"
2) "sk:0101:qt"
127.0.0.1:6379> get sk:0101:qt
"9"
127.0.0.1:6379> smembers sk:0101:user
1) "1025"

-------------------在客户端访问(进行秒杀)-------------------------
-----点了三次后-----

127.0.0.1:6379> smembers sk:0101:user
1) "1025"
2) "2281"
3) "2756"
4) "2927"
127.0.0.1:6379> get sk:0101:qt
"6"
127.0.0.1:6379> 

2、用 ab工具 模拟秒杀并发

(1)安装,yum install httpd-tools

(2)编写并发文件 和格式
vim postfile 模拟表单提交参数,以&符号结尾;存放当前目录。
内容:

prodid=0101&

2000个请求,每次200个并发同时进行
ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://106.52.23.202:8080/test/doSecKill

问题一:超卖,最终出现了问题,-4


image.png

问题二:连接超时问题


image.png

3、解决以上问题

(1)连接池工具类(防止超时)

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(100*1000);
                    poolConfig.setBlockWhenExhausted(true);
                    poolConfig.setTestOnBorrow(true);  // ping  PONG

                    jedisPool = new JedisPool(poolConfig, "192.168.44.168", 6379, 60000 );
                }
            }
        }
        return jedisPool;
    }

    // 释放资源,  3.0 后重新启用 close
    public static void release(Jedis jedis) {
        if (null != jedis) {
            jedis.close ();
        }
    }
}
image.png

(2)解决超卖代码(修改上面的方法)

    public static boolean doSecKill(String uid, String prodid) throws Exception {
        uid = String.valueOf ((int) (Math.random ( ) * 10000));
        prodid = "0101";
        // 1、入参的判空
        if (uid == null || prodid == null) {
            return false;
        }

        // 2.连接redis
        //Jedis jedis = new Jedis ("106.52.23.202", 6379);
        JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance ( );
        Jedis jedis = jedisPoolInstance.getResource ( );

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

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

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

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

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

        //7 秒杀过程
        //使用事务
        Transaction multi = jedis.multi ( );

        //组队操作
        multi.decr (kcKey);
        multi.sadd (userKey,uid);

        //执行
        List results = multi.exec ( );
        if (results ==null || results.size () ==0){
            System.out.println ("秒杀失败......" );
            jedis.close ();
            return false;
        }

        //7.1 库存-1
        //jedis.decr (kcKey);
        //7.2 把秒杀成功用户添加清单里面
        //jedis.sadd (userKey, uid);

        System.out.println ("秒杀成功了..");
        jedis.close ( );
        return true;
    }
 
 

(3)库存遗留问题
秒杀完之后,还有库存:
如果同时有2000人抢到500个商品,其中一个人抢到了并且把版本号从1.0改成2.0,就会导致其余1999个人版本号不同,无法再往加操作,导致修改失败,乐观锁带来的弊端。

(4)库存遗留问题解决方案:用lua脚本语言(仅支持redis2.6以上):

原理:实际是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。
lua代码:

第一行,第二行:表示定义两个变量,传入两个参数
第三行、第四行:拼接库存key,用户key
第五行:判断当前用户是否在清单中存在(sismember和代码中5,调用)
returm 2,表示一个约定,秒杀过了
returm 0,表示一个约定,秒杀结束
returm 1,表示一个约定,秒杀成功

local userid=KEYS[1]; 
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":qt";
local usersKey="sk:"..prodid.":usr'; 
local userExists=redis.call("sismember",usersKey,userid);
if tonumber(userExists)==1 then 
  return 2;
end
local num= redis.call("get" ,qtkey);
if tonumber(num)<=0 then 
  return 0; 
else 
  redis.call("decr",qtkey);
  redis.call("sadd",usersKey,userid);
end
return 1;

lua脚本工具类

public class SecKill_redisByScript {

    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=  .secKillScript;
        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;
    }
}

lua调用

boolean isSuccess= SecKill_redisByScript.doSecKill(userid,prodid);

小坑记录

1、springboot 打 jar失败
把插件的版本号去掉


image.png

你可能感兴趣的:(Redis事务_锁机制——秒杀★★★)