一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第17天,点击查看活动详情。
引言
应用场景:秒杀商品
分布式锁方案:本文以方案一和方案四为例子
- SETNX + EXPIRE
- SETNX + value值是(系统时间+过期时间)
- 使用Lua脚本(包含SETNX + EXPIRE两条指令)
- SET的扩展命令(SET EX PX NX)
- SET EX PX NX + 校验唯一随机值,再释放锁
- 开源框架~Redisson
- 多机实现的分布式锁Redlock
方案五可能存在「锁过期释放,业务没执行完」的问题,开源框架Redisson解决了这个问题。
给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。
I 并发处理
1.1 用压测模拟并发
Apache ab,模拟并发性,简单,要求低,不会占用很多的cpu,也不会占用很多内存。
ab -n 100 -c 100 接口地址(-n 表示发出100个请求 ,-c 表示100个并发)
ab -t 60-c 100 接口地址 (-t 表示连续60秒内不停发请求,-c 表示100个并发)
➜ retail git:(develop) ab
ab: wrong number of arguments
Usage: ab [options] [http[s]://]hostname[:port]/path
Options are:
-n requests Number of requests to perform
-c concurrency Number of multiple requests to make at a time
-t timelimit Seconds to max. to spend on benchmarking
This implies -n 50000
-s timeout Seconds to max. wait for each response
Default is 30 seconds
-b windowsize Size of TCP send/receive buffer, in bytes
-B address Address to bind to when making outgoing connections
-p postfile File containing data to POST. Remember also to set -T
-u putfile File containing data to PUT. Remember also to set -T
-T content-type Content-type header to use for POST/PUT data, eg.
'application/x-www-form-urlencoded'
Default is 'text/plain'
1.2 处理并发的方案
synchronized处理并发: synchronized 关键字声明的方法同一时间只能被一个线程访问,synchronized 修饰符可以应用于四个访问修饰符。
(1)需要消耗大量的CPU资源
(2)synchronized 无法做到细粒度的控制,只适合单点的情况。
- 基于redis的分布式锁
(1)redis有很高的性能,支持分布式,高可用。 (2)更细粒度的控制,多台机器上多个进程对一个数据进行操作的互斥(比如以商品ID作为key进行控制)。
(3)redis是单线程的,对此支持的命令较好,实现起来比较方便。
II synchronized案例
2.1 synchronized案例:Java多线程安全
java的线程同步机制很大程度上都是基于内存模型而设定的。
java内存模型 :
多线程的可见性:当一个共享变量在多个线程的工作内存中都有副本时,如果一个线程修改了这个共享变量,那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见性问题。
当线程操作某个对象时,执行顺序如下:
从主存复制变量到当前工作内存 (read and load)
执行代码,改变共享变量值 (use and assign)
用工作内存数据刷新主存相关内容 (store and write)
有序性: 线程在引用变量时不能直接从主内存中引用,如果线程工作内存中没有该变量,则会从主内存中拷贝一个副本到工作内存中,这个过程为read-load,完成后线程会引用该副本。 当同一线程再度引用该字段时,有可能重新从主存中获取变量副本(read-load-use),也有可能直接引用原来的副本 (use),也就是说 read,load,use顺序可以由JVM实现系统决定。
java用synchronized关键字做为多线程并发环境的执行有序性的保证手段之一。当一段代码会修改共享变量,这一段代码成为互斥区或临界区,为了保证共享变量的正确性,synchronized标示了临界区。
// 理论上,每个对象都可以做为锁,但一个对象做为锁时,应该被多个线程共享,这样才显得有意义,在并发环境下,一个没有共享的对象作为锁是没有意义的。
synchronized(锁){
// 临界区代码
}
一个线程执行临界区代码过程如下:
- 获得同步锁
- 清空工作内存
- 从主存拷贝变量副本到工作内存
- 对这些变量计算
- 将变量从工作内存写回到主存
- 释放锁
可见,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。
2.2 synchronized案例:OC安全隐患解决方法(互斥锁)
blog.csdn.net/z929118967/…
@synchronized(锁对象) { // 需要锁定的代码 }
注意:锁定1份代码只用1把锁,用多把锁是无效的
- 互斥锁的优缺点 优点:能有效防止因多线程抢夺资源造成的数据安全问题 缺点:需要消耗大量的CPU资源
- 互斥锁的使用前提:多条线程抢夺同一块资源
线程同步的意思是:多条线程在同一条线上执行(按顺序地执行任务), 互斥锁,就是使用了线程同步技术。
/**
atomic:原子属性,为setter方法加锁(默认就是atomic)
nonatomic:非原子属性,不会为setter方法加锁
1. nonatomic和atomic对比
atomic:线程安全,需要消耗大量的资源
nonatomic:非线程安全,适合内存小的移动设备
2. iOS开发的建议
1)所有属性都声明为nonatomic
2)尽量避免多线程抢夺同一块资源
3)尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力。【具体情况具体分析】
*/
@property (nonatomic,strong) NSThread *thread1;
- (void)run{
while (1) {
@synchronized(self) {//加锁
int count = self.poll;
if (count>0) {
[NSThread sleepForTimeInterval:0.1];
self.poll--;
NSLog(@"%s---name:%@,poll = %d",__func__,[[NSThread currentThread] name],self.poll);
}else{
break;
}
}//解锁
}
}
可变的数组、字典、或者字符串不是线程安全的。
III redis分布式锁
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- SETNX key value:只在键 key 不存在的情况下, 将键 key 的值设置为 value 。若键 key 已经存在, 则 SETNX 命令不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
SETNX KEY_NAME VALUE
- GETSET:将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
GETSET key value
-
expire key timeout:设置 key 的过期时间,key 过期后将不再可用。单位以秒计。超过这个时间锁会自动释放,避免死锁。 -
delete key:删除key
3.1 思想
将设置值和过期时间合并成一步操作:
- 获取锁的时候,使用setnx加锁,并给锁加一个超时时间,超过该时间则自动释放锁。
- 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
- 释放锁的时候,通过value判断是不是该锁,若是该锁,则执行delete进行锁释放。
获得锁的条件:锁超时或者第一次获取锁 释放锁的时机:任务完成
给锁加一个超时时间是避免异常情况没有进行解锁或者解锁异常了,造成死锁。
超时时间的控制可以使用expire命令,或者value 为当前时间+超时时间 ,自己结合当前时间进行判断。
3.2 实现
- 超时时间控制:使用expire命令
//加锁
String token = UUID.randomUUID().toString();
// NX是不存在时才set, XX是存在时才set, EX是秒,PX是毫秒
String lock = jedis.set(key, token, "NX", "EX",TIMEOUT);
- 超时的实现:value 为当前时间+超时时间 ,自己结合当前时间进行判断。
从资源释放的角度,推荐redis中的key都设置expire,即便是1年的时候。
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加锁
* @param key 例如:productId:123456;skuId
* @param value 当前时间+超时时间 例如:1582891777814
* @return
*/
public boolean lock(String key, String value) {
log.info("value={}",value);
/*如果key不存在 就赋值 返回true 加锁成功 相当于reids的setnx*/
if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
return true;
}
//如果key存在 获取当前key已经存在的value
String currentValue = redisTemplate.opsForValue().get(key);
log.info("currentValue={}",currentValue);
//如果锁过期
if (!StringUtils.isEmpty(currentValue)
&& Long.parseLong(currentValue) < System.currentTimeMillis()) {/*如果当前时间大于超时时间 说明已经抢单加锁超过10秒了*/
//获取上一个锁的时间
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);/*获取当前key的旧value 同时把新的value set进去*/
if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {/*代码严谨性判断*/
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);
}
}
}
3.3 应用
模拟不同用户秒杀同一商品的请求
private static final int TIMEOUT = 10 * 1000; //超时时间 10s
@Autowired
private RedisLock redisLock;//redis分布式锁
@Override
public void orderProductMockDiffUser(String productId)
{
// 加锁
long time = System.currentTimeMillis() + TIMEOUT;
log.info(String.valueOf(time));
Boolean str = redisLock.lock(productId,String.valueOf(time));
if(!str){
throw new SellException(101,"人太多了,换个姿势试试!");
}
//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);
}
//解锁
redisLock.unlock(productId,String.valueOf(time));
}
see also
redis缓存的使用