您的位置:首页 > 数据库 > Redis

springboot 整合 redis 主从同步 sentinel哨兵 实现商品抢购秒杀

2017-09-24 22:01 1236 查看
这段时间利用周末学习了一下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

到此,基本完成了,时间不早了,休息了,明天还要上班。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: