全局唯一ID
下订单 这种情况,如果采用数据库的自增,则会规律明显,所以采用手动的情况下生产id
这是id的生产策略,注意这个32位,后面的代码需要移动32位
代码:
public class RedisIDWork {
//当前的时间
private static final long BEGIN_TIME=1652453880L;
@Autowired
private StringRedisTemplate stringRedisTemplate;
//递增全局id
public long nextID(String key){
//生产时间戳
//产生一个调用时候的时间戳然后减去begin即可完成
LocalDateTime now = LocalDateTime.now();
long end = now.toEpochSecond(ZoneOffset.UTC);
long time=end-BEGIN_TIME;
//生产序列号
//获取当天日期
String format = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
//自增长
Long increment = stringRedisTemplate.opsForValue().increment("icr:" + key + ":" + format);
//拼接并返回
return time<<32 |increment;
}
}
秒杀下单
背景:
主要是数据库的背景:
是由两张表构成 普通卷 和秒杀卷 秒杀卷有外键链接普通卷
普通卷:
秒杀卷:
效果:
流程:
代码:
@Transactional
@Override
public Result seckillVoucherByvoucherId(Long voucherId) {
//1:首先查询是否有优惠卷
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
if (seckillVoucher==null){
//1.1: 没有返回错误
return Result.fail("没有该优惠卷");
}
//1.2:有
//2查询是否开始 或者结束
LocalDateTime beginTime = seckillVoucher.getBeginTime();
LocalDateTime endTime = seckillVoucher.getEndTime();
//判断是否开始
if (beginTime.isAfter(LocalDateTime.now())){
//2.1 还没有开始
return Result.fail("活动暂未开始");
}
if (endTime.isBefore(LocalDateTime.now())){
//2.2过期 返回错误
return Result.fail("活动已经结束");
}
//2.3 有效期之内
//3 进行库存的查询
Integer stock = seckillVoucher.getStock();
if (stock<=0){
//3.1没有库存 返回信息
return Result.fail("抱歉该商品已经被抢完了");
}
//3.2 还有库存
//4: 库存减一
boolean update = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher_id", voucherId).update();
if (!update){
return Result.fail("库存不足 插入时失败");
}
//5:创建订单
VoucherOrder voucherOrder=new VoucherOrder();
//给订单id赋值
voucherOrder.setId(redisIDWork.nextID("skillVouCher"));
//优惠卷的id 赋值
voucherOrder.setVoucherId(voucherId);
//用户的id 利用拦截器获取到
voucherOrder.setUserId(UserHolder.getUser().getId());
//保存到数据库
save(voucherOrder);
//返回订单 的id
return Result.ok(voucherOrder.getId());
}
效果图:
订单表达到效果 有订单的增加:
秒杀卷表达到效果 有库存的减少:
秒杀下的超卖问题
测试
采用jemter进行测试:
测试结果:
不出所料 出现了超卖的问题
原因
原因很简单 ,高并发的访问下,有没有设置锁,当然会造成这种情况的产生。
解决方案:
加锁:
由于并发不一定经常发生,所以建议采用乐观锁
乐观锁 版本号
由于 我们的库存和version 都在改变的 所以完全可以用库存当version
于是有了下面这个方法
乐观锁 CAS法
CAS: compare and set
CAS 方法:
代码
只需要在原来的基础上 在插入时进行这样的判断即可:
结果
两白个线程 只卖出26件
原因:
加入 有1个线程修改了 但在他修改的时候 已经有99个线程进来
那么用了这个方法 则会导致 100线程里面 只有一个 能够成功下单。
CAS2.0:
代码:
只需要库存 大于0即可
结果:
刚好抢完 也刚好100条数据
超卖问题的总结
一人一单
流程修改:
其实很简单,就是订单里面 判断 userid和优惠卷id 是否 已经存在一模一样的,有则证明已经下过单了,没有则没下过
代码:
只需要添加这一步的判断即可
结果:
原因
原因依旧是 高并发下的不安全问题,我们的这个设置时候 依旧可能有线程进来了。
解决方案!!!:
只能是悲观锁了
由于乐观锁需要加版本 适用于修改的,我们的这个业务是插入的业务,初始为空的,哪里来的什么版本
思路
第一步方法拆分
我们的方法 需要加锁的拆除来即可
以一人一单以上的方法都是在进行查询操作,
所以下面需要加事务 加锁 上面的方法则可以把事务取消
第二步加锁
思考:
1.0
**1.0:**首先最快想到的是 加在方法上,如下图。但是由于我们的锁一旦加了,如果锁的对象一样 则会进行锁住一个一个执行。加在方法上面锁的的对象是this,每一次不管怎么样都要锁住 效率过于低下,在本场景有更优解。
2.0
2.0: 我们要预防的是同一个ID的用户,不进行多抢即可。那我们只需要锁住id即可,如下图
但是这样相当于没锁 为什么?
因为userid 是Long类型的,每一个线程来虽然值一样但是地址是不一样的,
我们的锁看的是地址不是值。
测试结果:
3.0
3.0 基于上述,我们只需要 保证userid 值对应的对象唯一即可
所以用常量池,第一时间应该想到string,所以转换成string就行了嘛?
看源码:
依旧会new
正解是采用string的intern 就是 按照值去常量池找 有则返回 常量池里面的,没有则放进去。
上锁:
结果:
一人一单的多机问题
准备:
复制服务,开一个8082端口
ctrl+D
修改Nginx :
以便于达到负载均衡的效果:
重启Nginx即可:
测试
代码不变,还是用jemeter 进行测试:
会出现两个订单
原因
因为是两个服务,常量池都变了,所以会出现锁住了但没完全锁住的情况
解决方法:
采用分布式锁
分布式锁
原理:
原理的单机锁,是在一个JVM里面进行锁的监视,分布式就是将监视器变成公用的
分布式锁的几种实现:
基于Redis的实现1.0:
代码:
接口类 ILock
public interface ILock {
/**
* @description:尝试获取锁
* @author:Code-zyc
* @date: 2022/5/15 9:59
* @param timeout :给锁设置的超时时间
* @return boolean : 获取成功返回true
*/
boolean tryLock(Long timeout);
/**
* @description: 释放锁
* @author:Code-zyc
* @date: 2022/5/15 10:00
*/
void unlock();
}
实现类 SimpleRedisLock
public class SimpleRedisLock implements ILock{
private static final String LOCK_PROFIX="lock:";
private String key;
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean tryLock(Long timeout) {
//获取线程名字,作为值存放
String name = Thread.currentThread().getName();
// 采用senet 进行存放数据
Boolean flag = stringRedisTemplate.opsForValue()
.setIfAbsent(LOCK_PROFIX + key, name, timeout, TimeUnit.SECONDS);
return Boolean.TRUE.equals(flag);
}
@Override
public void unlock() {
stringRedisTemplate.delete(LOCK_PROFIX+key);
}
public SimpleRedisLock() {
}
public SimpleRedisLock(String key, StringRedisTemplate stringRedisTemplate) {
this.key = key;
this.stringRedisTemplate = stringRedisTemplate;
}
}
逻辑处理类
在原来的基础上,把synchronized锁缓存redis锁
@Autowired
StringRedisTemplate redisTemplate;
@Transactional
public Result createVoucherOvder(Long voucherId){
//一人一单子
Long userid = UserHolder.getUser().getId();
SimpleRedisLock lock=new SimpleRedisLock("order"+userid,redisTemplate);
if (!lock.tryLock(1200L)){
//获取失败 直接返回错误或者重写尝试
return Result.fail("不能重复下单");
}
//下面部分用try包裹起来 最后进行 锁的释放
try {
//判断是否下单了
Integer count = query().eq("user_id", userid)
.eq("voucher_id", voucherId)
.count();
if (count>0){
return Result.fail("已经购买过了!");
}
//4: 库存减一
boolean update = seckillVoucherService.update()
.setSql("stock=stock-1")
.eq("voucher_id", voucherId)
.gt("stock", 0) //大于0即可
.update();
if (!update){
return Result.fail("库存不足 插入时失败");
}
//5:创建订单
VoucherOrder voucherOrder=new VoucherOrder();
//给订单id赋值
voucherOrder.setId(redisIDWork.nextID("skillVouCher"));
//优惠卷的id 赋值
voucherOrder.setVoucherId(voucherId);
//用户的id 利用拦截器获取到
voucherOrder.setUserId(userid);
//保存到数据库
save(voucherOrder);
//返回订单 的id
return Result.ok(voucherOrder.getId());
} finally {
lock.unlock();
}
}
结果:
非常成功,在分布式的情况下,没有出现 重复购买的情况。
Redis实现2.0
1.0的毛病
1.0可能也会产生问题:
上面这张图片主要就是表达,由于业务阻塞存在,线程1的到期了还没执行完过期了,线程2 来上锁成功了 但是线程1又执行完 来释放锁了出现了恶性循环,导致都能执行了
解决方案
就是 释放锁之前 看看是不是自己的锁,是在进行释放
由于是分布式的,所以不能依靠线程id进行判断,因为每一个JVM是各自维护id的。可以采用UUID+线程ID实现全局唯一性。
流程:
代码
只需要修改 unlock即可
public class SimpleRedisLock implements ILock{
private static final String LOCK_PROFIX="lock:";
private static final String VALUE_PROFIX= UUID.randomUUID().toString();
private String key;
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean tryLock(Long timeout) {
//创建对应的全局的id
String value = VALUE_PROFIX+ Thread.currentThread().getId();
// 采用senet 进行存放数据
Boolean flag = stringRedisTemplate.opsForValue()
.setIfAbsent(LOCK_PROFIX + key, value, timeout, TimeUnit.SECONDS);
return Boolean.TRUE.equals(flag);
}
@Override
public void unlock() {
//首先获取到 正确的value值
String value = VALUE_PROFIX+ Thread.currentThread().getId();
//从缓存中获取到
String redisvalue = stringRedisTemplate.opsForValue().get(LOCK_PROFIX + key);
if (value.equals(redisvalue)) {
//相同表示同一个锁 可以进行释放
stringRedisTemplate.delete(LOCK_PROFIX+key);
}
}
public SimpleRedisLock() {
}
public SimpleRedisLock(String key, StringRedisTemplate stringRedisTemplate) {
this.key = key;
this.stringRedisTemplate = stringRedisTemplate;
}
}
结果
结果成功的略。
Redis实现3.0
2.0的毛病:
依旧是阻塞导致的问题,我们虽然进行判断但是在进行判断的时候,有可能出现判断了 但是要去释放锁的时候阻塞了,这时候别人乘虚而入,导致问题的发生:
解决方案Lua:
实现检测了上锁的原子性,采用Lua脚本进行实现
代码
Lua脚本代码:
逻辑代码:
Redis锁总结:
Redission
前期准备:
导入依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
写配置类: 或是写在yaml里面
@Component
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config=new Config();
//设值链接地址和密码
config.useSingleServer().setAddress("redis://192.168.254.138:6379").setPassword("1234");
return Redisson.create(config);
}
}
代码
这个相比于前面的版本 只需要修改这几个部分即可:
tryLock 的构造函数:
**如果是无参的话:**就是非阻塞的不回去重新获取
有参的
自定义的缺陷:
1:不可重入
就是 同一个锁一个方法调用另一个的时候需要进行获取,但是方法1已经拿到锁了还没释放,所以产生了死锁的情况。
解决方案 采用记录的方式 先通过唯一标识看看是不是你在调用,是的话获取次数+1 直到为0 才释放锁。
由于这一些列都必须是 原子性的 所以采用Lua进行编写
获取锁的Lua脚本:
释放锁的Lua脚本:
redisson底层也是采用的Lua脚本进行的编辑:
2,3:
4:主从一致
进行传输的时候,突然主机挂掉,还未与从机之间进行数据的交流。
解决方案:
多个主机节点,每一次要设置锁都向这几个进行设置,只有获取到的数量等于了主机的数量,才能算是获取到了锁。
采用的是redisson自带的: Multilock
补充
关于缓存的过期时间 默认是 30s
你也可以自己设置: