redis——事务、锁机制、秒杀案例

事务

定义

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

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

事务相关命令——Multi、Exec、discard

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

组队过程中,可以使用discard来放弃组队

事物的错误处理

  • 组队阶段报错,整个队列的命令都无法执行
  • 执行阶段某个命令报错,只有报错的命令不执行,其他命令依旧执行

锁机制

举例:一个账户有A,B,C三个人同时使用,账户上一共有10000,A使用8000,B使用5000,C使用1000,如果没有锁的限制,那么账户上可能会出现-4000的情况

悲观锁

顾名思义,非常悲观,他认为所有的操作可能都会被别人打断,所以在所有的操作时都先加上一把锁,再开始执行命令,这时候如果别人想要访问这个数据时,是不允许对数据执行操作的,直到命令队列执行完毕才释放锁。悲观锁虽然可以解决例子中出现的问题,但是效率非常低。

乐观锁

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

解决问题

模拟数据

127.0.0.1:6379> set balance 100
OK

打开两个客户端,分别输入以下操作

# 第一个客户端
127.0.0.1:6379> watch balance # watch key 表示监视这个key
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby balance 10
QUEUED
# 第二个客户端
127.0.0.1:6379> watch balance 
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incrby balance 20
QUEUED

分别执行

# 先执行第一个客户端,操作成功
127.0.0.1:6379(TX)> exec
1) (integer) 110
# 再执行第二个客户端,返回空,因为监视的key版本被修改,所以无法执行
127.0.0.1:6379(TX)> exec
(nil)

redis事务三特性

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

秒杀案例

基本实现

//秒杀过程
public static boolean doSecKill(String uid,String prodid) throws IOException {
   //1、判断uid、prodid不为空
   if ("".equals(uid) || "".equals(prodid)){
      return false;
   }
   //2、连接jedis
   Jedis jedis = new Jedis("192.168.200.130",6379);
   //3、拼接key:库存key、秒杀到的用户key
   String kcKey = "sk:" + prodid + ":gt";
   String userKey = "sk:" + prodid + ":user";
   //4、获取库存:库存为null表示秒杀还没开始
   String kc = jedis.get(kcKey);
   if (kc == null){
      System.out.println("秒杀还没有开始,请等待。。。");
      jedis.close();
      return false;
   }
   //5、判断用户是否以及秒杀成功,一个用户只允许购买一次
   Set<String> users = jedis.smembers(userKey);
   for (String user:users) {
      if (uid.equals(user)){
         System.out.println("您已秒杀成功,请把机会留给别人");
         jedis.close();
         return false;
      }
   }
   //6、判断库存数量,小于等于0,秒杀结束
   if (Integer.parseInt(kc) <= 0){
      System.out.println("手慢啦!秒杀结束");
      jedis.close();
      return false;
   }
   //7、秒杀过程
   //库存减一
   jedis.decr(kcKey);
   //加入名单
   jedis.sadd(userKey,uid);
   System.out.println("恭喜你,秒杀成功");

   jedis.close();
   return true;
}

使用ab测试

centos7需要自己安装ab工具

yum install httpd-tools

创建postfile文件,模拟表单提交参数,以&结尾,放在当前目录

vim postfile

测试

# -n 表示访问总数 -c 表示并发次数 -p 获取postfile文件 -T表单的enctype熟悉 访问地址
ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://192.168.0.10:8080/doseckill

我们设置库存为10个,测试之后发现库存为-21,出现了超卖的问题

127.0.0.1:6379> get sk:0101:gt
"-21"

解决超时、超卖问题

解决超时:使用redis连接池

解决超卖:使用乐观锁

    //秒杀过程
   public static boolean doSecKill(String uid,String prodid) throws IOException {
      //1、判断uid、prodid不为空
      if ("".equals(uid) || "".equals(prodid)){
         return false;
      }
      //2、连接jedis
//    Jedis jedis = new Jedis("192.168.200.130",6379);
      //使用redis连接池解决链接超时问题
      JedisPool jedisPool = JedisPoolUtil.getJedisPoolInstance();
      Jedis jedis = jedisPool.getResource();

      //3、拼接key:库存key、秒杀到的用户key
      String kcKey = "sk:" + prodid + ":gt";
      String userKey = "sk:" + prodid + ":user";
      //4、获取库存:库存为null表示秒杀还没开始
      String kc = jedis.get(kcKey);
      if (kc == null){
         System.out.println("秒杀还没有开始,请等待。。。");
         jedis.close();
         return false;
      }
      //5、判断用户是否以及秒杀成功,一个用户只允许购买一次
      Set<String> users = jedis.smembers(userKey);
      for (String user:users) {
         if (uid.equals(user)){
            System.out.println("您已秒杀成功,请把机会留给别人");
            jedis.close();
            return false;
         }
      }
      //6、判断库存数量,小于等于0,秒杀结束
      if (Integer.parseInt(kc) <= 0){
         System.out.println("手慢啦!秒杀结束");
         jedis.close();
         return false;
      }
      //7、秒杀过程:使用乐观锁解决超卖问题
      jedis.watch(kcKey);
      Transaction multi = jedis.multi();
//    //库存减一
//    jedis.decr(kcKey);
      multi.decr(kcKey);
//    //加入名单
//    jedis.sadd(userKey,uid);
      multi.sadd(userKey,uid);
      List<Object> list = multi.exec();

      //list返回值为空则代表执行失败
      if (list == null || list.size() == 0){
         System.out.println("秒杀失败");
         jedis.close();
         return false;
      }

      System.out.println("恭喜你,秒杀成功");
      jedis.close();
      return true;
   }

库存遗留问题

给大家看一下为什么会出现库存遗留问题

redis——事务、锁机制、秒杀案例_第1张图片

使用LUA脚本可以解决这一问题,不要求掌握,只需要能看懂即可

local userid=KEYS[1]; 
local prodid=KEYS[2];
local qtkey="sk:"..prodid..":gt";
local usersKey="sk:"..prodid..":user'; 
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;

对Java代码进行如下修改

public class SecKill_redisByScript {
	
	private static final  org.slf4j.Logger logger =LoggerFactory.getLogger(SecKill_redisByScript.class) ;
	
	static String secKillScript ="local userid=KEYS[1];\r\n" +
			"local prodid=KEYS[2];\r\n" +
			"local qtkey='sk:'..prodid..\":gt\";\r\n" +
			"local usersKey='sk:'..prodid..\":user\";\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" ;

	//秒杀过程
	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;
	}
}

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