type
status
date
slug
summary
tags
category
icon
password
redis 全局 ID 好处

唯一性:如果是分布式的话,不同的数据库之间需要保证数据的唯一性,虽然是不同的服务不同的库,但服务用的都是一个 redis(可以是集群的),redis 也是有自增主键的,这样的话就可以保证数据 id 的唯一性。
高可用:可以建立集群。
高性能:基于内存
递增性:redis 有递增方法
安全性:虽然有递增主键,但是如果只是简单的递增,容易被被人发现规律,可以拼接上时间戳的方式保证安全性。

代码实现
秒杀流程

按照正常流程测试是没有问题的,但是用 jemeter 测试会出现超卖现象。
库存 stock 变为了- 3 张

解决方案
乐观锁
乐观锁是在修改数据的时候假设没有其他线程修改过数据,在修改的时候再去判断有没有修改过数据。
这里也有两种方案,一种是使用版本号的方式进行判断,修改数据之前先查询一下版本号,修改的时候如果版本号一致就说明数据被人修改过了,就抛出异常。
还有一种是修改哪个字段的数据就用他本身来判断,也是在修改的时候先查询一遍,修改的时候以查询出来的数据作为条件进行修改。这叫做 CAS 自旋锁法
代码实现:
结果:
开了两百个线程,只卖出 34 张,有人失败了,竟然还没有卖完。

image-20230726111607572
得到这种结果也可以想到,因为更多的线程失败了。
可以改为下面这样
库存大于 0 才能修改
但是这样就和乐观锁无关了。
悲观锁
直接加锁,简单粗暴,但是效率会很低。
一人一单
需要在查询完库存之后,根据用户 id 和优惠券 id 进行联合查询,如果已经下过单了,就返回异常。
这样写肯定有多线程并发问题,第一次查询 count=0,还未插入数据的时候,第二个线程也查询 count=0 就会造成多线程并发的问题。
加锁
可以把获取用户,到插入该用户购买的订单加上锁,这样就可以保证同一次只有一个锁进来。
抽取成一个方法,加锁
使用serId.toString().intern()作为锁对象,那么不同的用户用的不同的锁。
事务生效
但是这样事物不生效,因为发生了自调用,使用的是本对象调用的方法,不是代理对象,所以无法交给 spring 管理。可以手动获取代理对象,调用方法。