Redis学习笔记3 Java + Redis模拟秒杀场景

来源:互联网 发布:淘宝拆车变速箱 编辑:程序博客网 时间:2024/05/18 02:18

秒杀场景中,客户端对服务器的访问可以抽象为两个:访问静态页面(列出静态商品页面),访问后台接口(抢购)

  • 静态页面可以使用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),如果超时就抛异常, 小于零:阻塞不确定的时间,// 默认-1config.setMaxWaitMillis(-1);// 是否启用后进先出,默认trueconfig.setLifo(true);// 最小空闲连接数, 默认0config.setMinIdle(0);// 每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3config.setNumTestsPerEvictionRun(3);// 对象空闲多久后逐出, 当空闲时间>该值 且 空闲连接>最大空闲数// 时直接逐出,不再根据MinEvictableIdleTimeMillis判断 (默认逐出策略)config.setSoftMinEvictableIdleTimeMillis(1800000);// 在获取连接的时候检查有效性, 默认falseconfig.setTestOnBorrow(false);// 在空闲时检查有效性, 默认falseconfig.setTestWhileIdle(false);// 逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1config.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);}@Overridepublic 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;
resultcount(*)010019291可以看到:
  • 最终成功100件,和商品总数一致。所有商品被抢购完了,且没有发生“超发”
  • 抢购中失败9次,即抢购提交中,数据已经被其它线程更改,因此失败
  • 其它91次失败,是商品已经售罄


原创粉丝点击