笔记 笔记
首页
  • 开发工具
  • Java Web
  • Java 进阶
  • 容器化技术
  • Java 专栏

    • Java 核心技术面试精讲
    • Java 业务开发常见错误 100 例
  • 数据库专栏

    • MySQL 实战 45 讲
    • Redis 核心技术与实战
  • 安全专栏

    • OAuth 2.0 实战课
  • 计算机系统
  • 程序设计语言
  • 数据结构
  • 知识产权
  • 数据库
  • 面向对象
  • UML
  • 设计模式
  • 操作系统
  • 结构化开发
  • 软件工程
  • 计算机网络
  • 上午题错题
在线工具 (opens new window)

EasT-Duan

Java 开发
首页
  • 开发工具
  • Java Web
  • Java 进阶
  • 容器化技术
  • Java 专栏

    • Java 核心技术面试精讲
    • Java 业务开发常见错误 100 例
  • 数据库专栏

    • MySQL 实战 45 讲
    • Redis 核心技术与实战
  • 安全专栏

    • OAuth 2.0 实战课
  • 计算机系统
  • 程序设计语言
  • 数据结构
  • 知识产权
  • 数据库
  • 面向对象
  • UML
  • 设计模式
  • 操作系统
  • 结构化开发
  • 软件工程
  • 计算机网络
  • 上午题错题
在线工具 (opens new window)

购买兑换码请添加

添加时候请写好备注,否则无法通过。

  • 设计模式

  • JVM 详解

  • Linux

  • Redis

    • Redis 入门

    • Redis 进阶

      • Redis 事务—锁机制
      • Redis 事务—秒杀案例
        • 解决计数器和人员记录的事务操作
        • 代码编写
        • 秒杀并发模拟
          • 使用工具ab模拟测试
          • 安装
          • 通过ab测试
          • 使用Jmeter模拟测试
        • 问题
          • 超时
          • 超卖
          • 库存遗留
      • Redis 持久化—RDB
      • Redis 持久化—AOF
      • Redis 主从复制
      • Redis 集群
      • Redis 应用问题解决
      • Redis6 新功能
  • 分布式锁

  • Shiro

  • Gradle

  • Java 进阶
  • Redis
  • Redis 进阶
EasT-Duan
2024-08-10
目录

Redis 事务—秒杀案例

欢迎来到我的 ChatGPT 中转站,极具性价比,为付费不方便的朋友提供便利,有需求的可以添加左侧 QQ 二维码,另外,邀请新用户能获取余额哦!最后说一句,那啥:请自觉遵守《生成式人工智能服务管理暂行办法》。

#

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

# 代码编写

创建一个 Web 工程

<!--web.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
		  http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	version="2.5">

	<servlet>
		<display-name>doseckill</display-name>
		<servlet-name>doseckill</servlet-name>
		<servlet-class>com.seckill.SecKillServlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>doseckill</servlet-name>
		<url-pattern>/doseckill</url-pattern>
	</servlet-mapping>
	<filter>
		<filter-name>EncodingFilter</filter-name>
		<filter-class>com.seck.EncodingFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>EncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
</web-app>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!--index.jsp-->
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>iPhone 13 Pro !!!  1元秒杀!!!
</h1>

<form id="msform" action="${pageContext.request.contextPath}/doseckill" enctype="application/x-www-form-urlencoded">
	<input type="hidden" id="prodid" name="prodid" value="0101">
	<input type="button"  id="miaosha_btn" name="seckill_btn" value="秒杀点我"/>
</form>

</body>
<script  type="text/javascript" src="${pageContext.request.contextPath}/script/jquery/jquery-3.1.0.js"></script>
<script  type="text/javascript">
$(function(){
	$("#miaosha_btn").click(function(){	 
		var url=$("#msform").attr("action");
	     $.post(url,$("#msform").serialize(),function(data){
     		if(data=="false"){
    			alert("抢光了" );
    			$("#miaosha_btn").attr("disabled",true);
    		}
		} );    
	})
})
</script>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * 秒杀案例
 */
public class SecKillServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    public SecKillServlet() {
        super();
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {

        String userid = new Random().nextInt(50000) + "";
        String prodid = request.getParameter("prodid");
		//秒杀业务处理
        boolean isSuccess=Seckill.doSecKill(userid,prodid);
        response.setContentType("text/html; charset=UTF-8");
        response.getWriter().print(isSuccess);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * 秒杀业务处理
 */
public class Seckill {

	//秒杀过程
	public static boolean doSecKill(String uid,String prodid) throws IOException {
		//1 uid和prodid非空判断
		if(uid == null || prodid == null) {
			return false;
		}

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

		//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 秒杀过程
		//7.1 库存-1
		jedis.decr(kcKey);
		//7.2 把秒杀成功用户添加清单里面
		jedis.sadd(userKey,uid);

		System.out.println("秒杀成功了..");
		jedis.close();
		return true;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

# 秒杀并发模拟

# 使用工具 ab 模拟测试

# 安装

yum install httpd-tools
1

# 通过 ab 测试

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

prodid=0101&
1

使用命令

ab -n 2000 -c 200 -k -p ~/postfile -T application/x-www-form-urlencoded http://ip:port/Seckill/doseckill
1

# 使用 Jmeter 模拟测试

# 问题

# 超时

使用连接池可以有效解决这个问题,节省每次连接 redis 服务带来的消耗,把连接好的实例反复利用。通过参数管理连接的行为。

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.25.100", 6379, 60000);
				}
			}
		}
		return jedisPool;
	}

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

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * 秒杀业务处理
 */
public class Seckill {
	.............
		//2 连接redis
		//通过连接池得到jedis对象
		JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
		Jedis jedis = jedisPoolInstance.getResource();
    .............
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
  • 链接池参数
    • MaxTotal:控制一个 pool 可分配多少个 jedis 实例,通过 pool.getResource () 来获取;如果赋值为 - 1,则表示不限制;如果 pool 已经分配了 MaxTotal 个 jedis 实例,则此时 pool 的状态为 exhausted;
    • MaxIdle:控制一个 pool 最多有多少个状态为 idle (空闲) 的 jedis 实例;
    • MaxWaitMillis:表示当 borrow 一个 jedis 实例时,最大的等待毫秒数,如果超过等待时间,则直接抛 JedisConnectionException;
    • TestOnBorrow:获得一个 jedis 实例的时候是否检查连接可用性(ping ());如果为 true,则得到的 jedis 实例均是可用的;

# 超卖

通过以上的测试可以看到在抢空后依然出现了抢购成功的提示。并且发现库存数量为负数。

利用乐观锁解决超卖问题。

public class SecKill {

    // 秒杀过程
    public static boolean doSecKill(String uid, String prodid) throws IOException {
        // 1 uid和prodid非空判断
        if (uid == null || prodid == null) {
            return false;
        }

        // 2 连接redis,通过连接池方式
        JedisPool poolInstance = JedisPoolUtil.getJedisPoolInstance();
        Jedis jedis = poolInstance.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 || "".equals(kc)) {
            System.out.println("秒杀还没有开始,请等待");
			JedisPoolUtil.release(poolInstance, jedis);
            return false;
        }

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

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

        // 7 秒杀过程
        // 开启事务
        Transaction transaction = jedis.multi();


        // 8 组合操作
        //库存-1
        transaction.decr(kcKey);
        //把秒杀成功用户添加清单里面
        transaction.sadd(userKey, uid);


        List<Object> exec = transaction.exec();

        if (exec == null || exec.size() == 0) {
            System.out.println("秒杀失败了....");
            JedisPoolUtil.release(poolInstance, jedis);
            return false;
        }


        System.out.println("秒杀成功了..");
		JedisPoolUtil.release(poolInstance, jedis);
        return true;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

# 库存遗留

ab -n 2000 -c 100 -p postfile -T 'application/x-www-form-urlencoded' http://IP:PORT/seckill/doseckill
1

已经秒光,可是还有库存。原因,就是乐观锁导致很多请求都失败。先点的没秒到,后点的可能秒到了。

解决方案:LUA 脚本

  • 将复杂的或者多步的 redis 操作,写为一个脚本,一次提交给 redis 执行,减少反复连接 redis 的次数。提升性能。
  • LUA 脚本是类似 redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些 redis 事务性的操作。
  • 但是注意 redis 的 lua 脚本功能,只有在 Redis 2.6 以上的版本才可以使用。
  • 利用 lua 脚本淘汰用户,解决超卖问题。
  • redis 2.6 版本以后,通过 lua 脚本解决争抢问题,实际上是 redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

#秒杀脚本
local userid=KEYS[1];
local prodid=KEYS[2];			
local qtkey='sk:'..prodid..":qt";
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SecKill {

    private static final Logger logger = LoggerFactory.getLogger(SecKill.class);

    /**
    *	执行脚本
    */
    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..\":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;
        System.out.println(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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#Redis
上次更新: 2025/04/12, 05:37:39
Redis 事务—锁机制
Redis 持久化—RDB

← Redis 事务—锁机制 Redis 持久化—RDB→

最近更新
01
Reactor 核心
02-24
02
前置条件
10-30
03
计算机网络
09-13
更多文章>
Theme by Vdoing | Copyright © 2019-2025 powered by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式