一、分布式锁
我们这里使用ab这个工具来进行并发测试
第二行的解读:
-n表示发出100个请求,-c模拟100个并发,相当于100个人同时访问这个百度网址
第三行解读:
-t表示60秒,-c表示100个并发,它会在60秒内,不停的100个并发。
二、synchronized处理并发
1.这里我们需要添加一个秒杀的功能,我们的商品是限量抢购的,10000件皮蛋瘦肉粥,每件只要1分钱,所以可能同时会有很多人来抢,有的人能抢到,有的人抢不到,这里我们就需要考虑使用并发。
首先介绍 SecKillController
这里有两个接口,一个是查询秒杀活动特价商品的信息的接口,这个接口会返回被秒杀的商品的信息,还剩多少件,是什么商品。、
第二个接口就是秒杀的接口,没有抢到获得"哎呦喂,xxxxx",抢到了会返回剩余的库存量。
@RestController
@RequestMapping("/skill")
@Slf4j
public class SecKillController {
@Autowired
private SecKillService secKillService;
/**
* 查询秒杀活动特价商品的信息
* @param productId
* @return
*/
@GetMapping("/query/{productId}")
public String query(@PathVariable String productId)throws Exception
{
return secKillService.querySecKillProductInfo(productId);
}
/**
* 秒杀,没有抢到获得"哎呦喂,xxxxx",抢到了会返回剩余的库存量
* @param productId
* @return
* @throws Exception
*/
@GetMapping("/order/{productId}")
public String skill(@PathVariable String productId)throws Exception
{
log.info("@skill request, productId:" + productId);
secKillService.orderProductMockDiffUser(productId);
return secKillService.querySecKillProductInfo(productId);
}
}
接口SecKillService
// An highlighted block
public interface SecKillService {
/**
* 查询秒杀活动特价商品的信息
* @param productId
* @return
*/
String querySecKillProductInfo(String productId);
/**
* 模拟不同用户秒杀同一商品的请求
* @param productId
* @return
*/
void orderProductMockDiffUser(String productId);
}
Service层实现类SecKillServiceImpl
这里模拟了三个map,分别对应着三个信息,商品的信息,库存,还有订单。
products.put(“123456”, 100000);
stock.put(“123456”, 100000);
这个表示,商品id和库存存进去。
第二个方法就是秒杀的方法,这个方法里面分为4步,
1.在下单这个商品之前,先判断这个商品有没有库存,如果没有库存就直接打印获得结束。
2.如果有库存,那就下单,,模拟不同的微信号,并把这个商品号添加进去。
3.减库存。
讲商品库存减一,然后让这个线程sleep100ms。
4.然后再更新一下这个库存。
@Service
public class SecKillServiceImpl implements SecKillService {
private static final int TIMEOUT = 10 * 1000; //超时时间 10s
@Autowired
private RedisLock redisLock;
/**
* 国庆活动,皮蛋粥特价,限量100000份
*/
static Map<String,Integer> products;
static Map<String,Integer> stock;
static Map<String,String> orders;
static
{
/**
* 模拟多个表,商品信息表,库存表,秒杀成功订单表
*/
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
products.put("123456", 100000);
stock.put("123456", 100000);
}
private String queryMap(String productId)
{
return "国庆活动,皮蛋粥特价,限量份"
+ products.get(productId)
+" 还剩:" + stock.get(productId)+" 份"
+" 该商品成功下单用户数目:"
+ orders.size() +" 人" ;
}
@Override
public String querySecKillProductInfo(String productId)
{
return this.queryMap(productId);
}
@Override
public void orderProductMockDiffUser(String productId)
{
//加锁
//1.查询该商品库存,为0则活动结束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活动结束");
}else {
//2.下单(模拟不同用户openid不同)
orders.put(KeyUtil.genUniqueKey(),productId);
//3.减库存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stock.put(productId,stockNum);
}
//解锁
}
}
我们现在在网页上测试一下,秒杀一下这个商品,每刷新一次,就会购买一件这个商品,然后库存就会减一。
我们使用ab工具,进行500个请求,创建100个线程。
压测结束,再查询商品。发现,两个数加一起,不是10000份,这就有问题了。
问题就是:在秒杀的过程中,线程sleep的时候,仍然有很多用户在下单,这就会造成下单数量远远大于减库存的数量。
我们再这个方法中加上关键字Synchronized,,这样在进行并发测试。
再次进行测试,我们会发现我们发出500个请求,这个过程会非常的慢,原因就是我们在这个方法前加了synchronized关键字,这个方法每次只能一个线程访问,这样就会非常慢。
但是结果是正确的,没什么问题。
总结:
使用synchronized的缺点
1.时间太慢
2,无法做到细粒度的控制,也就是说如果我们有很多个商品需要秒杀,我们使用synchronized这个方法,只能一视同仁,不能区分某个商品被秒杀的多,那个被秒杀的少,所以无法区分,无法做到细粒度的控制。
3,只能适合单点的情况,
三、Redis分布式锁
1.首先介绍一下两个命令,setnx
2.先get再set,Getset方法。
Getset方法。我们拆开来理解,先get到1,打印,然后再将其设置为0,
我们现在就用Redis来去解决这个问题。
我们在这里写加锁和解锁的处理,写到Service层里
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加锁
* @param key
* @param value 当前时间+超时时间
* @return
*/
public boolean lock(String key, String value) {//加锁的时候给他穿两个参数,一个key,一个value。
if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
return true;//如果可以设置的话,就返回true,否则返回false。
}
//加入两个线程要去执行,这两个线程的值都是B,其中一个线程拿到锁;
//currentValue=A 这两个线程的value都是B 其中一个线程拿到锁
String currentValue = redisTemplate.opsForValue().get(key);
//如果锁过期
if (!StringUtils.isEmpty(currentValue)//如果它的内容不是空的,
&& Long.parseLong(currentValue) < System.currentTimeMillis()) {
//获取上一个锁的时间,如果线程1执行,则oldvalue仍然是A,但是,value是B已经设置进去 啦。
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {//这里的A就等于原来的A,所以执行true、
return true;
}
}
return false;
}
/**
* 解锁
* @param key
* @param value
*/
public void unlock(String key, String value) {
try {
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e) {
log.error("【redis分布式锁】解锁异常, {}", e);
}
}
}
1.首先介绍一下,第一种死锁的情况,如果没有这部分,一旦在下单过程中发生异常,会导致解锁步骤不再进行,就会死锁。
2.如果有这段代码,我们就会再发生异常的时候,我们的这个里面的值小于当前时间的话,我们就会进行一些操作,
这里我们假设,上一个锁的时间oldvalue为A,然后要进入的两个线程的value值都是B,这时oldvalue的值为A,我们执行
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
这句话,得到的还是原来的valueA,但是我们已经把它设置成了现在的valueB。
然后再进行判断,第一个线程得到的oldvalue是A,然后当前的value currentValue也是A.所以就执行true。
然后第二个线程value为B,再次执行这句话,我们就得到了
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
此时的oldValue是B,上一个线程的值,但是当前的currentValue还是A,此时第二个if就不成立,返回false。所以B没有拿到锁,所以只有一个线程拿到锁。
这就避免了死锁。
那么线程a执行完之后会发生什么呢?
redis服务器中锁的value是100,现在有A、B两个进程想要竞争锁,这进程A在200的时候竞争锁,进程B在300的时候竞争锁,但是由于网络的原因,两个进程同时获取到了redis服务器中锁的value,也就是100,比较之后发现锁已经过期,两个进程便同时竞争锁。假设进程A获得了锁,redis服务器中锁的value变成了200+TIMEOUT;进程B在getSet的时候得到的是200+TIMEOUT,不等于100,所以获取锁失败,但进程B已经将redis服务器中锁的value改成了300+TIMEOUT。那么,当进程A完成操作后,想要主动释放锁的时候,就会释放失败。这样会出现这样的情况,进程B再次尝试获取锁的时候,误以为锁仍然被其他进程占有,直到TIMEOUT时间过后发现锁过期,再次竞争锁(因为进程B的当前时间是300,redis服务器中锁的值是300+TIMEOUT)。只要出现一次锁过期的情况,并且在锁过期的时候有并发的竞争锁,就会一直出现这个问题。
然后再进行解锁的操作,仍然是传进来,一个key一个value,解锁就是删掉key的一个操作,如果当前的value等于传进来的value,那么我们就删掉key。
然后回到SecKillServiceImpl 给线程加锁,然后执行下面的语句,最后再解锁。
以上就是我们实现的redis的分布式锁,也是redis实现分布式锁的核心代码,redis作为单线程服务的,又是nosql数据库,每秒支持十几万的并发,redis是高可用,分布式,集群,
上面我们以商品id为redis的key,采用分布式锁,可以更细粒度的控制代码,可以理解为分布式锁就是多台机器,多个进程,对一个数据进行操作的互斥。
redis能作为分布式锁,很重要的原因就是redis是单线程的,利用了setnx和getset命令这种特点,实现了分布式锁。
四、redis缓存
tomcat中是我们的java应用,如果我们要请求商品的信息,当请求来了之后,我们第一步是先在redis中找有没有这个商品的信息,如果有直接返回就行了,如果没有,就去mysql中去找,找到之后,把这个商品信息存到redis中去,这样下次再查该商品的信息的时候,就可以直接在redis中去找了,所以,如果对mysql中的信息进行crud,那么也要同时更新redis中的信息。
1.我们拿商品列表这个接口做例子来去说明我们怎样使用redis缓存,
a.现在启动类上面加一个注解,@EnableCaching,
在这个商品列表查询方法前加上列表。加上cacheable(里面写上商品的和key=123).
我们第一次访问这个方法的时候,就把这个product的信息放到了redis中,第二次再访问的时候,我们就不会再执行这个程序的里面的内容了,直接从redis中查询结果就打印出来了,这就是缓存的威力。这个key就是redis中的key
现在我们进行一个操作,在前端把芒果冰的价格改为21。
然后我们更新一下,发现数据库中的内容,价格确实改为了21,
这时我们再访问这个列表,还是直接从人redis中去数据,但是这时的芒果冰的价格还是20。缓存里面的数据并没有跟着数据库的更新,而更新。
2.解决上面的问题的方法:
当我们在更新商品信息的时候,也加一个缓存,让缓存也改变。里面的信息要和cacheable保持一致,但是注解不一样,说一下两者的区别。
cacheable这个注解,是第一次访问之后,第二次再访问,就直接在redis中取信息,不用再执行这个里面的方法的程序
cacheput这个注解不一样,每次访问的时候,都需要执行这个方法里面的程序。每次返回的内容,都放到这个redis中去,就相当于更新的操作。
cacheput这个注解有个问题,就是返回的是modeAndView,但是我们要保存到redis是resultVo,所以,我们使用另外一个注解
chaheEvict注解,这个注解是在执行下面的方法之后,清除掉redis里的内容删除。
使用这个注解之后,我们再在前端更新这个商品信息的时候,我们使用save方法,就会使用这个注解,这个注解就会把原来的redis中的商品信息删除,然后我们再查询列表的时候,又变成了第一次查询,还是从数据库中查,然后再把新 的结果放到redis中,方便下次查询。
3.在此插入一条序列化的知识实现Serializable接口,然后里面有一个序列号。
什么是Serializable接口?
一个对象序列化的接口,一个类只有实现了Serializable接口,它的对象才能被序列化。
什么是序列化?
将对象的状态信息转换为可以存储或传输的形式的过程,在序列化期间,对象将其当前状态写入到临时存储区或持久性存储区,之后,便可以通过从存储区中读取或反序列化对象的状态信息,来重新创建该对象。
什么情况下需要序列化?
当我们需要把对象的状态信息通过网络进行传输,或者需要将对象的状态信息持久化,以便将来使用时都需要把对象进行序列化。
Serializable是java所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作,使用Serializable来实现序列化很简单,只要在类的声明中指定一个类似下面的标识即可自动实现默认的序列化过程。
serialVersionUID
让一个对象实现序列化,只要这个类实现Serializable接口并声明一个serialVersionUID即可,实际上serialVersionUID也不是必须的,不声明它同样可以实现序列化,但是这将会对反序列化过程产生影响。
serialVersionUID是用来辅助序列化和反序列化过程的,序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中,当反序列化的时候系统会检测当前文件中的serialVersionUID是否和当前类的serialVersionUID一致,如果一致这个时候可以反序列化成功,否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型可能发生了改变,这个时候是无法正常反序列化的。
4.在2的方法中我们实现了更新缓存的操作,但是我们如果不想使用删除的注解,使用更新的注解,怎样实现更新呢。
我们拿ProductServiceImpl层的方法为例
在查找和保存的两个方法加入注解
我们先查询这个商品,然后在前端修改这个商品的信息,然后再保存,这时就已经更新了redis中缓存的商品的信息了。然后再查询商品的信息,这时候就不会再调用查询方法了。
使用这个方法的前提是,两个方法返回的类型要保持一致。
补充1:
注解中的key是否可以删除?答案是不行,key相当于强制把两个方法的参数设置为一致,这样他们才知道对方是谁。
如果把cacheNames=“product” 去掉,那么我们需要在最上面加上CacheConfig注解才可以。
补充2:
我们的key可以写成另外一种形式,叫做SPEL表达式。
补充3:
我们可以在注解里面加条件,如果这个条件成立才会缓存。
补充4:
我们想根据结果来判断是否缓存,使用unless=,如果结果不等于0就缓存,这里的result就相当于方法中的返回对象ResultVO。
这个使用较多,因为我们不可能把错误的结果也缓存起来。
补充5:
我们可以使用Cacheable、CachePut、CacheEvict这三个注解,实现对一些方法执行之后的缓存。
但是我们要缓存的对象,涉及到的类(DAO层),一定要进行序列化才可以。
缓存不可滥用。