1. 程式人生 > >springboot 整合 redis 主從同步 sentinel哨兵 實現商品搶購秒殺

springboot 整合 redis 主從同步 sentinel哨兵 實現商品搶購秒殺

這段時間利用週末學習了一下redis的主從同步,同時配合sentinel哨兵機制,sentinel是redis實現HA的一種方式。為了能學以致用,這裡使用springboot 把redis 主從同步及sentinel整合起來實現一個商品搶購的demo,同時在開發過程中遇到的問題也都整理下來了。一方面加深對所學知識的印象,另一方面希望可以為那些剛接觸過這些知識的同學提供點實際幫助,springboot 整合redis主從及sentinel哨兵,自己從零開始,也花了一些時間,當然對那些大牛來說,此文可以繞過。

一、redis主從及sentinel環境配置

1、官方網站下載redis  https://redis.io/download
2、解壓壓縮包
  tar -xvf redis-4.0.1.tar.gz

3、解壓完成後,進入目錄 redis-4.0.1
cd redis-4.0.1

4、執行make命令
make
執行make報錯,提示cc:未找到命令,原因是虛擬機器系統中缺少gcc,安裝gcc即可
   
虛擬機器安裝gcc命令
  安裝命令:yum -y install gcc automake autoconf libtool make 

安裝gcc後,執行make 繼續報錯:zmalloc.h:50:31: 致命錯誤:jemalloc/jemalloc.h:沒有那個檔案或目錄

解決方案:
執行命令由make  改成  make MALLOC=libc

5、安裝redis服務到指定的目錄  /usr/local/redis
make PREFIX=/usr/local/redis install

6、建立配置檔案
mkdir /etc/redis
 
複製配置檔案到/etc/redis/ 下面去
cp redis.cnf  /etc/redis/

7、啟動redis客戶端  進入redis 的bin 目錄
./redis-server

8、檢視redis是否正常啟動
ps -ef | grep redis

或者檢視redis埠是否被監聽
netstat -tunple | grep 6379

9、修改redis 配置檔案  後臺啟動
vi /etc/redis/redis.conf
修改daemonize no   將no改成yes  即可

./redis-server  /etc/redis/redis-conf    使用配置檔案後臺啟動

10、關閉linux後臺執行的redis服務
進入 bin 目錄
使用 pkill 命令
pkill -9 redis-server

11、redis客戶端連線
./redis-cli -h 192.168.137.30 -p 6379
set name hello
get name

12、redis 主從同步,slave啟動時,會給master傳送一個同步命令,然後master以檔案的形式同步給slave;
第一次是全量同步,以後會以增量的形式同步,
master同步時資料是非阻塞的,slave同步時時阻塞的(當slave正在同步時,如果應用傳送請求過來,必須等slave同步完之後,才能接受請求)

哨兵機制 切換回來之前的主從 一是修改sentinel配置檔案,二是關掉sentinel程序,重啟redis主從
master  讀寫並行
slave  只讀
    ./redis-cli -h 192.168.137.30 -p 6379 客戶端連線
  
進入後
192.168.137.30:6379> info  檢視配置資訊

13、哨兵後臺啟動 
修改配置檔案sentinel.conf 增加 daemonize yes

啟動  ./redis-sentinel /etc/redis/sentinel.conf

通過以上步驟,redis環境配置基本上就完成了。

開始建立springboot專案,這裡使用的是idea,新建project-->Spring Initializr 然後next,定義相關包名 路徑即可.

1、application.yml配置如下:

spring:
  redis:
    hostName: 192.168.137.30
    port: 6379
    password:
    pool:
      maxActive: 200
      maxWait: -1
      maxIdle: 8
      minIdle: 0
    timeout: 0
    database: 
0 sentinel: master: mymaster nodes: 192.168.137.32 port: 26379 server: port: 8080

2、pom.xml增加redis依賴

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
   <version>1.5.6.RELEASE</
version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>

3、RedisConfig.java redis相關配置類,容器啟動時會載入。

@Configuration
@EnableAutoConfiguration
public class RedisConfig {
    private static Logger logger = LoggerFactory.getLogger(RedisConfig.class);

    @Value("${spring.redis.sentinel.master}")
    private String master;

    @Value("${spring.redis.sentinel.nodes}")
    private String sentinelHost;

    @Value("${spring.redis.sentinel.port}")
    private Integer sentinelPort;


    @Bean
    @ConfigurationProperties(prefix="spring.redis")
    public JedisPoolConfig getRedisConfig(){
        JedisPoolConfig config = new JedisPoolConfig();
        return config;
    }

    @Bean
    @ConfigurationProperties(prefix="spring.redis")
    public JedisConnectionFactory getConnectionFactory(){
        JedisPoolConfig config = getRedisConfig();
        JedisConnectionFactory factory = new JedisConnectionFactory(getRedisSentinelConfig(), config);
        factory.setPoolConfig(config);
        return factory;
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.sentinel")
    public RedisSentinelConfiguration getRedisSentinelConfig(){
        RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration();
        sentinelConfiguration.setMaster(master);
        sentinelConfiguration.sentinel(sentinelHost,sentinelPort);
        return sentinelConfiguration;
    }


    @Bean
public RedisTemplate<?, ?> getRedisTemplate(){
        RedisTemplate<?,?> redisTemplate = new StringRedisTemplate(getConnectionFactory());
        return redisTemplate;
    }
}

4、redis服務介面,這裡只寫了一個下單是否成功方法

public interface IRedisService {

    public boolean isOrderSuccess(int buyCount,long flashSellEndDate);

}

5、redis服務介面的實現類

@Service
public class RedisServiceImpl implements IRedisService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
RedisTemplate redisTemplate;

    private static final int GOODS_TOTAL_COUNT = 200;//商品總數量
private static final String LOCK_KEY = "checkLock";//執行緒鎖key
private static final String SELL_COUNT_KEY = "sellCountKeyNew2";//redis中存放的已賣數量的key
private static final int LOCK_EXPIRE = 6 * 1000; //鎖佔有時長
/**
     * 檢查下單是否成功
* @param buyCount 購買數量
* @param flashSellEndDate 截止時間
* @return
*/
public boolean isOrderSuccess(int buyCount,long flashSellEndDate) {

        if(flashSellEndDate <= 0){
            logger.info("搶購活動已經結束:" + flashSellEndDate);
            return false;
        }

        boolean resultFlag = false;
        try {
            if (redisLock(LOCK_KEY, LOCK_EXPIRE)) {
                Integer haveSoldCount = (Integer) this.getValueByKey(SELL_COUNT_KEY);
                Integer totalSoldCount = (haveSoldCount == null ? 0 : haveSoldCount) + buyCount;
                if (totalSoldCount <= GOODS_TOTAL_COUNT) {
                    this.setKeyValueWithExpire(SELL_COUNT_KEY, totalSoldCount, flashSellEndDate);
                    resultFlag = true;
                    logger.info("已買數量: = " + totalSoldCount);
                    logger.info("剩餘數量:= " + (GOODS_TOTAL_COUNT - totalSoldCount));
                }else{
                    if(haveSoldCount < GOODS_TOTAL_COUNT){
                        logger.info("對不起,您購買的數量已經超出商品庫存數量,請重新下單.");
                    }else{
                        logger.info("對不起,商品已售完.");
                    }
                }
                this.removeByKey(LOCK_KEY);
            } else {
                Integer soldCount = (Integer) this.getValueByKey(LOCK_KEY);
                if(soldCount != null && soldCount >= GOODS_TOTAL_COUNT){
                    //商品已經售完
logger.info("all goods have sold out");
                    return false;
                }
                Thread.sleep(1000);//沒有獲取到鎖 1s後重試
return isOrderSuccess(buyCount,flashSellEndDate);
            }
        }catch (Exception e){
            e.printStackTrace();
        }

        return resultFlag;
    }


    /**
     *  redis * @param lock 鎖的key
     * @param expire 鎖的時長
* @return
*/
public Boolean redisLock(final String lock, final int expire) {

        return (Boolean) redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                boolean locked = false;
                byte[] lockKeyName = redisTemplate.getStringSerializer().serialize(lock);
                byte[] lockValue = redisTemplate.getValueSerializer().serialize(getDateAferExpire(expire));
                locked = connection.setNX(lockKeyName, lockValue);
                if (locked){
                    connection.expire(lockKeyName, TimeoutUtils.toSeconds(expire, TimeUnit.MILLISECONDS));
                }
                return locked;
            }
        });
    }


    /**
     *
     *  判斷key是否存在
* @param key
*/
public boolean existsKey(final String key) {

        return redisTemplate.hasKey(key);
    }

    /**
     * 獲取指定時長後的Date
     * @param expireTime
* @return
*/
public Date getDateAferExpire(int expireTime){
        Calendar calendar = Calendar.getInstance();

        calendar.add(Calendar.MILLISECOND, expireTime);

        return calendar.getTime();
    }

    /**
     * 根據key 獲取對應的value
     *
     * @param key
*/
public Object getValueByKey(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }
    /**
     * 刪除指定的key
     *
     * @param key
*/
public void removeByKey(final String key) {
        if (existsKey(key)) {
            redisTemplate.delete(key);
        }
    }

    /**
     * 設定帶有指定時長的key value
     * @param key
* @param value
* @param expireTime
* @return
*/
public boolean setKeyValueWithExpire(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            if (expireTime != null) {
                redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            }
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

}

6、測試類TestOrderController

@RestController
public class TestOrderController {

    @Autowired
IRedisService redisService;

    private int buyCount = 20;

    private static final int TEST_NUM = 15;

    private static final String SELL_END_DATE = "2017-09-24 23:50:00";

    private CountDownLatch cdl = new CountDownLatch(TEST_NUM);

    @RequestMapping("orderTest/{buyCountParam}")
    public String orderTest(@PathVariable int buyCountParam){
        buyCount = buyCountParam;
        for (int i=0; i<TEST_NUM; i++){
            new Thread(new MyThread()).start();
            cdl.countDown();
        }

        return "success";

    }

    private class MyThread implements Runnable{

        @Override
public void run() {
            try {
                cdl.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Calendar calendar = Calendar.getInstance();

            Calendar calendar1 = Calendar.getInstance();
            try {
                calendar1.setTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(SELL_END_DATE));
            } catch (ParseException e) {
                e.printStackTrace();
            }
            redisService.isOrderSuccess(buyCount,(calendar1.getTime().getTime() - calendar.getTime().getTime()) / 1000);

        }
    }
}

7、測試地址:http://localhost:8080/orderTest/20

控制檯輸出:

2017-09-24 22:58:57.069  INFO 3856 --- [      Thread-35] c.h.h.miaosha.service.RedisServiceImpl   : 已買數量: = 20
2017-09-24 22:58:57.069  INFO 3856 --- [      Thread-35] c.h.h.miaosha.service.RedisServiceImpl   : 剩餘數量:= 180
2017-09-24 22:59:03.081  INFO 3856 --- [      Thread-37] c.h.h.miaosha.service.RedisServiceImpl   : 已買數量: = 40
2017-09-24 22:59:03.081  INFO 3856 --- [      Thread-37] c.h.h.miaosha.service.RedisServiceImpl   : 剩餘數量:= 160
2017-09-24 22:59:09.096  INFO 3856 --- [      Thread-25] c.h.h.miaosha.service.RedisServiceImpl   : 已買數量: = 60
2017-09-24 22:59:09.097  INFO 3856 --- [      Thread-25] c.h.h.miaosha.service.RedisServiceImpl   : 剩餘數量:= 140
2017-09-24 22:59:15.120  INFO 3856 --- [      Thread-32] c.h.h.miaosha.service.RedisServiceImpl   : 已買數量: = 80
2017-09-24 22:59:15.120  INFO 3856 --- [      Thread-32] c.h.h.miaosha.service.RedisServiceImpl   : 剩餘數量:= 120
2017-09-24 22:59:21.141  INFO 3856 --- [      Thread-29] c.h.h.miaosha.service.RedisServiceImpl   : 已買數量: = 100
2017-09-24 22:59:21.141  INFO 3856 --- [      Thread-29] c.h.h.miaosha.service.RedisServiceImpl   : 剩餘數量:= 100
2017-09-24 22:59:27.154  INFO 3856 --- [      Thread-38] c.h.h.miaosha.service.RedisServiceImpl   : 已買數量: = 120
2017-09-24 22:59:27.154  INFO 3856 --- [      Thread-38] c.h.h.miaosha.service.RedisServiceImpl   : 剩餘數量:= 80
2017-09-24 22:59:33.172  INFO 3856 --- [      Thread-26] c.h.h.miaosha.service.RedisServiceImpl   : 已買數量: = 140
2017-09-24 22:59:33.172  INFO 3856 --- [      Thread-26] c.h.h.miaosha.service.RedisServiceImpl   : 剩餘數量:= 60
2017-09-24 22:59:39.180  INFO 3856 --- [      Thread-31] c.h.h.miaosha.service.RedisServiceImpl   : 已買數量: = 160
2017-09-24 22:59:39.180  INFO 3856 --- [      Thread-31] c.h.h.miaosha.service.RedisServiceImpl   : 剩餘數量:= 40
2017-09-24 22:59:45.190  INFO 3856 --- [      Thread-36] c.h.h.miaosha.service.RedisServiceImpl   : 已買數量: = 180
2017-09-24 22:59:45.190  INFO 3856 --- [      Thread-36] c.h.h.miaosha.service.RedisServiceImpl   : 剩餘數量:= 20
2017-09-24 22:59:51.210  INFO 3856 --- [      Thread-34] c.h.h.miaosha.service.RedisServiceImpl   : 已買數量: = 200
2017-09-24 22:59:51.211  INFO 3856 --- [      Thread-34] c.h.h.miaosha.service.RedisServiceImpl   : 剩餘數量:= 0
2017-09-24 22:59:57.234  INFO 3856 --- [      Thread-27] c.h.h.miaosha.service.RedisServiceImpl   : 對不起,商品已售完.
2017-09-24 23:00:03.243  INFO 3856 --- [      Thread-28] c.h.h.miaosha.service.RedisServiceImpl   : 對不起,商品已售完.
2017-09-24 23:00:09.254  INFO 3856 --- [      Thread-24] c.h.h.miaosha.service.RedisServiceImpl   : 對不起,商品已售完.
2017-09-24 23:00:15.266  INFO 3856 --- [      Thread-30] c.h.h.miaosha.service.RedisServiceImpl   : 對不起,商品已售完.
2017-09-24 23:00:21.275  INFO 3856 --- [      Thread-33] c.h.h.miaosha.service.RedisServiceImpl   : 對不起,商品已售完.

8、進入sentinel客戶端 可以檢視:

info

當前master地址: 192.168.137.30 : 6379

# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=192.168.137.30:6379,slaves=1,sentinels=1

此時kill掉當前的master

檢視sentinel日誌:

2934:X 24 Sep 23:03:21.659 # +switch-master mymaster 192.168.137.30 6379 192.168.137.31 6379
2934:X 24 Sep 23:03:21.659 * +slave slave 192.168.137.30:6379 192.168.137.30 6379 @ mymaster 192.168.137.31 6379
2934:X 24 Sep 23:03:51.723 # +sdown slave 192.168.137.30:6379 192.168.137.30 6379 @ mymaster 192.168.137.31 6379

可以看到sentinel已經進行了 switch-master 將之前的slave切換成master,而之前的master則轉換成了slave

專案控制檯輸出:

2017-09-24 23:06:27.336  INFO 3856 --- [8.137.32:26379]] redis.clients.jedis.JedisSentinelPool    : Created JedisPool to master at 192.168.137.31:6379

這個時候也可以看到master由之前的192.168.137.30:6379 切換成 192.168.137.31:6379

到此,基本完成了,時間不早了,休息了,明天還要上班。