部署在一个tomcat下
- 模拟一个减库存的代码 这段代码请求少的话是没有问题的,但是在高并发的场景下就会出现问题
@RequestMapping("redisTest") public String redisTest() { String key = "count"; //获取库存数量 int count = Integer.parseInt(stringRedisTemplate.opsForValue().get(key)); //有库存减少 if (count > 0) { count = count - 1; stringRedisTemplate.opsForValue().set(key, String.valueOf(count)); System.out.println("扣除成功,剩余数量:" + count); } else { System.out.println("扣除失败"); } return "end"; }
- 当前redis count的值为500
- 使用jmemter 200个线程同时访问
- 结果,可以看到有些库存并没有减掉
- 解决方案,加个方法锁(必须获取到锁才能进入,否则阻塞)
@RequestMapping("redisTest") public synchronized String redisTest() { System.out.println(this); String key = "count"; //获取库存数量 int count = Integer.parseInt(stringRedisTemplate.opsForValue().get(key)); //有库存减少 if (count > 0) { count = count - 1; stringRedisTemplate.opsForValue().set(key, String.valueOf(count)); System.out.println("扣除成功,剩余数量:" + count); } else { System.out.println("扣除失败"); } return "end"; }
集群
- 以上是一个tomcat,如果有两个tomcat使用nginx代理做负载均衡指向8080和8081端口
- 本机启动两个客户端,启动jmemter测试,可以看到会有重复的剩余数量
- 此时我们就需要使用到redis的分布式锁来解决,这种也没有完美的解决方案
/**
* 分布式锁
* 1. stringRedisTemplate.opsForValue().setIfAbsent("key", "value");if (!flag){return "";}finally { stringRedisTemplate.delete("key");} 如果加锁后运维部署把程序kill了,其他线程永远进不来
* 2. stringRedisTemplate.opsForValue().setIfAbsent("key", "value", 10, TimeUnit.SECONDS);finally { stringRedisTemplate.delete("key");}
* 在超高并发时程序运行时间不一致,第一个线程15秒 10秒后锁自动解除程序还未运行完,第二个线程进来获得锁后第一个线程运行完把锁删除了 依次类推,释放的永远不是自己的锁
* 3. 动态设置锁的方法String lockValue = UUID.randomUUID().toString();Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lock, lockValue, 10, TimeUnit.SECONDS);if (!flag) { return ""; }
* if (lockValue.equals(stringRedisTemplate.opsForValue().get(lock))) { stringRedisTemplate.delete("key"); }, 这里还是会出现锁失效的可能
* 4. 续命 获取锁后开一个定时器进行在锁自动释放前获取当前自己锁是否被释放 如果没有证明代码还没执行完,进行续命,若执行完删除当前定时器
*/
redisson
redis客户端,内部实现了很多对分布式锁的支持
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.2</version>
</dependency>
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.150.110").setDatabase(0);
return ((Redisson) Redisson.create(config));
}
String lockKey = "key";
RLock redissonLock = redisson.getLock(lockKey);
try {
//内部使用续命的方式 使用lua脚本保证原子性
redissonLock.lock();
String countKey = "count";
//获取库存数量
int count = Integer.parseInt(stringRedisTemplate.opsForValue().get(countKey));
//有库存减少
if (count > 0) {
count = count - 1;
stringRedisTemplate.opsForValue().set(countKey, String.valueOf(count));
System.out.println("扣除成功,剩余数量:" + count);
} else {
System.out.println("扣除失败");
}
} finally {
redissonLock.unlock();
}
return "end";