以下引用大神的:

测试项目: https://github.com/14251104246/redis-demo.git

准备

重现秒杀时出现的超卖问题

  • 核心测试代码如下:
/**
* 用于测试redis秒杀
*/
@RestController
@RequestMapping("/api/spike")
@Slf4j
public class SpikeController { @Resource(name = "stringRedisTemplate")
private StringRedisTemplate stringRedisTemplate; @Autowired
private RedissonClient redissonClient;
 //记录实际卖出的商品数量
private AtomicInteger successNum = new AtomicInteger(0);
@RequestMapping(value = "/initSku", method = RequestMethod.GET)
public String initSku() {
//初始化库存数量
stringRedisTemplate.opsForValue().set("product_sku", "5");
//初始化实际卖出的商品数量0
successNum.set(0);
return "初始化库存成功";
}
 /**
* 会出现超卖情况的减少库存方式
* @return
*/
@RequestMapping(value = "/reduceSku", method = RequestMethod.GET)
public String reduceSku() {
Integer sku = Integer.parseInt(stringRedisTemplate.opsForValue().get("product_sku"));
sku = sku - 1;
if (sku < 0) {
return "库存不足";
} stringRedisTemplate.opsForValue().set("product_sku", sku.toString());
//记录实际卖出的商品数量
return "减少库存成功,共减少" + successNum.incrementAndGet();
} @RequestMapping(value = "/successNum", method = RequestMethod.GET)
public String successNum() {
return "顾客成功抢到的商品数量:" + successNum.get();
}
}
  • 测试api:
API{初始化库存数量} >> http://127.0.0.1:8090/api/spike/initSku
API{减少库存数量} >> http://127.0.0.1:8090/api/spike/reduceSku
API{查看共减少库存数量} >> http://127.0.0.1:8090/api/spike/successNum
  • 第一个api用于:初始化库存中的商品数量为5

  • 第二个api用于:减少库存1个商品(即客户购买一个商品)

  • 第三个api用于:查看用户实际购买的商品

  • 少量用户请求的情况展示:

超卖问题原因分析

  • 从上面测试结果,我们知道,高并发请求http://127.0.0.1:8090/api/spike/reduceSku,会出现超卖的情况
  • 下面我们看下超卖问题的原因
/**
* 会出现超卖情况的减少库存方式
* @return
*/
@RequestMapping(value = "/reduceSku", method = RequestMethod.GET)
public String reduceSku() {
Integer sku = Integer.parseInt(stringRedisTemplate.opsForValue().get("product_sku"));
sku = sku - 1;
if (sku < 0) {
return "库存不足";
} stringRedisTemplate.opsForValue().set("product_sku", sku.toString());
//记录实际卖出的商品数量
return "减少库存成功,共减少" + successNum.incrementAndGet();
}
  • 从代码片可以看出,问题原因是库存数量sku的读和写操作不在同一个原子操作上,导致类似不可重复读的现象。可以类比多线程的问题。

通过redis事务解决超卖问题

使用redis原生的sdk
  • 如下改造reduceSku()方法,作为一个新接口http://127.0.0.1:8090/api/spike/reduceSku3
    /**
* 加入事务的减少库存方式
* @return
*/
@RequestMapping(value = "/reduceSku3", method = RequestMethod.GET)
public String reduceSku3() {
Jedis jedis = new Jedis("127.0.0.1", 6379);
List<Object> result ;
Transaction transaction = null;
try {
jedis.watch("product_sku");
int sku = Integer.parseInt(jedis.get("product_sku"));
if (sku > 0) {
transaction = jedis.multi();
transaction.set("product_sku", String.valueOf(sku - 1));
// int exp = 1/0;
result = transaction.exec();
if (result == null || result.isEmpty()) {
System.out.println("Transaction error...");// 可能是watch-key被外部修改,或者是数据操作被驳回
// transaction.discard(); //watch-key被外部修改时,discard操作会被自动触发
return "Transaction error...";
}
} else {
return "库存不足";
}
return "减少库存成功,共减少" + successNum.incrementAndGet();
} catch (Exception e) {
log.error(e.getMessage());
transaction.discard();
return "fail";
}
}
spring的redisTemplate执行事务
  • 注意: 若要使用spring的redisTemplate执行事务,需要在开启事务后执行一个redis的查询操作(但不能使用查询到的值)。原因有两点:

    • spring对redis事务的exec()方法返回结果做了处理(把返回值的 OK结果删掉)。

      • 导致在事务中只有set等更新操作时,事务执行失败与成功返回的结果一样
    • 事务过程中查询redis的值只会在事务执行成功后才放回。而在事务执行过程中只会返回null
  • 接口http://127.0.0.1:8090/api/spike/reduceSku3是使用spring的redisTemplate执行事务的例子。代码如下
@RequestMapping(value = "/reduceSku2", method = RequestMethod.GET)
public String reduceSku2() {
stringRedisTemplate.setEnableTransactionSupport(true);
List<Object> results = stringRedisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
operations.watch("product_sku");
String product_sku = (String) operations.opsForValue().get("product_sku");
operations.multi();
operations.opsForValue().get("product_sku");//必要的空查询
Integer sku = Integer.parseInt(product_sku);
sku = sku - 1;
if (sku < 0) {
return null;
}
operations.opsForValue().set("product_sku", sku.toString());
return operations.exec();
// operations.unwatch(); //执行exec()后自动unwatch() }
}); if (results != null && results.size() > 0) {
return "减少库存成功,共减少" + successNum.incrementAndGet();
} return "库存不足";
// return result.toString();
}
spring的redisTemplate执行事务(使用zset
  • 接口http://127.0.0.1:8090/api/set/reduceSku是使用zset的方式
@RequestMapping(value = "/reduceSku", method = RequestMethod.GET)
public String reduceSku5(String pid) {
pid = pid==null? String.valueOf(1) :pid;
String finalPid = pid;
List<Object> results = redisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations redisOperations) throws DataAccessException {
String key = "product";
redisOperations.watch(key);
ZSetOperations<String, String> kvzSetOperations = redisOperations.opsForZSet();
Object score = kvzSetOperations.score(key, finalPid);
redisOperations.multi();
if (score != null && Double.valueOf(score.toString()) > 0) {
kvzSetOperations.incrementScore("product", finalPid, -1);
}
return redisOperations.exec(); }
}); if (results != null && results.size() > 0) {
return "减少库存成功,共减少" + successNum.incrementAndGet();
} return "库存不足";
}
@RequestMapping(value = "/reduceSku4", method = RequestMethod.GET)
public String reduceSku4() {
RLock rLock = redissonClient.getLock("product_sku");
try {
rLock.lock(); Integer sku = Integer.parseInt(stringRedisTemplate.opsForValue().get("product_sku"));
sku = sku - 1;
if (sku < 0) {
return "库存不足";
} stringRedisTemplate.opsForValue().set("product_sku", sku.toString()); return "减少库存成功,共减少" + successNum.incrementAndGet();
} finally {
rLock.unlock();
} }


对redis高并发测试的研究的更多相关文章

  1. Java高并发测试框架JCStress

    前言 如果要研究高并发,一般会借助高并发工具来进行测试.JCStress(Java Concurrency Stress)它是OpenJDK中的一个高并发测试工具,它可以帮助我们研究在高并发场景下JV ...

  2. Redis高并发和快速的原因

    一.Redis的高并发和快速原因 1.redis是基于内存的,内存的读写速度非常快: 2.redis是单线程的,省去了很多上下文切换线程的时间:   3.redis使用多路复用技术,可以处理并发的连接 ...

  3. 《Netty Zookeeper Redis 高并发实战》 图书简介

    <Netty Zookeeper Redis 高并发实战> 图书简介 本书为 高并发社群 -- 疯狂创客圈 倾力编著, 高度剖析底层原理,深度解读面试难题 疯狂创客圈 Java 高并发[ ...

  4. Jmeter之仿真高并发测试-集合点

    场景: 大家在使用Jmeter测试的时候应该发现了, (1)线程启动了就会直接发送测试请求:--如果要模拟在一瞬间高并发量测试的时候,需要调高线程数量,这很耗测试机器的性能,往往无法支持较大的并发数, ...

  5. Redis高并发快的3大原因详解

    1. Redis的高并发和快速的原因 1.redis是基于内存的,内存的读写速度非常快: 2.redis是单线程的,省去了很多上下文切换线程的时间: 3.redis使用多路复用技术,可以处理并发的连接 ...

  6. Redis 高并发解决方案

    针对大流量瞬间冲击,比如秒杀场景 redis前面可以加一层限流 sentinel / Hystrix redis高并发(读多写少)下缓存数据库双写误差: 1. 修改操作使用分布式锁(就是修改的时候加锁 ...

  7. Redis高并发分布式锁详解

    为什么需要分布式锁 1.为了解决Java共享内存模型带来的线程安全问题,我们可以通过加锁来保证资源访问的单一,如JVM内置锁synchronized,类级别的锁ReentrantLock. 2.但是随 ...

  8. nginx+lua+redis高并发应用建设

    ngx_lua将lua嵌nginx,让nginx运行lua脚本.高并发,非堵塞过程中的各种请求. url要求nginxserver,然后lua查询redis,返回json数据. 一.安装lua-ngi ...

  9. 2020重新出发,NOSQL,redis高并发系统的分析和设计

    高并发系统的分析和设计 任何系统都不是独立于业务进行开发的,真正的系统是为了实现业务而开发的,所以开发高并发网站抢购时,都应该先分析业务需求和实际的场景,在完善这些需求之后才能进入系统开发阶段. 没有 ...

随机推荐

  1. C++拷贝构造函数心得

    C++Primer作者提到拷贝构造函数调用的三种时机: 1. 当用一个类对象去初始化另外一个类对象(类似于 AClass aInstance = bInstance),这里不是调用赋值构造函数(也叫赋 ...

  2. Selenium学习之==>WebDriver驱动对照表

    转自www.imdsx.cn 1.Chrome 对于chrome浏览器,有时候会有闪退的情况,也许是版本冲突的问题,我们要对照着这个表来对照查看是不是webdriver和chrome版本不对. chr ...

  3. :hover 鼠标同时触发两个元素变化

    HTML代码: <li> <span>4</span> <a href="#">巨型一号丝瓜水320ML</a>< ...

  4. cocos2dx基础篇(14) 滚动视图CCScrollView

    [3.x]     (1)去掉 "CC"     (2)滚动方向         > CCScrollViewDirection 改为强枚举 ScrollView::Dire ...

  5. java创建多线程实现并行计算任务处理

    1.直接上代码一看明白: package multithreadingTest; class fblib extends Thread{ public static Integer fb(Intege ...

  6. 我在拼多多百亿补贴时买了个MAC probook16G苹果笔记本电脑用来写代码,有图有真相,靠谱吗?

    在拼多多买了拼了个MACBOOK pro,有点担心质量问题,商家承诺有电子普票,有大哥在上面买过贵重物品吗?欢迎评论留言交流

  7. 【嵌入式linux】用户登录密码验证配置

    在配置BusyBox中配置登录密码: 配置位置BusyBox Setting -->           General Configuration -->           Suppo ...

  8. stringstream istringstream ostringstream 三者的区别

    stringstream istringstream ostringstream 三者的区别 说明 ostringstream : 用于执行C风格字符串的输出操作. istringstream : 用 ...

  9. Linear Discriminant Analysis

    Suppose that we model each class density as multivariate Gaussian, in practice we do not know the pa ...

  10. 思维体操: HDU1008 Elevator

    Elevator Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total S ...