业务场景:高并发场景下的减库存代码实现
方案一:使用JVM或JDK级别的锁【synchronized】
问题:使用synchronized的加锁,如果是单机环境的话没有问题,但是对于集群/分布式环境则会出问题,对于跨tomcat就会锁不住。
@RestController public class IndexControlelr { @Autowired private Redisson redisson; @Autowired private StringRedisTemplate stringRedisTemplate; @RequestMapping("/deduct_stock") public String deductStock() { //以下的代码高并发场景下有问题 synchronized(this) { int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); //获取redis值 jedis.setnx(key.value) if (stock > 0) { int realStock = stock - 1; stringRedisTemplate.opsForValue().set("", realStock + ""); //设置redis值 jedis.set(key.value) Sysytem.out.printLn("扣减成功,剩余库存:" + realStock); } else { Sysytem.out.printLn("扣减失败,库存不足"); } } return "end"; } }
方案二:为了解决方案一的问题,使用redis分布式锁的SETNX命令可以解决刚刚方案一的问题。
使用格式:setnx key value 将key的值设为value,当且仅当key不存在。若给定的key已存在,则SETNX不做任何操作。
问题:会出现死锁,就是当程序执行一般,中间的代码出现异常导致无法释放这把锁,此时就会出现死锁的现象。
//使用redis分布式锁 @RequestMapping("/deduct_stock") public String deductStock() { String lockKey = "lockKey"; boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"xl"); if(!result){ return "error_code"; }
//如果执行到这里下面的代码抛异常则无法完成释放锁,死锁产生 int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); //获取redis值 jedis.setnx(key.value) if (stock > 0) { int realStock = stock - 1; stringRedisTemplate.opsForValue().set("", realStock + ""); //设置redis值 jedis.set(key.value) Sysytem.out.printLn("扣减成功,剩余库存:" + realStock); } else { Sysytem.out.printLn("扣减失败,库存不足"); } //释放锁 stringRedisTemplate.delete(lockKey); return "end"; }
方案三:为了解决方案二的问题,设置key和操作时间+try ...catch...finally释放锁
问题:时间问题,高并发场景下此代码将就可以用了,但是会出现自己加的锁会被别人释放掉。
@RequestMapping("/deduct_stock") public String deductStock() { String lockKey = "lockKey"; try { //解决:给锁加一个超时时间, //boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"xl"); //stringRedisTemplate.expire(lockKey,10,TimeUnit.SECONDS); //设置key和操作时间==保证原子性 将上面的两行合并为下面的一行 boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"xl",10,TimeUnit.SECONDS); if(!result){ return "error_code"; } int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); //获取redis值 jedis.setnx(key.value) if (stock > 0) { int realStock = stock - 1; stringRedisTemplate.opsForValue().set("", realStock + ""); //设置redis值 jedis.set(key.value) Sysytem.out.printLn("扣减成功,剩余库存:" + realStock); } else { Sysytem.out.printLn("扣减失败,库存不足"); } //解决释放锁的问题使用finally释放锁 }funally { //释放锁 stringRedisTemplate.delete(lockKey); } return "end"; }
方案四:为了解决方案三的问题,生成UUID,将这个UUID设置到锁对应的value里面,自己加的锁只能自己释放,并在后台启动一个分线程每个一段时间(一般这个时间是你设置的超时时间的1/3,超时时间一般默认为30s)去检查一下主线程是否还持有这把锁,如果还持有这把锁的话就把设置的超时时间顺延30s
问题:代码量太大了
@RequestMapping("/deduct_stock") public String deductStock() { String lockKey = "lockKey"; //生成一个UUID String clientId = UUID.randomUUID().toString(); try { //boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"xl"); //stringRedisTemplate.expire(lockKey,10,TimeUnit.SECONDS); //将UUID设置到锁对应的value里面 //这里的时间还是会存在问题==解决,在后台起一个分线程:起一个定时任务每10s(不超过30s,设置值的1/3时间)检查一下主线程是否还持有这把锁, //如果还持有把这个超时时间延长(重新设置30s),====>问题代码量太大了 //解决方案:reddison框架底层的原理就是我们现在写的这些逻辑。 //使用:pom直接引入依赖包即可,可以支持很多redis架构模式(主从,哨兵,高并发等) boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,30,TimeUnit.SECONDS); if(!result){ return "error_code"; } int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); //获取redis值 jedis.setnx(key.value) if (stock > 0) { int realStock = stock - 1; stringRedisTemplate.opsForValue().set("", realStock + ""); //设置redis值 jedis.set(key.value) Sysytem.out.printLn("扣减成功,剩余库存:" + realStock); } else { Sysytem.out.printLn("扣减失败,库存不足"); } //解决释放锁的问题使用finally释放锁 }funally { //判断一下这把锁是不是自己加的锁(线程id) if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){ stringRedisTemplate.delete(lockKey); } } return "end"; } }
方案五:为了解决方案四的问题。使用Redission框架
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class,args); } @Bean public Redisson redisson() { Config config = new Config(); config.useSingleServer().setAddress("redis://localhost:8769").setDatabase(0); return (Redisson) Redisson.create(config); } } ----------------------------------------------------------------------------------------------------- @RequestMapping("/deduct_stock") public String deductStock() { String lockKey = "lockKey"; RLock redissonLock = redisson.getLock(lockKey); try { //加锁默认的超时时间30s redissonLock.lock(); //setIfAbsent(lockKey,clientId,30,TimeUnit.SECONDS); int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); //获取redis值 jedis.setnx(key.value) if (stock > 0) { int realStock = stock - 1; stringRedisTemplate.opsForValue().set("", realStock + ""); //设置redis值 jedis.set(key.value) Sysytem.out.printLn("扣减成功,剩余库存:" + realStock); } else { Sysytem.out.printLn("扣减失败,库存不足"); } }funally { //释放锁 redissonLock.unlock(); } } return "end"; }
Redission 的源码剖析
Lua脚本具有原子性,所以redis把上面的一整串字符串当作一条命令来执行,要么成功,要么失败(解决了分布式一致性的问题)。
Redission分布式锁实现原理
Redis集群架构一般是满足AP,redis主节点获取到锁之后会立马返回给客户端。QPS理论上是10万,但是一般达不到10万,才几万。
zookeeper是CP:一致性,主节点获取到锁之后先同步给从节点,半数以上的从节点获取到锁之后再返回给客户端。
****一般会使用zookeeper来做分布式锁,redis做缓存,但是还是有很多的公司选择使用redis来做分布式锁,因为redis的性能高(对并发要求比较高的情况)。