Redis学习笔记3 Java + Redis模拟秒杀场景
2017-05-20 14:37
246 查看
秒杀场景中,客户端对服务器的访问可以抽象为两个:访问静态页面(列出静态商品页面),访问后台接口(抢购)
静态页面可以使用DNS实现,压力不大;
后台接口是重点要解决的问题。
一定要快
不要直接访问传统数据库,太慢。建议使用内存数据库技术,本例使用Redis进行示例
防止同一账号短时间内的多次请求
防止超发(即本来只有100件商品,却最终成交了101件)
悲观锁:即实际对某个商品的购买api,同时只允许一个用户访问,“查询该商品数量”、“商品数量减1”是在同一个事务中,保证数据的完整性。缺点是性能,通常无法满足抢购的场景。
FIFO: 客户端的抢购指令,只是插入一个交易表,由另外一个统一的线程来处理交易表,标记交易的成功或失败(如商品已售完)。缺点是客户端无法立即得到反馈,需要等待统一的线程处理完自己的交易后才知道抢购是否成功。不知道是否有公司采用此种方案实现抢购场景,个人感觉还是可行的。
乐观锁:即每个抢购指令前:step 1. 首先做个特殊标记; step 2. 然后正常执行指令; step 3. 在指令提交时,根据标记判断step 1至step 3之间商品数据是否有变化,如果有,则失败;否则,则抢购成功。
以下采用Java + Redis模拟乐观锁的实现。
Redis服务器需要事先搭建好,作者将具体ip mask掉
本例采用异步方式记录交易log表,之所以要插入此log表,是为了方便统计最终商品交易的成功数、失败数。不是必须的。可以注释掉这些代码。(当然实际业务中应该会记录类似的表)
1. MyJedisPool.java // Redis客户端pool的实现
2. FlashSaleTest.java // 抢购模拟
3. Trade.java 交易记录数据模型
4. LogManager.java 异步记录交易Log的服务
5. 本文用到的库
6. 运行结果
select result, count(*) from t_buy group by result;
可以看到:
最终成功100件,和商品总数一致。所有商品被抢购完了,且没有发生“超发”
抢购中失败9次,即抢购提交中,数据已经被其它线程更改,因此失败
其它91次失败,是商品已经售罄
静态页面可以使用DNS实现,压力不大;
后台接口是重点要解决的问题。
一定要快
不要直接访问传统数据库,太慢。建议使用内存数据库技术,本例使用Redis进行示例
防止同一账号短时间内的多次请求
防止超发(即本来只有100件商品,却最终成交了101件)
悲观锁:即实际对某个商品的购买api,同时只允许一个用户访问,“查询该商品数量”、“商品数量减1”是在同一个事务中,保证数据的完整性。缺点是性能,通常无法满足抢购的场景。
FIFO: 客户端的抢购指令,只是插入一个交易表,由另外一个统一的线程来处理交易表,标记交易的成功或失败(如商品已售完)。缺点是客户端无法立即得到反馈,需要等待统一的线程处理完自己的交易后才知道抢购是否成功。不知道是否有公司采用此种方案实现抢购场景,个人感觉还是可行的。
乐观锁:即每个抢购指令前:step 1. 首先做个特殊标记; step 2. 然后正常执行指令; step 3. 在指令提交时,根据标记判断step 1至step 3之间商品数据是否有变化,如果有,则失败;否则,则抢购成功。
以下采用Java + Redis模拟乐观锁的实现。
Redis服务器需要事先搭建好,作者将具体ip mask掉
本例采用异步方式记录交易log表,之所以要插入此log表,是为了方便统计最终商品交易的成功数、失败数。不是必须的。可以注释掉这些代码。(当然实际业务中应该会记录类似的表)
1. MyJedisPool.java // Redis客户端pool的实现
package com.cloudboy.redis; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class MyJedisPool { private static JedisPool pool; static { JedisPoolConfig config = new JedisPoolConfig(); // 设置的逐出策略类名, 默认DefaultEvictionPolicy(当连接超过最大空闲时间,或连接数超过最大空闲连接数) config.setEvictionPolicyClassName("org.apache.commons.pool2.impl.DefaultEvictionPolicy"); // 最大连接数 config.setMaxTotal(8); // 最大空闲连接数 config.setMaxIdle(8); // 获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间, // 默认-1 config.setMaxWaitMillis(-1); // 是否启用后进先出,默认true config.setLifo(true); // 最小空闲连接数, 默认0 config.setMinIdle(0); // 每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3 config.setNumTestsPerEvictionRun(3); // 对象空闲多久后逐出, 当空闲时间>该值 且 空闲连接>最大空闲数 // 时直接逐出,不再根据MinEvictableIdleTimeMillis判断 (默认逐出策略) config.setSoftMinEvictableIdleTimeMillis(1800000); // 在获取连接的时候检查有效性, 默认false config.setTestOnBorrow(false); // 在空闲时检查有效性, 默认false config.setTestWhileIdle(false); // 逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1 config.setTimeBetweenEvictionRunsMillis(-1); pool = new JedisPool(config, "????????"); } public static Jedis getJedis() { return pool.getResource(); } /** 归还jedis对象 */ public static void recycleJedisOjbect(Jedis jedis) { jedis.close(); } }
2. FlashSaleTest.java // 抢购模拟
package com.cloudboy.redis.flashSale; import java.util.List; import com.cloudboy.redis.MyJedisPool; import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; public class FlashSaleTest { private static String KEY = "COUNT"; private int userCount; private int interval; /** * @param totalItemCount 商品总数 * @param userCount 模拟用户数 * @param interval 用户采购间隔(毫秒) */ public FlashSaleTest(int totalItemCount, int userCount, int interval) { this.userCount = userCount; this.interval = interval; Jedis jedis = MyJedisPool.getJedis(); jedis.set(KEY, "" + totalItemCount); MyJedisPool.recycleJedisOjbect(jedis); } public void start() { for(int i=0; i<userCount; i++) { Thread tt = new UserThread("Thread" + i); tt.start(); try { Thread.sleep(interval); } catch (InterruptedException e) { e.printStackTrace(); } } } private static int buy() { Jedis jedis = MyJedisPool.getJedis(); jedis.watch(KEY); int value = Integer.valueOf(jedis.get(KEY)).intValue(); int result; if(value > 0) { Transaction tx = jedis.multi(); tx.decr(KEY); List<Object> res = tx.exec(); if(res.size() == 0) { result = 1; // 失败 } else { result = 0; // 成功 } } else { result = 2; // 已售完 } MyJedisPool.recycleJedisOjbect(jedis); return result; } static class UserThread extends Thread { private String user = null; public UserThread(String user) { this.user = user; } public void run() { int result = buy(); Trade trade = new Trade(); trade.setUser(this.user); trade.setResult(result); LogManager.addLog(trade); System.out.println("user(" + user + ") result(" + result + ")"); } } public static void main(String[] args) { FlashSaleTest test = new FlashSaleTest(100, 200, 100); test.start(); } }
3. Trade.java 交易记录数据模型
package com.cloudboy.redis.flashSale; public class Trade { private String user; private int result; public int getResult() { return result; } public void setResult(int result) { this.result = result; } public String getUser() { return user; } public void setUser(String user) { this.user = user; } }
4. LogManager.java 异步记录交易Log的服务
package com.cloudboy.redis.flashSale; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.concurrent.LinkedBlockingQueue; public class LogManager implements Runnable { private static LinkedBlockingQueue<Trade> list = new LinkedBlockingQueue<Trade>(); private static String url="jdbc:mysql://......../feitu?useUnicode=true&characterEncoding=UTF-8"; private static Connection conn; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); conn = DriverManager.getConnection(url, "user","password"); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } new Thread(new LogManager()).start(); } public static void addLog(Trade log) { list.add(log); } @Override public void run() { while(true) { Trade trade = null; try { trade = list.take(); log(trade); } catch (InterruptedException e) { e.printStackTrace(); } } } private void log(Trade trade) { String sql = "insert into t_buy (user, result) values(?, ?)"; try { PreparedStatement pst = conn.prepareStatement(sql); pst.setString(1, trade.getUser()); pst.setInt(2, trade.getResult()); pst.execute(); } catch(SQLException e) { e.printStackTrace(); } } }
5. 本文用到的库
compile group:'redis.clients', name:'jedis', version:'2.9.0' compile group:'com.thoughtworks.xstream', name:'xstream', version:'1.4.7' compile group:'xmlpull', name:'xmlpull', version:'1.1.3.4d_b4_min' compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.5' compile group: 'commons-pool', name: 'commons-pool', version: '1.6'
6. 运行结果
select result, count(*) from t_buy group by result;
result | count(*) |
0 | 100 |
1 | 9 |
2 | 91 |
最终成功100件,和商品总数一致。所有商品被抢购完了,且没有发生“超发”
抢购中失败9次,即抢购提交中,数据已经被其它线程更改,因此失败
其它91次失败,是商品已经售罄
相关文章推荐
- redis学习笔记---java操作redis,使用expire模拟指定时间段内限制ip访问的次数;
- SB集成Redis学习笔记之实际应用场景-java干货
- java 银行模拟叫号系统 黑马程序员学习笔记(9)
- Java程序员的JavaScript学习笔记(6——面向对象模拟)
- Redis学习笔记3-本机java程序调用虚拟机redis
- JAVA之 Redis 学习笔记(二) Redis的连接IP、端口号、连接密码的修改以及与JAVA简单交互
- Java-马士兵设计模式学习笔记-工厂模式-用Jdom模拟Spring
- java连接redis数据库-redis学习笔记(二)
- Redis学习笔记-Java连接Redis
- Android(java)学习笔记233: 远程服务的应用场景(移动支付案例)
- JAVA高并发秒杀API项目的学习笔记
- JAVA学习笔记(2)_____线程同步锁(synchronized)模拟火车售票窗口
- Java-马士兵设计模式学习笔记-策略模式-模拟 Comparator接口
- JAVA学习笔记30——模拟实现LinkedList
- Java Web 学习笔记之三: HttpURLConnection 模拟表单上传文件
- java 从零开始,学习笔记之基础入门<网络编程_带QQ模拟功能>(二十)
- [原]Java程序员的JavaScript学习笔记(6——面向对象模拟)
- JAVA学习笔记38——模拟实现Iterator+HashMap的“分拣”原理+“分拣”的应用
- Java-马士兵设计模式学习笔记-策略模式-模拟Comparable接口
- 学习java多线程的笔记3-使用BlockingQueue阻塞队列来模拟两个线程之间的通信