作者:大坑啊同志 | 来源:互联网 | 2023-09-16 15:18
需求双十二要搞一个一分钱门票抢购的活动。分析性能分析,抢购时会发生高并发,如果仅仅依靠Mysql数据库,有可能因为大量的请求频繁访问数据库造成服务器雪崩,所以考虑通过Redis减库
需求
双十二要搞一个一分钱门票抢购的活动。
分析
性能分析,抢购时会发生高并发,如果仅仅依靠Mysql数据库,有可能因为大量的请求频繁访问数据库造成服务器雪崩,所以考虑通过Redis减库存,最终的数据落地到DB中。
在高并发的情况下,还要考虑到超卖的问题,因而打算使用Lua脚本完成原子减的操作。
在这里,我们只针对减库存的操作进行分析。
实现
不使用原子操作,出现超卖的情况。第一步:先从redis中查出库存进行判断,第二步:如果库存>0,则进行减库存的操作。
代码实现:
1 // 第一步:从redis中查出库存
2 Integer stock = (Integer) RedisUtils.get("stock");
3
4 // 第二步:如果库存>0,则进行减库存的操作
5 if (stock > 0) {
6 long spareStock = RedisUtils.decr("stock", 1);
7 System.out.println(getName() + "抢到了第" + spareStock + "件");
8 } else {
9 System.out.println("库存不足");
10 }
用多线程模拟并发请求:库存为500,创建505个线程去抢购。
1 for(int i =1;i<=505;i++){
2 MyThread2 thread =new MyThread2("线程"+i);
3 thread.start();
4 }
执行结果:出现超卖问题,原因是:查询库存及减库存不是原子性操作。
使用原子性操作:直接减库存。
1 public void run() {
2 long stock = RedisUtils.stock("stock");
3 if (stock > 0) {
4 System.out.println(getName() + "抢到了第" + stock + "件");
5 } else {
6 System.out.println("库存不足");
7 }
8
9 }
Lua脚本实现减库存操作:
/**
* 库存不足
*/
public static final int LOW_STOCK = 0;
/**
* 不限库存
*/
public static final long UNINITIALIZED_STOCK = -1L;
/**
* 执行扣库存的脚本
*/
public static final String STOCK_LUA;
static {
// 初始化减库存lua脚本
StringBuilder sb = new StringBuilder();
sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
sb.append(" local stock = tonumber(redis.call('get', KEYS[1]));");
sb.append(" if (stock == -1) then");
sb.append(" return 1;");
sb.append(" end;");
sb.append(" if (stock > 0) then");
sb.append(" redis.call('incrby', KEYS[1], -1);");
sb.append(" return stock;");
sb.append(" end;");
sb.append(" return 0;");
sb.append("end;");
sb.append("return -1;");
STOCK_LUA = sb.toString();
}
/**
* 扣库存
*
* @param key 库存key
* @return 扣减之前剩余的库存【0:库存不足; -1:库存未初始化; 大于0:扣减库存之前的剩余库存】
*/
public static Long stock(String key) {
// 脚本里的KEYS参数
List keys = new ArrayList<>();
keys.add(key);
// 脚本里的ARGV参数
List args = new ArrayList<>();
Long result = (Long)redisTemplate.execute(new RedisCallback() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args);
}
// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long)((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);
}
return UNINITIALIZED_STOCK;
}
});
return result;
}
执行结果:505个线程去抢500个商品,有五个线程会抢不到,测试结果与预期一致,解决了超卖的问题。
参考:https://blog.csdn.net/xiaolyuh123/article/details/79208959