最近公司有一個需求:遍歷網站所有用戶,分幾個指標對用戶進行劃分,並對其進行一些操作,其中這些指標中就有一個是用戶最近三個月的登錄情況。
如果網站用戶量大,且lv較高的話,遍歷用戶select一個log庫,必然低效;有的人提出用一個字串進行表示,比如0010表示今天未登錄,昨天登錄,倒退兩天都未登錄,這種方式可行,但是如果記錄90天,每個用戶都必須記錄一個長度爲90的varchar,而且用java操作String比較複雜,也相當消耗內存;而我的想法就是二進制進行記錄。
我最開始想設計一個表,uid爲主鍵,一個整形字段來表示用戶行爲:昨天登錄,今天登錄即爲11B=3;昨天登錄,今天未登錄即爲10B=2;昨天未登錄,今天登錄即爲01B=1;昨天未登錄,今天未登錄即爲00B=0,這樣數據庫中只需要存儲一個整數就OK了。
但是在java中一個long才64位,怎樣才能記錄90天甚至更長時間呢?沒用過bigint,不知道其效率如何,我後來想了一個辦法,用int數組,如果數組的長度設置成12或者更多,甚至都能精確記錄用戶一年的行爲,經過一番研究修改與優化,最終終於做出來一套算法了,問題解決了,但缺點是代碼可讀性差,不知道符不符合算法的設計原理
以下就是代碼
一、定義操作方法:
package com.dajie.centre.mail.utils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public abstract class ActionService {
protected static final long A_DAY = 24 * 3600 * 1000L;
protected static final long EIGHT_HOUR = 8 * 3600 * 1000L;
private static final Map<String, Integer> datePool = new HashMap<String, Integer>();
private static final Map<Integer, String> dayPool = new HashMap<Integer, String>();
private static final Map<String, Integer> monthPool = new HashMap<String, Integer>();
private static final Map<String, Integer> dayOfMaonthPool = new HashMap<String, Integer>();
private static final String FORMAT = "yyyy-MM-dd";
protected String statDay;
protected int statDayOf;
protected int nums;
/**
* 記錄登錄
*
* @param actions
* @param date
* @param isLog
* @return
*/
public abstract int[] login(int[] actions, boolean isLog);
/**
* 記錄登錄
*
* @param actions
* @param date
* @param isLog
* @return
*/
public abstract int[] login(int[] actions, String date, boolean isLog);
/**
* 登錄記錄由List到int數組
*
* @param actions
* @param date
* @param isLog
* @return
*/
public abstract int[] login(Collection<String> dates);
/**
* 登錄記錄由int數組到List
*
* @param actions
* @return
*/
public abstract List<String> loginDates(int[] actions);
/**
* 登錄記錄由字符串到數組的轉換
*
* @param actions
* @param date
* @param isLog
* @return
*/
public int[] login(String actionStrs) {
String[] actionStr = actionStrs.split(",");
int length = actionStr.length;
int[] actions = new int[length];
for (int i = 0; i < length; i++) {
actions[i] = Integer.parseInt(actionStr[i]);
}
return actions;
}
/**
* 登錄記錄由數組到字符串的轉換
*
* @param actions
* @param date
* @param isLog
* @return
*/
public String loginStr(int[] actions) {
int length = actions.length;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(actions[i]);
if (i != length - 1) {
sb.append(",");
}
}
return sb.toString();
}
/**
* 查看最近幾天內的登錄次數
*
* @param actions
* @param days
* @return
*/
public abstract int loginCounts(int[] actions, int startDay, int endDay, int weekend);
/**
* 查看是否是週末
*
* @param dayOf
* @return
*/
protected boolean isWeekend(int dayOf) {
return dayOf % 7 == 3 || dayOf % 7 == 2;
}
/**
* 查看日期距離1970-01-01有多少天
*
* @param date
* @return
*/
protected int getDayOf(String date) {
date = date.trim();
Integer dayOf = datePool.get(date);
if (dayOf == null) {
try {
Date day = new SimpleDateFormat(FORMAT).parse(date);
dayOf = ((int) ((day.getTime() + EIGHT_HOUR) / (A_DAY)));
datePool.put(date, dayOf);
dayPool.put(dayOf, date);
} catch (ParseException e) {
e.printStackTrace();
}
}
return dayOf;
}
/**
* 查看日期距離1970-01-01有多少天
*
* @param date
* @return
*/
protected String getDateOf(int dayOf) {
String date = dayPool.get(dayOf);
if (date == null || "".equals(date)) {
Date day = new Date(A_DAY * dayOf + EIGHT_HOUR);
date = new SimpleDateFormat(FORMAT).format(day);
datePool.put(date, dayOf);
dayPool.put(dayOf, date);
}
return date;
}
/**
* 查看日期的月份
*
* @param date
* @return
*/
public int getMonth(String date) {
date = date.trim();
Integer month = monthPool.get(date);
if (month == null) {
try {
Date day = new SimpleDateFormat(FORMAT).parse(date);
Calendar cal = Calendar.getInstance();
cal.setTime(day);
month = cal.get(Calendar.MONTH);
monthPool.put(date, month);
dayOfMaonthPool.put(date, cal.get(Calendar.DAY_OF_MONTH) - 1);
} catch (ParseException e) {
e.printStackTrace();
}
}
return month;
}
/**
* 查看日期是一個月的第多少天
*
* @param date
* @return
*/
public int getDayOfMonth(String date) {
date = date.trim();
Integer dayOfMonth = dayOfMaonthPool.get(date);
if (dayOfMonth == null) {
try {
Date day = new SimpleDateFormat(FORMAT).parse(date);
Calendar cal = Calendar.getInstance();
cal.setTime(day);
dayOfMonth = cal.get(Calendar.DAY_OF_MONTH) - 1;
monthPool.put(date, cal.get(Calendar.MONTH));
dayOfMaonthPool.put(date, dayOfMonth);
} catch (ParseException e) {
e.printStackTrace();
}
}
return dayOfMonth;
}
}
二、新建一個工廠類,添加一些操作功能方法,並實現這個接口:
package com.dajie.centre.mail.utils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class ActionFactory {
public static ActionService getInstance(String date) {
return new Action1(date);
}
private static class Action1 extends ActionService {
private int month = 0;
private int days = 0;
private Action1(String date) {
this.statDay = date;
this.statDayOf = getDayOf(date);
this.month = getMonth(date);
this.days = getDayOfMonth(date);
this.nums = 12;
}
@Override
public int[] login(int[] actions, boolean isLog) {
actions[month] = isLog ? actions[month] | (1 << days) : actions[month] & ~(1 << days);
return actions;
}
@Override
public int[] login(int[] actions, String date, boolean isLog) {
int thatDayOf = getDayOf(date);
if(statDayOf >= thatDayOf && statDayOf - thatDayOf < 366){
int thatMonth = getMonth(date);
int thatDayOfMonth = getDayOfMonth(date);
actions[thatMonth] = isLog ? actions[thatMonth] | (1 << thatDayOfMonth) : actions[thatMonth] & ~(1 << thatDayOfMonth);
}
return actions;
}
@Override
public int[] login(Collection<String> dates) {
int[] actions = new int[nums];
for (String date : dates) {
login(actions, date, true);
}
return actions;
}
@Override
public List<String> loginDates(int[] actions) {
List<String> dates = new ArrayList<String>();
int dayOf = getDayOf(statDay);
for (int i = 0; i < 366; i++) {
String date = getDateOf(dayOf);
int month = getMonth(date);
int dayOfMonth = getDayOfMonth(date);
if ((actions[month] & (1 << dayOfMonth)) > 0) {
dates.add(date);
}
dayOf--;
}
return dates;
}
@Override
public int loginCounts(int[] actions, int startDay, int endDay, int weekend) {
int logCnt = 0;
int days = endDay - startDay;
int dayOf = getDayOf(statDay) - startDay;
boolean once = false;
for (int i = 0; i < days; i++) {
String date = getDateOf(dayOf);
int month = getMonth(date);
int dayOfMonth = getDayOfMonth(date);
if ((actions[month] & (1 << dayOfMonth)) > 0) {
if (isWeekend(dayOf)) {
if (weekend == 1 && !once) {
logCnt++;
once = true;
}else if(weekend == 2){
logCnt++;
}else{
System.out.println(date);
}
} else {
logCnt++;
once = false;
}
} else {
once = false;
}
dayOf--;
}
return logCnt;
}
}
static void print(List<String> list) {
StringBuilder sb = new StringBuilder("dates : [");
int length = list.size();
for (int i = 0; i < length; i++) {
sb.append(list.get(i));
if (i != length - 1) {
sb.append(", ");
}
}
sb.append("]");
System.out.println(sb);
}
}
三、測試代碼:
測試一:
while (true) {
synchronized (RunningThread.class) {
uids = kpiDemoDao.getLoginUid(maxUid, ONCE);
if (!uids.isEmpty()) {
int maxTmpUid = Collections.max(uids);
maxUid = maxUid < maxTmpUid ? maxTmpUid : maxUid;
}
}
if (uids != null && !uids.isEmpty()) {
for (int uid : uids) {
List<String> dates = kpiDemoDao.getLoginLog(uid);
int size = dates.size();
if (size > 0) {
int[] actions = actionService.login(dates);
String lastDate = dates.get(size - 1);
emailCentreDao.deleteAction(uid);
emailCentreDao.insertAction(uid, actions, lastDate);
}
}
} else {
break;
}
}
測試二:
int month = actionService.getMonth(date);
int[] actions = new int[12];
actionService.login(actions, true);
log.info("size : " + uids.size());
for (int uid : uids) {
if (uid%THREAD == this.id) {
int update = emailCentreDao.updateAction(uid, month, actions[month], date);
if (update <= 0) {
emailCentreDao.insertAction(uid, actions, date);
}
}
}