java高并发-商品秒杀redis篇 watch功能

代码:https://codechina.csdn.net/wwwzhouzy/rediswatch

1、将商品数量初始化到redis

import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import com.zhouzy.boot.zhouzyBoot.controller.ApiController;

@Component
@SuppressWarnings("rawtypes")
public class InitData {
	Integer shopNum = 110;
	private static Logger logger = LoggerFactory.getLogger(InitData.class);
	
	@Autowired
	RedisTemplate redisTemplate;

	@PostConstruct
	public void initRedisShopNum(){
		redisTemplate.opsForValue().set("shopNum", shopNum,2,TimeUnit.MINUTES);
		logger.info("商品的库存数量为:{}",redisTemplate.opsForValue().get("shopNum"));
	}
}

2、接口实现-不加锁的情况

import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.alibaba.fastjson.JSONObject;
import com.zhouzy.boot.zhouzyBoot.model.User;

@RestController
public class ApiController {
	private static Logger logger = LoggerFactory.getLogger(ApiController.class);
	@Autowired
	RedisTemplate redisTemplate;

	@SuppressWarnings("unchecked")
	@RequestMapping(value = "/api/miaosha",method =RequestMethod.POST)
    public String miaosha(@RequestBody User user,HttpServletRequest request){
		int shopNum = Integer.valueOf(redisTemplate.opsForValue().get("shopNum").toString());
		logger.info("当前商品数量:{}",shopNum);
		if(shopNum <= 0){
			 logger.info("商品数量不够了,还有:{}",redisTemplate.opsForValue().get("shopNum"));
			 return "fail";
		}
		//每个用户只能抢一次,
		String userId = user.getUserId();
		Integer num = redisTemplate.opsForValue().get(userId) == null ? 0 : Integer.valueOf(redisTemplate.opsForValue().get(userId).toString());
        if(num > 0){
        	logger.info("用户:{},已经抢过了,当前数量:{}",userId,num);
        	return "fail";
        }
        num = num + 1;
        shopNum = shopNum - 1;
        //这个是没有考虑并发的问题场景,可以试试看结果如何
        redisTemplate.opsForValue().set(userId, num,2,TimeUnit.MINUTES);
        redisTemplate.opsForValue().set("shopNum", shopNum,2,TimeUnit.MINUTES);
        logger.info("用户:{},抢成功了,当前数量:{}",userId,num);
        shopNum = Integer.valueOf(redisTemplate.opsForValue().get("shopNum").toString());
        //库存数量
        logger.info("商品数量还有:{}",shopNum);
        return "success";
    }
	
	 
}

3、模拟高并发测试

利用CyclicBarrier 栅栏,100个请求同时调用

package com.zhouzy.boot.zhouzyBoot;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.junit.Test;

public class MiaoshaTest {
	//JUC工具类 栅栏,实现满一定数量线程后往下执行,跟countdownlatch不同的是,这个可以重复使用
	private static CyclicBarrier cyclicBarrier = new CyclicBarrier(100);


	@Test
	public void test() {
		ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i=0;i<200;i++){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            final int num = i%50;//这样是为了让一批线程里有两个相同的用户去竞争,主要是为了测试并发
            executorService.execute(()->{
                try {
                    play("userId"+num,"name"+num);
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
	}
	
	public static void play(String userId,String name) throws BrokenBarrierException, InterruptedException {
        System.out.println(Thread.currentThread().getName() + " 已准备");
        cyclicBarrier.await();
        
        Map<String,String> map = new HashMap();
        map.put("userId", userId);
        map.put("name", name);
        HttpClientUtil.doPost("http://127.0.0.1:8081/api/miaosha", map, "UTF-8");
        System.out.println(Thread.currentThread().getName() + " 开始执行");
    }

}

4、测试结果

商品数量竟然未减少

5、使用redis watch功能 

package com.zhouzy.boot.zhouzyBoot.controller;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.RandomUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.alibaba.fastjson.JSONObject;
import com.zhouzy.boot.zhouzyBoot.model.User;

@RestController
public class ApiController {
	private static Logger logger = LoggerFactory.getLogger(ApiController.class);
	@Autowired
	RedisTemplate redisTemplate;

	@SuppressWarnings("unchecked")
	@RequestMapping(value = "/api/miaosha",method =RequestMethod.POST)
    public String miaosha(@RequestBody User user,HttpServletRequest request) throws Exception{
		//每个用户只能抢一次,
		String userId = user.getUserId();
		int shopNum = Integer.valueOf(redisTemplate.opsForValue().get("shopNum").toString());
		logger.info("当前商品数量:{}",shopNum);
		if(Integer.valueOf(redisTemplate.opsForValue().get("shopNum").toString()) <= 0){
			 logger.info("商品数量不够了,还有:{}",redisTemplate.opsForValue().get("shopNum"));
			 return "fail";
		}
		Integer num = redisTemplate.opsForValue().get(userId) == null ? 0 : Integer.valueOf(redisTemplate.opsForValue().get(userId).toString());
        if(num > 0){
        	logger.info("用户:{},已经抢过了,当前数量:{}",userId,num);
        	return null;
        }
        // 开启事务支持,在同一个 Connection 中执行命令
    	
		redisTemplate.execute((RedisOperations operations) -> {
			//特别注意,在事务方法块里都用operations操作数据,而不是用redisTemplate
			//这个是没有考虑并发的问题场景,可以试试看结果如何
	    	List<Object> result = null;
			do{
    			operations.watch("shopNum");
    			int shopNum2 = Integer.valueOf(String.valueOf(operations.opsForValue().get("shopNum")));
    			int num2 = operations.opsForValue().get(userId) == null ? 0 : Integer.valueOf(operations.opsForValue().get(userId).toString());
	        	if(num2 > 0){
	        		logger.info("用户:{},已经抢过了,当前数量:{}",userId,num2);
	            	break;
	        	}
	            //shopNum = shopNum - 1;
	        	operations.multi();//开启事务
	        	operations.opsForValue().set(userId, num2+1);
	        	operations.opsForValue().set("shopNum", shopNum2-1);
	        	result = operations.exec();//执行
	        	logger.info(JSONObject.toJSONString(result));
	        	try {
					Thread.sleep(RandomUtils.nextInt(1, 100));
				} catch (Exception e) {
					e.printStackTrace();
				}
			}while (CollectionUtils.isEmpty(result)); //如果失败则重试,注意不是null,而是判断非空,包括长度为0
			
			return result;
        });
		shopNum = Integer.valueOf(redisTemplate.opsForValue().get("shopNum").toString());
		logger.info("执行后当前商品数量:{}",shopNum);
        return "success";
    }
	
	 
}

利用redis的watch功能配合事务,会存在一次不成功多次重试的问题,这是乐观锁不能避免的,代码里需要注意的地方都添加了注释,执行结果如下 

从执行结果看,充分提现出了redis乐观锁的特征,50个用户200次并发请求, 每个用户一个,最终库存60个数据正常!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wwwzhouzy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值