第1步:基于Spring Initialzr方式创建zmall-redisson模块
第2步:在zmall-redisson模块中添加相关依赖
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
org.redisson
redisson-spring-boot-starter
3.17.0
第3步:配置application.yml
server:
port: 8081
spring:
redis:
host: 127.0.0.1
password: 123456
database: 0
port: 6379
@RestController
public class RedissonController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/updateStock")
public String updateStock() {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
return "end";
}
}
直接打开浏览器输入:http://localhost:8081/updateStock,查看redis中库存扣减情况。
第1步:配置多启动服务
第2步:配置nginx,实现负载均衡
upstream tomcats{
server 127.0.0.1:8081 weight=1;
server 127.0.0.1:8082 weight=2;
}
server
{
listen 80;
server_name localhost;
location / {
proxy_pass http://tomcats/;
}
}
第3步:配置jmeter,实现压测
创建测试用例,循环发送4组线程,每组200个;
查看redis中库存结果为0;查看多服务控制台信息均显示扣减失败,库存不足提示。
1)在单线程情况下,调用updatestock方法扣减库存,订单下单正常(没啥好说的)
2)在多线程情况下,调用updatestock方法扣减库存正常,订单下单异常(超卖了)原因分析:在高并发情况下同时多个线程调用updateStock方法,按照正常思路线程1、线程2、线程3应该是分别实现库存减一(在库存为100的情况下,现在应该剩余97),同时生成三个秒杀订单;然后并发情况下根本不会按照剧本设计来执行,而是出现了线程1、线程2、线程3同时扣减库存,导致库存剩余99,但是订单却产生了3个,说明超卖了。
@RestController
public class RedissonController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/updateStock")
public String updateStock() {
//jvm级锁,单机锁
synchronized (this){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}
return "end";
}
}
jvm级的同步锁,单机锁。上述同步代码块中,在单机环境下同一时刻只有一个线程能进行秒杀下单库存扣减,完毕之后才能有后续线程进入。但是在分布式环境下依然还是会出现商品超卖情况。
重新启动jmeter压测,连续发送4组,每组200个请求。
格式:setnx key value
将key的值设置为value,当且仅当key不存在;若给定的key存在,则setnx不做任何动作。
setnx是set if not exists
(如果不存在,则set)的简写。
setnx "zking" "xiaoliu" 第一次设置有效
setnx "zking" "xiaoliu666" 第二次设置无效
第一次使用setnx设置zking直接成功,第二次使用setnx设置zking则失败,也意味着加锁失败。
redis级分布式锁之setnx使用
@RestController
public class RedissonController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/updateStock")
public String updateStock() {
//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zking");
if(!flag)
return "error_code";
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
//解锁
stringRedisTemplate.delete(lockKey);
}
return "end";
}
基于以上redis分布式锁setnx的代码,实现场景分析。
解决办法:通过try/catch/finally代码块解决。
//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
try{
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zking");
if(!flag)
return "error_code";
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}finally{
//解锁
stringRedisTemplate.delete(lockKey);
}
解决办法:加锁时设置过期时间,确保原子性。
//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
try{
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,"zking",10,TimeUnit.SECONDS);
if(!flag)
return "error_code";
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}finally{
//解锁
stringRedisTemplate.delete(lockKey);
}
场景分析:
线程1:业务执行时间15s,加锁时间10s,那么导致业务未执行完成锁被提前释放;
线程2:业务执行时间8s,加锁时间10s;
线程3:业务执行时间5s,加锁时间10s,那么导致线程2的任务还没有执行完成就是线程3将所删除掉了;以此类推,只要是高并发场景一直存在,那么锁一直处于失效状态(永久失效)
解决办法:可以在加锁的时候设置一个线程ID,只有是相同的线程ID才能进行解锁操作。
//使用redis级分布式锁setnx加锁
String lockKey="lockKey";
String clientId= UUID.randomUUID().toString();
try{
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10,TimeUnit.SECONDS);
if(!flag)
return "error_code";
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
}finally{
//只有是相同的线程ID时才进行解锁操作
if(stringRedisTemplate.opsForValue().get(lockKey).equals(clientId)) {
//业务代码执行完毕删除redis锁(解锁)
stringRedisTemplate.delete(lockKey);
}
}
**问题4:**锁要加多次时间才是最合理有效的?
解决办法:redisson,看门狗机制。
Redisson - 是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象,Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端,Jedis、Lettuce 的 API 更侧重对 Redis 数据库的 CRUD(增删改查),而 Redisson API 侧重于分布式开发。
特点:
创建RedissonConfig配置类
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String password;
@Bean
public RedissonClient redissonClient(){
Config config=new Config();
String url="redis://"+host+":"+port;
config.useSingleServer().setAddress(url).setPassword(password).setDatabase(0);
return Redisson.create(config);
}
}
使用redisson分布式锁实现秒杀下单
@RequestMapping("/updateStock")
public String updateStock() {
String lockKey="lockKey";
RLock clientLock = redissonClient.getLock(lockKey);
clientLock.lock();
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));//jedis.get("stock");
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");//jedis.set("stock",realStock+"");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} finally {
//解锁
clientLock.unlock();
}
return "end";
}
重新启动jmeter压测,连续发送4组,每组200个请求。查看多服务控制台,结果显示秒杀订单下单正常,无超卖情况发生。
第1步:在zmall-order模块中配置pom.xml
org.redisson
redisson-spring-boot-starter
3.17.0
第2步:创建Redisson配置类
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password}")
private String password;
@Bean
public RedissonClient redissonClient(){
Config config=new Config();
String url="redis://"+host+":"+port;
config.useSingleServer().setAddress(url).setPassword(password).setDatabase(0);
return Redisson.create(config);
}
}
第3步:整合项目实现Redisson分布式锁
@Transactional
@Override
public JsonResponseBody> createKillOrder(User user, Integer pid) {
//6.根据秒杀商品ID和用户ID判断是否重复抢购
Order order = redisService.getKillOrderByUidAndPid(user.getId(), pid);
if(null!=order)
return new JsonResponseBody<>(JsonResponseStatus.ORDER_REPART);
RLock clientLock = redissonClient.getLock("scekill:goods:" + pid);
clientLock.lock();
try {
//7.Redis库存预减
long stock = redisService.decrement(pid);
if (stock < 0) {
redisService.increment(pid);
return new JsonResponseBody<>(JsonResponseStatus.STOCK_EMPTY);
}
//创建订单
order = new Order();
order.setUserId(user.getId());
order.setLoginName(user.getLoginName());
order.setPid(pid);
//将生成的秒杀订单保存到Redis中
redisService.setKillOrderToRedis(pid, order);
//将生成的秒杀订单推送到RabbitMQ中的订单队列中
rabbitTemplate.convertAndSend(RabbitmqOrderConfig.ORDER_EXCHANGE,
RabbitmqOrderConfig.ORDER_ROUTING_KEY, order);
}catch (Exception e){
e.printStackTrace();
throw new BusinessException(JsonResponseStatus.ORDER_ERROR);
}finally {
clientLock.unlock();
}
return new JsonResponseBody<>();
}
重新启动jmeter压测。