这段时间利用周末学习了一下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
到此,基本完成了,时间不早了,休息了,明天还要上班。