您的位置:首页 > 数据库 > Redis

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的实现
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;

resultcount(*)
0100
19
291
可以看到:

最终成功100件,和商品总数一致。所有商品被抢购完了,且没有发生“超发”
抢购中失败9次,即抢购提交中,数据已经被其它线程更改,因此失败
其它91次失败,是商品已经售罄
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: