redis--14--Redis的事务,锁,秒杀案例

redis–14–Redis的事务,锁,秒杀案例


代码位置

https://gitee.com/DanShenGuiZu/learnDemo/tree/master/redis-learn/jedis

1、Redis的事务

1.1、Redis事务是一个单独的隔离操作

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

1.2、主要作用

串联多个命令防止别的命令插队。

1.3、Redis事务三特性

单独的隔离操作

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

没有隔离级别的概念

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

不保证原子性

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

2、Multi、Exec、discard

redis--14--Redis的事务,锁,秒杀案例_第1张图片

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

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

2.1、redis事务过程

  1. multi:告诉程序,下面的命令先不要执行,暂时存起来
  2. queued:该命令进入等待执行的事务队列中
  3. exec:执行事务队列的命令,按顺序执行,顺序返回每条命令的执行结果。

2.2、案例

2.2.1、案例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
127.0.0.1:6379> 

结果:所有命令都成功执行

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

127.0.0.1:6379> Multi
OK
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> set k4  
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> 


结果:所有命令都不执行

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

127.0.0.1:6379> Multi
OK
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> incr k5  
QUEUED
127.0.0.1:6379(TX)> set k6 v6
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> 


结果:除了incr k5 ,其他命令都成功

2.2.4、案例4:取消组队

127.0.0.1:6379> Multi
OK
127.0.0.1:6379(TX)> set k5 v5
QUEUED
127.0.0.1:6379(TX)> incr k5  
QUEUED
127.0.0.1:6379(TX)> set k6 v6
QUEUED
127.0.0.1:6379(TX)> discard
OK
127.0.0.1:6379> 


2.3、事务的错误处理总结

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

3、为什么要做成事务

想想一个场景:有很多人有你的账户,同时去参加双十一抢购,会引起事务冲突问题,

3.1、事务冲突的问题

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

redis--14--Redis的事务,锁,秒杀案例_第2张图片

3.2、悲观锁

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

redis--14--Redis的事务,锁,秒杀案例_第3张图片

3.3、乐观锁

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

Redis就是利用乐观锁check-and-set机制实现事务的。

redis--14--Redis的事务,锁,秒杀案例_第4张图片

4、WATCH key [key …]

  1. 在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,监控一直持续到exec命令
  2. 如果在事务执行之前这个(或这些) key 被修改或者删除(除了键到期自动删除外),那么事务将被打断。

4.1、买票:key 值改变情况

# 当前票数为1
127.0.0.1:6379> set ticket 1
OK
# 用户A的钱500
127.0.0.1:6379> set money 500
OK
# 设置锁,如果票数发生变化,下面的事务不执行
127.0.0.1:6379> watch ticket
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decr ticket
QUEUED
127.0.0.1:6379> decrby money 100
QUEUED
# 在exec之前,另一个用户B购买了票数,之后当前用户A执行exec
127.0.0.1:6379> exec
(nil) :表示用户A的事务没有生效
 
# 事务里面的命令都没有生效,所以钱还是500
127.0.0.1:6379> get money
"500"

另一个用户B,在用户A exec之前,先买了票

127.0.0.1:6379> decr ticket
(integer) 0
127.0.0.1:6379> 

redis--14--Redis的事务,锁,秒杀案例_第5张图片

5、unwatch

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

6、事务_秒杀案例

6.1、解决计数器和人员记录的事务操作

redis--14--Redis的事务,锁,秒杀案例_第6张图片

6.2、ab工具的安装


https://blog.csdn.net/zhou920786312/article/details/122769472

6.3、秒杀–超卖问题

数据初始化


127.0.0.1:6379> flushdb
OK
设置商品111的库存为100
127.0.0.1:6379> set stock:111 100
127.0.0.1:6379> 

代码



 

/**
 * 秒杀案例
 *
 * @author 周飞
 * @class: RedisTestController
 * @date 2022/2/3 14:11
 * @Verson 1.0 -2022/2/3 14:11
 * @see
 */

public class DoSecKill_v1 {
    
 
    
    /**
     *
     * @param userId 用户id
     * @param prodId 商品id
     * @return boolean
     * @author 周飞
     * @since 2022/2/3 16:01
     */
    // 秒杀过程
    public static boolean doSecKill(String userId, String prodId) throws IOException {
        
        // 库存 key
        String stock_key = "stock:" + prodId;
        // 秒杀成功用户key
        String success_user_key = "success:" + prodId;
        
        // 获取jedis
        Jedis jedis = getJedis_v1();
        // 监视库存
        jedis.watch(stock_key);
        
        // 判断用户是否重复秒杀操作
        if (jedis.sismember(success_user_key, userId)) {
            System.out.println("该用户已经秒杀成功");
            jedis.close();
            return false;
        }
        
        // 获取库存数量
        String stock_count = jedis.get(stock_key);
        
        // 判断如果商品数量,库存数量小于1,秒杀结束
        if (Integer.parseInt(stock_count) <= 0) {
            System.out.println("秒杀已经结束了");
            jedis.close();
            return false;
        }
        
        // 库存-1
        jedis.decr(stock_key);
        // 把秒杀成功用户添加清单里面
        jedis.sadd(success_user_key, userId);
        
        System.out.println("秒杀成功了..");
        jedis.close();
        return true;
    }

    
    /**
     * 构建Jedis
     *
     * @author 周飞
     * @since 2022/2/3 15:18
     */
    public static Jedis getJedis_v1() {
        // 连接redis
        return new Jedis("192.168.187.138", 6379);
    }
    
}
@RestController
public class SecondKillController {
    
    /**
     * @param prodId 商品id
     * @author 周飞
     * @since 2022/2/3 14:51
     */
    @RequestMapping("/second_kill")
    public boolean secondKill(@RequestParam("prodId") String prodId) throws IOException {
        
        String userId = new Random().nextInt(50000) + "";
        return DoSecKill_v1.doSecKill(userId, prodId);
    }
    
}

redis--14--Redis的事务,锁,秒杀案例_第7张图片

压测和结果

ab -n 2000 -c 200 -k  http://192.168.43.45:8080/second_kill?prodId=111

 

redis--14--Redis的事务,锁,秒杀案例_第8张图片

超卖问题

redis--14--Redis的事务,锁,秒杀案例_第9张图片

超卖原理

redis--14--Redis的事务,锁,秒杀案例_第10张图片

6.4、利用乐观锁淘汰用户,解决超卖问题–>引发过剩问题

原理

redis--14--Redis的事务,锁,秒杀案例_第11张图片

代码改动

redis--14--Redis的事务,锁,秒杀案例_第12张图片

redis--14--Redis的事务,锁,秒杀案例_第13张图片


// 利用乐观锁淘汰用户,解决超卖问题
// 使用事务
Transaction multi = jedis.multi();

// 组队操作
multi.decr(stock_key);
multi.sadd(success_user_key, userId);

// 执行
List results = multi.exec();

if (results == null || results.size() == 0) {
    System.out.println("秒杀失败了....");
    jedis.close();
    return false;
}

 
  

数据初始化


127.0.0.1:6379> flushdb
OK
设置商品111的库存为100
127.0.0.1:6379> set stock:111 100
127.0.0.1:6379> 

压测


ab -n 2000 -c 200 -k  http://192.168.43.45:8080/second_kill?prodId=111

过剩问题

redis--14--Redis的事务,锁,秒杀案例_第14张图片

6.5、秒杀–使用LUA脚本解决 过剩问题

6.5.1、LUA脚本在Redis中的优势

  1. 将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。
  2. LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。

原理

通过lua脚本解决超卖问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

任务队列的方式执行

redis--14--Redis的事务,锁,秒杀案例_第15张图片

lua 脚本代码

local userId=KEYS[1]; 
local prodId=KEYS[2];
local stock_key='stock:'..prodId;
local success_user_key='success:'..prodId; 
local userExists=redis.call('sismember',success_user_key,userId);
if tonumber(userExists)==1 then 
  return 2;
end
local num= redis.call('get' ,stock_key);
if tonumber(num)<=0 then 
  return 0; 
else 
  redis.call('decr',stock_key);
  redis.call('sadd',success_user_key,userId);
end
return 1 


代码

redis--14--Redis的事务,锁,秒杀案例_第16张图片


 

/**
 * 秒杀案例
 *
 * @author 周飞
 * @class: RedisTestController
 * @date 2022/2/3 14:11
 * @Verson 1.0 -2022/2/3 14:11
 * @see
 */

public class DoSecKill_v3 {



    public static void main(String[] args) throws IOException {





        doSecKill("201","111");
    }

    static String secKillScript =
        "local userId=KEYS[1]; \n" +
        "local prodId=KEYS[2];\n" +
        "local stock_key=\"stock:\"..prodId;\n" +
        "local success_user_key=\"success:\"..prodId; \n" +
        "local userExists=redis.call(\"sismember\",success_user_key,userId);\n" +
        "if tonumber(userExists)==1 then \n" +
        "   return 2;\n" +
        "end\n" +

        "local num= redis.call(\"get\" ,stock_key);\n" +
        "if tonumber(num)<=0 then \n" +
        "  return 0; \n" +
        "else \n" +

        "  redis.call(\"decr\",stock_key);\n" +
        "  redis.call(\"sadd\",success_user_key,userId);\n" +
        "end\n" +
        "return 1 " ;


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


        Jedis jedis=getJedis_v1();

        //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;
    }
    /**
     * 构建Jedis
     *
     * @author 周飞
     * @since 2022/2/3 15:18
     */
    public static Jedis getJedis_v1() {
        // 2 连接redis
        return new Jedis("192.168.187.138", 6379);
    }
    
}

数据初始化


127.0.0.1:6379> flushdb
OK
设置商品111的库存为100
127.0.0.1:6379> set stock:111 100
127.0.0.1:6379> 

压测


ab -n 2000 -c 200 -k  http://192.168.43.45:8080/second_kill?prodId=111

问题解决,库存为0

redis--14--Redis的事务,锁,秒杀案例_第17张图片

原理解释

lua 脚本代码 按照任务队列依次执行,本质就是redis单线程执行任务。

你可能感兴趣的:(redis,redis,缓存,数据库)