1、节省每次连接redis 服务带来的消耗,把连接好的实例反复利用。
2、链接池参数
通过连接池,可以指定连接超时时间, 这个连接超时时间,也需要合理设置,要考虑到用户的实际体验
src\com\seckill\utils\JedisPoolUtil.java
public class JedisPoolUtil {
//解读volatile作用
//1. 线程的可见性: 当一个线程去修改一个共享变量时, 另外一个线程可以读取这个修改的值
//2. 顺序的一致性: 禁止指令重排
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil() {
}
//保证每次调用返回的 jedisPool是单例-这里使用了双重校验
public static JedisPool getJedisPoolInstance() {
if (null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if (null == jedisPool) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//对连接池进行配置
jedisPoolConfig.setMaxTotal(200);
jedisPoolConfig.setMaxIdle(32);
jedisPoolConfig.setMaxWaitMillis(60 * 1000);
jedisPoolConfig.setBlockWhenExhausted(true);
jedisPoolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(jedisPoolConfig, "192.168.198.135", 6379, 60000);
}
}
}
return jedisPool;
}
//释放连接资源
public static void release(Jedis jedis) {
if(null != jedis) {
jedis.close();//如果这个jedis是从连接池获取的,这里jedis.close(),就是将jedis对象/连接,释放到连接池
}
}
}
ticket\src\com\seckill\redis\SecKillRedis.java
//- 连接到Redis, 得到jedis对象
//Jedis jedis = new Jedis("192.168.198.135", 6379);
//- 通过连接池获取到jedis对象/连接
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
System.out.println("---使用的连接池技术----");
src\com\seckill\redis\SecKillRedis.java
public class SecKillRedis {
/**
* 编写一个测试方法-看看是否能够连通到指定的Redis
*/
@Test
public void testRedis() {
Jedis jedis = new Jedis("192.168.198.135", 6379);
//jedis.auth("foobared");//如果需要认证, 就使用auth
System.out.println(jedis.ping());
jedis.close();
}
/**
* 秒杀过程/方法
*/
/**
* @param uid 用户id - 在后台生成
* @param ticketNo 票的编号, 比如北京-成都的ticketNo 就是bj_cd
* @return
*/
public static boolean doSecKill(String uid, String ticketNo) {
//- uid 和 ticketNo进行非空校验
if (uid == null || ticketNo == null) {
return false;
}
//- 连接到Redis, 得到jedis对象
//Jedis jedis = new Jedis("192.168.198.135", 6379);
//- 通过连接池获取到jedis对象/连接
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
System.out.println("---使用的连接池技术----");
//- 拼接票的库存key
String stockKey = "sk:" + ticketNo + ":ticket";
//- 拼接秒杀用户要存放到的set集合对应的key,这个set集合可以存放多个userId
String userKey = "sk:" + ticketNo + ":user";
//监控库存
jedis.watch(stockKey);
//- 获取到对应的票的库存, 判断是否为null
String stock = jedis.get(stockKey);
if (stock == null) {
System.out.println("秒杀还没有开始, 请等待..");
jedis.close(); //如果jedis是从连接池获取的,则这里的close就是将jedis对象/连接释放到连接池
return false;
}
//- 判断用户是否重复秒杀/复购
if (jedis.sismember(userKey, uid)) {
System.out.println(uid + " 不能重复秒杀...");
jedis.close();
return false;
}
//- 判断火车票,是否还有剩余
if (Integer.parseInt(stock) <= 0) {
System.out.println("票已经卖完了, 秒杀结束..");
jedis.close();
return false;
}
- 可以购买
1. 将票的库存量-1
//jedis.decr(stockKey);
2. 将该用户加入到抢购成功对应的set集合中
//jedis.sadd(userKey, uid);
//使用事务,完成秒杀
Transaction multi = jedis.multi();
//组队操作
multi.decr(stockKey);//减去票的库存
multi.sadd(userKey, uid);//将该用户加入到抢购成功对应的set集合中
//执行
List<Object> results = multi.exec();
if(results == null || results.size() == 0) {
System.out.println("抢票失败...");
jedis.close();
return false;
}
System.out.println(uid + " 秒杀成功..");
jedis.close();
return true;
}
}
ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.98.1:8080/seckill/secKillServlet
ab -n 1000 -c 300 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.98.1:8080/seckill/secKillServlet
可以看到, 剩余票数为543, 并不是0
出现库存遗留问题的分析
1、Lua 是一个小巧的脚本语言,Lua 脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua 并没有提供强大的库,一个完整的Lua 解释器不过200k,所以Lua 不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。
2、很多应用程序、游戏使用LUA 作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。
3、将复杂的或者多步的Redis 操作,写为一个脚本,一次提交给redis 执行,减少反复连接redis 的次数。提升性能。
4、LUA 脚本是类似Redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些redis 事务性的操作
5、Redis 的lua 脚本功能,只有在Redis 2.6 以上的版本才可以使用
6、通过lua 脚本解决争抢问题,实际上是Redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题
1、编写lua 脚本文件
local userid=KEYS[1]; -- 获取传入的第一个参数
local ticketno=KEYS[2]; -- 获取传入的第二个参数
local stockKey='sk:'..ticketno..:ticket; -- 拼接stockKey
local usersKey='sk:'..ticketno..:user; -- 拼接usersKey
local userExists=redis.call(sismember,usersKey,userid); -- 查看在redis 的
usersKey set 中是否有该用户
if tonumber(userExists)==1 then
return 2; -- 如果该用户已经购买, 返回2
end
local num= redis.call("get" ,stockKey); -- 获取剩余票数
if tonumber(num)<=0 then
return 0; -- 如果已经没有票, 返回0
else
redis.call("decr",stockKey); -- 将剩余票数-1
redis.call("sadd",usersKey,userid); -- 将抢到票的用户加入set
end
return 1 -- 返回1 表示抢票成功
– 参考文档: https://blog.csdn.net/qq_41286942/article/details/124161359
2 、创建SecKillRedisByLua
\src\com\seckill\redis\SecKillRedisByLua.java
public class SecKillRedisByLua {
/**
* 说明
* 1. 这个脚本字符串是在lua脚本上修改的, 但是要注意不完全是字符串处理
* 2. 比如 : 这里我就使用了 \" , 还有换行使用了 \r\n
* 3. 这些都是细节,如果你直接把lua脚本粘贴过来,不好使,一定要注意细节
* 4. 如果写的不成功,就在这个代码上修改即可
*/
static String secKillScript = "local userid=KEYS[1];\r\n" +
"local ticketno=KEYS[2];\r\n" +
"local stockKey='sk:'..ticketno..\":ticket\";\r\n" +
"local usersKey='sk:'..ticketno..\":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\" ,stockKey);\r\n" +
"if tonumber(num)<=0 then \r\n" +
" return 0;\r\n" +
"else \r\n" +
" redis.call(\"decr\",stockKey);\r\n" +
" redis.call(\"sadd\",usersKey,userid);\r\n" +
"end\r\n" +
"return 1";
//使用lua脚本完成秒杀的核心方法
public static boolean doSecKill(String uid,String ticketNo) {
//先从redis连接池,获取连接
JedisPool jedisPoolInstance = JedisPoolUtil.getJedisPoolInstance();
Jedis jedis = jedisPoolInstance.getResource();
//就是将lua脚本进行加载
String sha1 = jedis.scriptLoad(secKillScript);
//evalsha是根据指定的 sha1校验码, 执行缓存在服务器的脚本
Object result = jedis.evalsha(sha1, 2, uid, ticketNo);
String resString = String.valueOf(result);
//根据lua脚本执行返回的结果,做相应的处理
if("0".equals(resString)) {
System.out.println("票已经卖光了..");
jedis.close();
return false;
}
if("2".equals(resString)) {
System.out.println("不能重复购买..");
jedis.close();
return false;
}
if("1".equals(resString)) {
System.out.println("抢购成功");
jedis.close();
return true;
} else {
System.out.println("购票失败..");
jedis.close();
return false;
}
}
}
3 、修改SecKillServlet
src\com\seckill\web\SecKillServlet.java
public class SecKillServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 请求时,模拟生成一个userId
String userId = new Random().nextInt(10000) + "";
//2. 获取用户要购买的票的编号
String ticketNo = request.getParameter("ticketNo");
//3. 调用秒杀的方法
//boolean isOk = SecKillRedis.doSecKill(userId, ticketNo);
//4. 调用lua脚本完成秒杀方法
boolean isOk = SecKillRedisByLua.doSecKill(userId, ticketNo);
//4. 将结果返回给前端-这个地方可以根据业务需要调整
response.getWriter().print(isOk);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doPost(request, response);
}
}