天天看点

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

到此,基本完成了,时间不早了,休息了,明天还要上班。

继续阅读